From be22bb89db0b952ea0545e7c65b46e92ebb205d2 Mon Sep 17 00:00:00 2001 From: Kyren223 Date: Wed, 27 Nov 2024 17:34:28 +0200 Subject: Added create network to server api --- internal/data/networks.sql.go | 148 ++++++++++++++++++++++++++++------------- internal/packet/types.go | 29 +++++--- internal/server/api/api.go | 93 +++++++++++++++++++++++++- internal/server/api/helpers.go | 23 +++++++ internal/server/api/models.go | 15 +++++ internal/server/server.go | 3 + 6 files changed, 252 insertions(+), 59 deletions(-) create mode 100644 internal/server/api/helpers.go create mode 100644 internal/server/api/models.go (limited to 'internal') diff --git a/internal/data/networks.sql.go b/internal/data/networks.sql.go index 8064fa9..5e1e0db 100644 --- a/internal/data/networks.sql.go +++ b/internal/data/networks.sql.go @@ -64,7 +64,7 @@ func (q *Queries) DeleteNetwork(ctx context.Context, id snowflake.ID) error { return err } -const getBannedUsersInNetwork = `-- name: GetBannedUsersInNetwork :many +const getNetworkBannedUsers = `-- name: GetNetworkBannedUsers :many SELECT users.id, users.name, users.public_key, users.description, users.is_public_dm, users.is_deleted, users_networks.ban_reason @@ -73,20 +73,20 @@ JOIN users ON users.id = users_networks.user_id WHERE users_networks.network_id = ? ` -type GetBannedUsersInNetworkRow struct { +type GetNetworkBannedUsersRow struct { User User BanReason *string } -func (q *Queries) GetBannedUsersInNetwork(ctx context.Context, networkID snowflake.ID) ([]GetBannedUsersInNetworkRow, error) { - rows, err := q.db.QueryContext(ctx, getBannedUsersInNetwork, networkID) +func (q *Queries) GetNetworkBannedUsers(ctx context.Context, networkID snowflake.ID) ([]GetNetworkBannedUsersRow, error) { + rows, err := q.db.QueryContext(ctx, getNetworkBannedUsers, networkID) if err != nil { return nil, err } defer rows.Close() - var items []GetBannedUsersInNetworkRow + var items []GetNetworkBannedUsersRow for rows.Next() { - var i GetBannedUsersInNetworkRow + var i GetNetworkBannedUsersRow if err := rows.Scan( &i.User.ID, &i.User.Name, @@ -129,43 +129,7 @@ func (q *Queries) GetNetworkById(ctx context.Context, id snowflake.ID) (Network, return i, err } -const getPublicNetworks = `-- name: GetPublicNetworks :many -SELECT id, owner_id, name, icon, bg_hex_color, fg_hex_color, is_public FROM networks -WHERE is_public = true -` - -func (q *Queries) GetPublicNetworks(ctx context.Context) ([]Network, error) { - rows, err := q.db.QueryContext(ctx, getPublicNetworks) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Network - for rows.Next() { - var i Network - if err := rows.Scan( - &i.ID, - &i.OwnerID, - &i.Name, - &i.Icon, - &i.BgHexColor, - &i.FgHexColor, - &i.IsPublic, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const getUsersInNetwork = `-- name: GetUsersInNetwork :many +const getNetworkMembers = `-- name: GetNetworkMembers :many SELECT users.id, users.name, users.public_key, users.description, users.is_public_dm, users.is_deleted, users_networks.joined_at, @@ -176,22 +140,22 @@ JOIN users ON users.id = users_networks.user_id WHERE users_networks.network_id = ? AND is_member = true ` -type GetUsersInNetworkRow struct { +type GetNetworkMembersRow struct { User User JoinedAt string IsAdmin bool IsMuted bool } -func (q *Queries) GetUsersInNetwork(ctx context.Context, networkID snowflake.ID) ([]GetUsersInNetworkRow, error) { - rows, err := q.db.QueryContext(ctx, getUsersInNetwork, networkID) +func (q *Queries) GetNetworkMembers(ctx context.Context, networkID snowflake.ID) ([]GetNetworkMembersRow, error) { + rows, err := q.db.QueryContext(ctx, getNetworkMembers, networkID) if err != nil { return nil, err } defer rows.Close() - var items []GetUsersInNetworkRow + var items []GetNetworkMembersRow for rows.Next() { - var i GetUsersInNetworkRow + var i GetNetworkMembersRow if err := rows.Scan( &i.User.ID, &i.User.Name, @@ -216,6 +180,42 @@ func (q *Queries) GetUsersInNetwork(ctx context.Context, networkID snowflake.ID) return items, nil } +const getPublicNetworks = `-- name: GetPublicNetworks :many +SELECT id, owner_id, name, icon, bg_hex_color, fg_hex_color, is_public FROM networks +WHERE is_public = true +` + +func (q *Queries) GetPublicNetworks(ctx context.Context) ([]Network, error) { + rows, err := q.db.QueryContext(ctx, getPublicNetworks) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Network + for rows.Next() { + var i Network + if err := rows.Scan( + &i.ID, + &i.OwnerID, + &i.Name, + &i.Icon, + &i.BgHexColor, + &i.FgHexColor, + &i.IsPublic, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const setNetworkIcon = `-- name: SetNetworkIcon :one UPDATE networks SET icon = ?, @@ -306,6 +306,58 @@ func (q *Queries) SetNetworkName(ctx context.Context, arg SetNetworkNameParams) return i, err } +const setNetworkUser = `-- name: SetNetworkUser :one +INSERT INTO users_networks ( + user_id, network_id, + is_member, is_admin, is_muted, + is_banned, ban_reason +) VALUES ( + ?, ?, + ?, ?, ?, + ?, ? +) +ON CONFLICT DO +UPDATE SET + is_member = ?, is_admin = ?, is_muted = ?, + is_banned = ?, ban_reason = ? +WHERE user_id = ? AND network_id = ? +RETURNING user_id, network_id, joined_at, is_member, is_admin, is_muted, is_banned, ban_reason +` + +type SetNetworkUserParams struct { + UserID snowflake.ID + NetworkID snowflake.ID + IsMember bool + IsAdmin bool + IsMuted bool + IsBanned bool + BanReason *string +} + +func (q *Queries) SetNetworkUser(ctx context.Context, arg SetNetworkUserParams) (UsersNetwork, error) { + row := q.db.QueryRowContext(ctx, setNetworkUser, + arg.UserID, + arg.NetworkID, + arg.IsMember, + arg.IsAdmin, + arg.IsMuted, + arg.IsBanned, + arg.BanReason, + ) + var i UsersNetwork + err := row.Scan( + &i.UserID, + &i.NetworkID, + &i.JoinedAt, + &i.IsMember, + &i.IsAdmin, + &i.IsMuted, + &i.IsBanned, + &i.BanReason, + ) + return i, err +} + const transferNetwork = `-- name: TransferNetwork :one UPDATE networks SET owner_id = ? diff --git a/internal/packet/types.go b/internal/packet/types.go index 405513e..031e983 100644 --- a/internal/packet/types.go +++ b/internal/packet/types.go @@ -26,8 +26,8 @@ func (m *CreateNetwork) Type() PacketType { } type UpdateNetwork struct { - Network snowflake.ID CreateNetwork + Network snowflake.ID } func (m *UpdateNetwork) Type() PacketType { @@ -52,25 +52,34 @@ func (m *DeleteNetwork) Type() PacketType { } type SetNetworkUser struct { - Network snowflake.ID - User snowflake.ID Member *bool Admin *bool Muted *bool Banned *bool BanReason *string + Network snowflake.ID + User snowflake.ID } func (m *SetNetworkUser) Type() PacketType { return PacketSetNetworkUser } +type Member struct { + JoinedAt string + User data.User + IsAdmin bool + IsMuted bool +} + +type FullNetwork struct { + Network data.Network + Frequencies []data.Frequency + Members []Member +} + type NetworksInfo struct { - Networks []struct { - Network data.Network - Frequencies []data.Frequency - Members []data.User - } + Networks []FullNetwork } func (m *NetworksInfo) Type() PacketType { @@ -78,9 +87,9 @@ func (m *NetworksInfo) Type() PacketType { } type CreateFrequency struct { - Network snowflake.ID Name string HexColor string + Network snowflake.ID Perms int } @@ -89,9 +98,9 @@ func (m *CreateFrequency) Type() PacketType { } type UpdateFrequency struct { - Frequency snowflake.ID Name string HexColor string + Frequency snowflake.ID Perms byte } diff --git a/internal/server/api/api.go b/internal/server/api/api.go index 6c8d51a..f320838 100644 --- a/internal/server/api/api.go +++ b/internal/server/api/api.go @@ -4,6 +4,7 @@ import ( "context" "crypto/ed25519" "database/sql" + "fmt" "log" "strconv" "strings" @@ -14,6 +15,8 @@ import ( "github.com/kyren223/eko/pkg/snowflake" ) +var internalError = packet.Error{Error: "internal server error"} + func SendMessage(ctx context.Context, sess *session.Session, request *packet.SendMessage) packet.Payload { if (request.ReceiverID != nil) == (request.FrequencyID != nil) { return &packet.Error{Error: "either receiver id or frequency id must exist"} @@ -34,7 +37,7 @@ func SendMessage(ctx context.Context, sess *session.Session, request *packet.Sen }) if err != nil { log.Println(sess.Addr(), "database error:", err, "in SendMessage") - return &packet.Error{Error: "internal server error"} + return &internalError } return &packet.MessagesInfo{Messages: []data.Message{message}} @@ -79,3 +82,91 @@ func CreateOrGetUser(ctx context.Context, node *snowflake.Node, pubKey ed25519.P } return user, nil } + +func CreateNetwork(ctx context.Context, sess *session.Session, request packet.CreateNetwork) packet.Payload { + name := strings.TrimSpace(request.Name) + if name == "" { + return &packet.Error{Error: "server name must not be blank"} + } + + if len(request.Icon) > MaxIconSize { + return &packet.Error{Error: fmt.Sprintf("icon is too large, must be smaller than %v bytes", MaxIconSize)} + } + + if ok, err := isValidHexColor(request.BgHexColor); !ok { + return &packet.Error{Error: err} + } + if ok, err := isValidHexColor(request.FgHexColor); !ok { + return &packet.Error{Error: err} + } + + tx, err := db.BeginTx(ctx, nil) + if err != nil { + log.Println("database error:", err) + return &internalError + } + + queries := data.New(db) + qtx := queries.WithTx(tx) + + network, err := qtx.CreateNetwork(ctx, data.CreateNetworkParams{ + ID: sess.Manager().Node().Generate(), + OwnerID: sess.ID(), + Name: name, + IsPublic: request.IsPublic, + Icon: request.Icon, + BgHexColor: request.BgHexColor, + FgHexColor: request.FgHexColor, + }) + if err != nil { + log.Println("database error:", err) + return &internalError + } + + frequency, err := qtx.CreateFrequency(ctx, data.CreateFrequencyParams{ + ID: sess.Manager().Node().Generate(), + NetworkID: network.ID, + Name: DefaultFrequencyName, + HexColor: nil, + Perms: PermReadWrite, + }) + if err != nil { + log.Println("database error:", err) + return &internalError + } + + networkUser, err := qtx.SetNetworkUser(ctx, data.SetNetworkUserParams{ + UserID: network.OwnerID, + NetworkID: network.ID, + IsMember: true, + IsAdmin: true, + IsMuted: false, + IsBanned: false, + BanReason: nil, + }) + if err != nil { + log.Println("database error:", err) + return &internalError + } + + user, err := qtx.GetUserById(ctx, network.OwnerID) + if err != nil { + log.Println("database error:", err) + return &internalError + } + + fullNetwork := packet.FullNetwork{ + Network: network, + Frequencies: []data.Frequency{frequency}, + Members: []packet.Member{{ + JoinedAt: networkUser.JoinedAt, + User: user, + IsAdmin: networkUser.IsAdmin, + IsMuted: networkUser.IsMuted, + }}, + } + return &packet.NetworksInfo{ + Networks: []packet.FullNetwork{fullNetwork}, + } +} + diff --git a/internal/server/api/helpers.go b/internal/server/api/helpers.go new file mode 100644 index 0000000..967f308 --- /dev/null +++ b/internal/server/api/helpers.go @@ -0,0 +1,23 @@ +package api + +import "strings" + +const hex = "0123456789abcdefABCDEF" + +func isValidHexColor(color string) (bool, string) { + if len(color) != 7 { + return false, "color must be hex with length of 7" + } + + if color[0] != '#' { + return false, "color must start with '#'" + } + + for _, c := range color { + if !strings.ContainsRune(hex, c) { + return false, "color must start with '#' and contain exactly 6 digits 0-9, a-f, A-F" + } + } + + return true, "" +} diff --git a/internal/server/api/models.go b/internal/server/api/models.go new file mode 100644 index 0000000..317ee6e --- /dev/null +++ b/internal/server/api/models.go @@ -0,0 +1,15 @@ +package api + +const ( + MaxIconSize = 16 + DefaultFrequencyName = "main" +) + +const ( + PermNoAccess = 0 + iota + PermRead + PermReadWrite + PermMax +) + + diff --git a/internal/server/server.go b/internal/server/server.go index 5f4376a..c15414f 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -289,6 +289,8 @@ func processRequest(ctx context.Context, sess *session.Session, request packet.P // TODO: add a way to measure the time each request/response took and log it // Potentially even separate time for code vs DB operations switch request := request.(type) { + case *packet.CreateNetwork: + return timeout(20*time.Millisecond, api.CreateNetwork, ctx, sess, request) case *packet.SendMessage: return timeout(20*time.Millisecond, api.SendMessage, ctx, sess, request) case *packet.RequestMessages: @@ -303,6 +305,7 @@ func timeout[T packet.Payload]( apiRequest func(context.Context, *session.Session, T) packet.Payload, ctx context.Context, sess *session.Session, request T, ) packet.Payload { + // TODO: Remove the channel and just wait directly? responseChan := make(chan packet.Payload) ctx, cancel := context.WithTimeout(ctx, timeoutDuration) defer cancel() -- cgit v1.3.1