diff --git a/cache.go b/cache.go index 2402e38..3efd49a 100644 --- a/cache.go +++ b/cache.go @@ -8,9 +8,8 @@ import ( ) var ( - mu sync.RWMutex - CacheUsers = make(map[uuid.UUID]*User) - CacheGroups = make(map[uuid.UUID]*Group) + mu sync.RWMutex + CacheUsers = make(map[uuid.UUID]*User) ) func CacheGetUserById(id uuid.UUID) (*User, error) { @@ -50,19 +49,6 @@ func CacheDeleteUser(id uuid.UUID) { delete(CacheUsers, id) } -func CacheSaveGroup(group *Group) { - mu.Lock() - defer mu.Unlock() - - CacheGroups[group.Id] = group -} - -func CacheDeleteGroup(id uuid.UUID) { - mu.Lock() - defer mu.Unlock() - delete(CacheGroups, id) -} - func CacheAddConnection(a, b *User, conn *Connection) { first, second := a, b if a.Id.String() > b.Id.String() { @@ -96,14 +82,3 @@ func CacheGetConnection(user *User, id uuid.UUID) (*Connection, bool) { return conn, ok } -func CacheGetGroup(id uuid.UUID) (*Group, error) { - mu.RLock() - defer mu.RUnlock() - - group, ok := CacheGroups[id] - if !ok { - return nil, fmt.Errorf("group %s not found", id) - } - - return group, nil -} diff --git a/database.go b/database.go index bab69b9..311aed5 100644 --- a/database.go +++ b/database.go @@ -6,7 +6,6 @@ import ( "time" "github.com/google/uuid" - "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) @@ -66,44 +65,6 @@ func DbInit(ctx context.Context) { panic(err) } - _, err = dbConn.Exec(ctx, ` - CREATE TABLE IF NOT EXISTS chat_groups ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL, - creator_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, - owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, - enable_client_colors BOOLEAN NOT NULL DEFAULT true, - color_red SMALLINT DEFAULT NULL, - color_green SMALLINT DEFAULT NULL, - color_blue SMALLINT DEFAULT NULL, - created_at TIMESTAMP NOT NULL DEFAULT NOW() - ) - `) - if err != nil { - panic(err) - } - - _, err = dbConn.Exec(ctx, ` - CREATE TABLE IF NOT EXISTS chat_messages ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - sender_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, - group_id UUID NOT NULL REFERENCES chat_groups(id) ON DELETE CASCADE, - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - content TEXT NOT NULL - ) - `) - - _, err = dbConn.Exec(ctx, ` - CREATE TABLE IF NOT EXISTS chat_group_members ( - group_id UUID NOT NULL REFERENCES chat_groups(id) ON DELETE CASCADE, - user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, - joined_at TIMESTAMP NOT NULL DEFAULT NOW(), - PRIMARY KEY (group_id, user_id) - ) - `) - if err != nil { - panic(err) - } } func DbUserSave(ctx context.Context, user *User) error { @@ -137,26 +98,6 @@ func DbUserGetById(ctx context.Context, user *User) error { return err } -func DbUserGetGroups(ctx context.Context, user *User) error { - rows, err := dbConn.Query(ctx, ` - SELECT group_id FROM chat_group_members WHERE user_id = $1 - `, user.Id) - if err != nil { - return err - } - defer rows.Close() - - user.Groups = make(map[uuid.UUID]struct{}) - for rows.Next() { - var groupId uuid.UUID - if err := rows.Scan(&groupId); err != nil { - return err - } - user.Groups[groupId] = struct{}{} - } - return rows.Err() -} - func DbUserSetColor(ctx context.Context, user *User) error { _, err := dbConn.Exec(ctx, ` UPDATE users SET color_red = $1, color_green = $2, color_blue = $3 WHERE id = $4 @@ -175,9 +116,6 @@ func DbGetWholeUser(ctx context.Context, user *User) error { if err := DbUserGetById(ctx, user); err != nil { return err } - if err := DbUserGetGroups(ctx, user); err != nil { - return err - } if err := DbConnectionsGetBelongingToUser(ctx, user); err != nil { return err } @@ -186,14 +124,6 @@ func DbGetWholeUser(ctx context.Context, user *User) error { return nil } -func DbGroupSetColor(ctx context.Context, group *Group) error { - _, err := dbConn.Exec(ctx, ` - UPDATE chat_groups SET color_red = $1, color_green = $2, color_blue = $3 WHERE id = $4 - `, group.Color[0], group.Color[1], group.Color[2], group.Id) - - return err -} - func DbConnectionSave(ctx context.Context, conn *Connection) error { return dbConn.QueryRow(ctx, ` INSERT INTO user_connections (requestor_id, recipient_id, state, created_at) VALUES ($1, $2, $3, $4) @@ -240,7 +170,7 @@ func DbConnectionUpdateState(ctx context.Context, conn *Connection) error { return err } -func DbMessageSave(ctx context.Context, message *Message) error { +func DbConnectionMessageSave(ctx context.Context, message *Message) error { if message.Id != (uuid.UUID{}) { _, err := dbConn.Exec(ctx, ` INSERT INTO direct_messages (id, sender_id, receiver_id, created_at, content) VALUES ($1, $2, $3, $4, $5) @@ -282,113 +212,3 @@ func DbConnectionGetMessagesBefore(ctx context.Context, before time.Time, connec return messages, rows.Err() } -func DbGroupSave(ctx context.Context, group *Group) error { - err := dbConn.QueryRow(ctx, ` - INSERT INTO chat_groups (name, creator_id, owner_id, enable_client_colors, color_red, color_green, color_blue, created_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) - RETURNING id - `, group.Name, group.CreatorId, group.OwnerId, group.EnableUserColors, group.Color[0], group.Color[1], group.Color[2], group.CreatedAt). - Scan(&group.Id) - if err != nil { - return err - } - _, err = dbConn.Exec(ctx, ` - INSERT INTO chat_group_members (group_id, user_id, joined_at) - VALUES ($1, $2, $3) - `, group.Id, group.OwnerId, group.CreatedAt) - return err -} - -func DbGroupDelete(ctx context.Context, group *Group) error { - _, err := dbConn.Exec(ctx, ` - DELETE FROM chat_groups WHERE id = $1 - `, group.Id) - return err -} - -func DbGroupGetById(ctx context.Context, group *Group) error { - err := dbConn.QueryRow(ctx, ` - SELECT name, creator_id, owner_id, enable_client_colors, color_red, color_green, color_blue, created_at FROM chat_groups WHERE id = $1 - `, group.Id).Scan(&group.Name, &group.CreatorId, &group.OwnerId, &group.EnableUserColors, &group.Color[0], &group.Color[1], &group.Color[2], &group.CreatedAt) - return err -} - -func DbGroupGetMembers(ctx context.Context, group *Group) error { - rows, err := dbConn.Query(ctx, ` - SELECT user_id FROM chat_group_members WHERE group_id = $1 - `, group.Id) - if err != nil { - return err - } - defer rows.Close() - - group.Users = make(map[uuid.UUID]struct{}) - for rows.Next() { - var userId uuid.UUID - if err := rows.Scan(&userId); err != nil { - return err - } - group.Users[userId] = struct{}{} - } - return rows.Err() -} - -func DbGroupAddUsers(ctx context.Context, groupId uuid.UUID, userIds *[MaxUsersInGroup]uuid.UUID) error { - batch := &pgx.Batch{} - now := time.Now() - var count int - for _, uid := range userIds { - if uid == (uuid.UUID{}) { - continue - } - batch.Queue(` - INSERT INTO chat_group_members (group_id, user_id, joined_at) - VALUES ($1, $2, $3) - ON CONFLICT DO NOTHING - `, groupId, uid, now) - count++ - } - br := dbConn.SendBatch(ctx, batch) - defer br.Close() - for range count { - if _, err := br.Exec(); err != nil { - return err - } - } - return nil -} - -func DbGroupRemoveUsers(ctx context.Context, groupId uuid.UUID, userIds *[MaxUsersInGroup]uuid.UUID) (int, error) { - batch := &pgx.Batch{} - var count int - for _, uid := range userIds { - if uid == (uuid.UUID{}) { - continue - } - batch.Queue(` - DELETE FROM chat_group_members WHERE group_id = $1 AND user_id = $2 - `, groupId, uid) - count++ - } - br := dbConn.SendBatch(ctx, batch) - defer br.Close() - var deleted int - for range count { - tag, err := br.Exec() - if err != nil { - return deleted, err - } - deleted += int(tag.RowsAffected()) - } - return deleted, nil -} - -func DbGroupSetOwnerId(ctx context.Context, group *Group) error { - _, err := dbConn.Exec(ctx, ` - UPDATE chat_groups SET owner_id = $1 WHERE id = $2 - `, group.OwnerId, group.Id) - - return err -} - -DbGroup \ No newline at end of file diff --git a/get.go b/get.go index e2f8c46..d6e6a18 100644 --- a/get.go +++ b/get.go @@ -26,18 +26,3 @@ func GetUserByToken(ctx context.Context, token string) (*User, error) { } return GetUserById(ctx, userId) } - -func GetGroup(ctx context.Context, groupId uuid.UUID) (*Group, error) { - group, err := CacheGetGroup(groupId) - if err != nil { - group = &Group{Id: groupId} - if err = DbGroupGetById(ctx, group); err != nil { - return nil, err - } - if err = DbGroupGetMembers(ctx, group); err != nil { - return nil, err - } - CacheSaveGroup(group) - } - return group, nil -} diff --git a/globals.go b/globals.go index 5d1946b..2c1f07e 100644 --- a/globals.go +++ b/globals.go @@ -1,9 +1,6 @@ package main const ( - MaxGroupsForUser uint32 = 8 - MaxUsersInGroup uint32 = 12 MaxDirectMsgCache uint32 = 12 - MaxGroupMsgCache uint32 = 8 MessagesPartitions uint8 = 2 ) diff --git a/go-socket b/go-socket index b7dce9d..d6c0a7c 100755 Binary files a/go-socket and b/go-socket differ diff --git a/httpConnectionAndDm.go b/httpConnectionAndDm.go index 934893d..e08e9bd 100644 --- a/httpConnectionAndDm.go +++ b/httpConnectionAndDm.go @@ -70,7 +70,7 @@ func HttpHandleDm(response http.ResponseWriter, request *http.Request) { Event: message, }) - err = DbMessageSave(ctx, message) + err = DbConnectionMessageSave(ctx, message) if err != nil { http.Error(response, "internal server error", http.StatusInternalServerError) return diff --git a/httpGroup.go b/httpGroup.go deleted file mode 100644 index 7bf6473..0000000 --- a/httpGroup.go +++ /dev/null @@ -1,433 +0,0 @@ -package main - -import ( - "context" - json2 "encoding/json" - "errors" - "fmt" - "maps" - "net/http" - "slices" - "strconv" - "strings" - "time" - - "github.com/google/uuid" -) - -func isOwnerOfGroup(user *User, group *Group) bool { - if group.OwnerId == user.Id { - return true - } - return false -} - -func getIfOwnerUserAndGroup(ctx context.Context, response *http.ResponseWriter, request *http.Request) (*User, *Group, error) { - user, err := GetUserByToken(ctx, request.FormValue("token")) - if err != nil { - http.Error(*response, "invalid token", http.StatusUnauthorized) - return nil, nil, err - } - - affectedGroupId, err := ConvertStringUuid(request.FormValue("groupid")) - if err != nil { - http.Error(*response, "no such group", http.StatusUnauthorized) - return nil, nil, err - } - - group, err := GetGroup(ctx, affectedGroupId) - if err != nil { - http.Error(*response, "no such group", http.StatusUnauthorized) - return nil, nil, err - } - - if !isOwnerOfGroup(user, group) { - http.Error(*response, "no such group", http.StatusUnauthorized) - return nil, nil, errors.New("not an owner") - } - return user, group, nil -} - -func HttpHandeGroupCreate(response http.ResponseWriter, request *http.Request) { - if !HttpMethodAllowed(&response, request) { - return - } - - ctx := request.Context() - - user, err := GetUserByToken(ctx, request.FormValue("token")) - if err != nil { - http.Error(response, "invalid token", http.StatusUnauthorized) - return - } - - name := request.FormValue("name") - if name == "" { - name = "Best group ever" - } - - colorString := request.FormValue("color") - color, err := ConvertStringToRgb(colorString) - if err != nil { - http.Error(response, "invalid color", http.StatusBadRequest) - return - } - - group := Group{ - Name: name, - CreatedAt: time.Now(), - OwnerId: user.Id, - CreatorId: user.Id, - Color: color, - Users: map[uuid.UUID]struct{}{user.Id: {}}, - } - - enableUserColors := request.FormValue("enableUserColors") - if enableUserColors == "1" { - group.EnableUserColors = true - } - - err = DbGroupSave(ctx, &group) - if err != nil { - http.Error(response, err.Error(), http.StatusInternalServerError) - return - } - response.WriteHeader(http.StatusCreated) - fmt.Fprintf(response, "%s", group.Id) -} - -func HttpHandleGroupDelete(response http.ResponseWriter, request *http.Request) { - if !HttpMethodAllowed(&response, request) { - return - } - - ctx := request.Context() - _, group, err := getIfOwnerUserAndGroup(ctx, &response, request) - if err != nil { - return - } - - err = DbGroupDelete(ctx, group) - if err != nil { - http.Error(response, "internal server error", http.StatusInternalServerError) - return - } - CacheDeleteGroup(group.Id) - - response.WriteHeader(http.StatusAccepted) -} - -func HttpHandleGroupAddUsers(response http.ResponseWriter, request *http.Request) { - if !HttpMethodAllowed(&response, request) { - return - } - - ctx := request.Context() - - _, group, err := getIfOwnerUserAndGroup(ctx, &response, request) - if err != nil { - return - } - - usersString := request.FormValue("users") - var remainingUsersCount = int(MaxUsersInGroup) - len(group.Users) - if remainingUsersCount < 1 { - http.Error(response, "max users", http.StatusUnauthorized) - return - } - - usersStringSlice := strings.SplitN(usersString, ",", remainingUsersCount+1) - if len(usersStringSlice) == 0 { - http.Error(response, "no users to add", http.StatusBadRequest) - return - } - - var ids [MaxUsersInGroup]uuid.UUID - var idx uint32 = 0 - for _, s := range usersStringSlice { - if idx >= MaxUsersInGroup { - break - } - id, err := ConvertStringUuid(strings.TrimSpace(s)) - if err != nil { - continue - } - ids[idx] = id - idx++ - } - if idx == 0 { - http.Error(response, "no valid users", http.StatusBadRequest) - return - } - - err = DbGroupAddUsers(ctx, group.Id, &ids) - if err != nil { - http.Error(response, "internal server error", http.StatusInternalServerError) - return - } - - for i := uint32(0); i < idx; i++ { - group.Users[ids[i]] = struct{}{} - if cachedUser, err := CacheGetUserById(ids[i]); err == nil { - cachedUser.Mu.Lock() - cachedUser.Groups[group.Id] = struct{}{} - cachedUser.Mu.Unlock() - } - } - - response.WriteHeader(http.StatusAccepted) -} - -func HttpHandleGroupRemoveUser(response http.ResponseWriter, request *http.Request) { - if !HttpMethodAllowed(&response, request) { - return - } - - ctx := request.Context() - - _, group, err := getIfOwnerUserAndGroup(ctx, &response, request) - if err != nil { - return - } - - usersString := request.FormValue("users") - - usersStringSlice := strings.SplitN(usersString, ",", int(MaxUsersInGroup)+1) - - var ids [MaxUsersInGroup]uuid.UUID - var idx uint32 = 0 - for _, s := range usersStringSlice { - if idx >= MaxUsersInGroup { - break - } - id, err := ConvertStringUuid(strings.TrimSpace(s)) - if err != nil { - continue - } - ids[idx] = id - idx++ - } - if idx == 0 { - http.Error(response, "no valid users", http.StatusBadRequest) - return - } - - count, err := DbGroupRemoveUsers(ctx, group.Id, &ids) - if err != nil { - http.Error(response, "internal server error", http.StatusInternalServerError) - return - } - - for i := uint32(0); i < idx; i++ { - delete(group.Users, ids[i]) - if cachedUser, err := CacheGetUserById(ids[i]); err == nil { - cachedUser.Mu.Lock() - delete(cachedUser.Groups, group.Id) - cachedUser.Mu.Unlock() - } - } - - response.WriteHeader(http.StatusAccepted) - response.Write([]byte(strconv.Itoa(count))) -} - -func HttpHandleGroupChangeColor(response http.ResponseWriter, request *http.Request) { - if !HttpMethodAllowed(&response, request) { - return - } - - ctx := request.Context() - _, group, err := getIfOwnerUserAndGroup(ctx, &response, request) - if err != nil { - return - } - - color, err := ConvertStringToRgb(request.FormValue("color")) - if err != nil { - http.Error(response, "invalid color", http.StatusBadRequest) - return - } - - group.Color = color - err = DbGroupSetColor(ctx, group) - if err != nil { - http.Error(response, "internal server error", http.StatusInternalServerError) - return - } - - response.WriteHeader(http.StatusAccepted) - response.Write([]byte("changed")) -} - -func HttpHandleGroupChangeOwner(response http.ResponseWriter, request *http.Request) { - if !HttpMethodAllowed(&response, request) { - return - } - - ctx := request.Context() - _, group, err := getIfOwnerUserAndGroup(ctx, &response, request) - if err != nil { - return - } - - newOwnerName := request.FormValue("newOwner") - - newOwner, err := CacheGetUserByName(newOwnerName) - if err != nil { - newOwner = &User{Name: newOwnerName} - err = DbUserGetStandardInfoByName(ctx, newOwner) - if err != nil { - http.Error(response, "user not in group", http.StatusBadRequest) - return - } - - if err = DbGetWholeUser(ctx, newOwner); err != nil { - http.Error(response, "internal server error", http.StatusInternalServerError) - return - } - } - - _, ok := group.Users[newOwner.Id] - if !ok { - http.Error(response, "user not in group", http.StatusBadRequest) - return - } - - group.OwnerId = newOwner.Id - err = DbGroupSetOwnerId(ctx, group) - if err != nil { - http.Error(response, "internal server error", http.StatusInternalServerError) - return - } - - response.WriteHeader(http.StatusAccepted) -} - -func HttpHandleGroupMessage(response http.ResponseWriter, request *http.Request) { - if !HttpMethodAllowed(&response, request) { - return - } - - ctx := request.Context() - - user, err := GetUserByToken(ctx, request.FormValue("token")) - if err != nil { - http.Error(response, "invalid token", http.StatusBadRequest) - return - } - groupIdStr := request.FormValue("groupid") - groupId, err := ConvertStringUuid(groupIdStr) - if err != nil { - http.Error(response, "no such group", http.StatusUnauthorized) - return - } - - group, err := GetGroup(ctx, groupId) - if err != nil { - http.Error(response, "no such group", http.StatusUnauthorized) - return - } - - content := request.FormValue("content") - if content == "" { - http.Error(response, "empty message", http.StatusBadRequest) - return - } - - _, ok := group.Users[user.Id] - if !ok { - http.Error(response, "no such group", http.StatusUnauthorized) - return - } - - err = WsSendToGroupAsUser(group, user, content) - if err != nil { - http.Error(response, err.Error(), http.StatusBadRequest) - return - } - response.WriteHeader(http.StatusAccepted) -} - -func HttpHandleGroupsGetWithoutMembers(response http.ResponseWriter, request *http.Request) { - if !HttpMethodAllowed(&response, request) { - return - } - - ctx := request.Context() - - user, err := GetUserByToken(ctx, request.FormValue("token")) - if err != nil { - http.Error(response, "invalid token", http.StatusUnauthorized) - return - } - - user.Mu.RLock() - groupIds := make([]uuid.UUID, 0, len(user.Groups)) - for groupId := range user.Groups { - groupIds = append(groupIds, groupId) - } - user.Mu.RUnlock() - - groups := make(map[uuid.UUID]*Group, len(groupIds)) - for _, groupId := range groupIds { - group, err := GetGroup(ctx, groupId) - if err != nil { - continue - } - groups[groupId] = group - } - - json, err := json2.Marshal(groups) - if err != nil { - http.Error(response, "internal server error", http.StatusInternalServerError) - return - } - - response.WriteHeader(http.StatusAccepted) - response.Write(json) -} - -func HttpHandleGroupMembersGet(response http.ResponseWriter, request *http.Request) { - if !HttpMethodAllowed(&response, request) { - return - } - - ctx := request.Context() - user, err := GetUserByToken(ctx, request.FormValue("token")) - if err != nil { - http.Error(response, "invalid token", http.StatusUnauthorized) - return - } - - groupStr := request.FormValue("group") - groupId, err := ConvertStringUuid(groupStr) - if err != nil { - http.Error(response, "invalid group", http.StatusBadRequest) - return - } - - user.Mu.RLock() - _, ok := user.Groups[groupId] - user.Mu.RUnlock() - if !ok { - http.Error(response, "no such group", http.StatusUnauthorized) - return - } - - group, err := GetGroup(ctx, groupId) - if err != nil { - http.Error(response, "no such group", http.StatusUnauthorized) - return - } - - groupMembers := slices.Collect(maps.Keys(group.Users)) - - json, err := json2.Marshal(groupMembers) - if err != nil { - http.Error(response, "internal server error", http.StatusInternalServerError) - return - } - - response.WriteHeader(http.StatusAccepted) - response.Write(json) -} diff --git a/main.go b/main.go index 2069e33..7c93291 100644 --- a/main.go +++ b/main.go @@ -20,27 +20,17 @@ func main() { http.HandleFunc("/new/user", withCORS(HttpHandleUserNew)) http.HandleFunc("/new/connection", withCORS(HttpHandleUserNewConnection)) http.HandleFunc("/new/token", withCORS(HttpHandleUserNewToken)) - http.HandleFunc("/new/group", withCORS(HttpHandeGroupCreate)) - http.HandleFunc("/mod/user/appearence", withCORS(HttpHandleUserModifyAppearance)) http.HandleFunc("/mod/user/about", withCORS(HttpHandleUserModifyAbout)) http.HandleFunc("/mod/connection/accept", withCORS(HttpHandleUserElevateConnection)) - http.HandleFunc("/mod/group/addusers", withCORS(HttpHandleGroupAddUsers)) - http.HandleFunc("/mod/group/removeusers", withCORS(HttpHandleGroupRemoveUser)) - http.HandleFunc("/mod/group/color", withCORS(HttpHandleGroupChangeColor)) - http.HandleFunc("/mod/group/owner", withCORS(HttpHandleGroupChangeOwner)) - http.HandleFunc("/get/groups", withCORS(HttpHandleGroupsGetWithoutMembers)) http.HandleFunc("/get/connections", withCORS(HttpHandleUserGetConnections)) http.HandleFunc("/get/connection/messages", withCORS(HttpHandleUserGetConnectionMessages)) - http.HandleFunc("/get/group/members", withCORS(HttpHandleGroupMembersGet)) http.HandleFunc("/del/user", withCORS(HttpHandleUserDelete)) - http.HandleFunc("/del/group", withCORS(HttpHandleGroupDelete)) http.HandleFunc("/del/connection", withCORS(HttpHandleUserDeleteConnection)) http.HandleFunc("/msg/user", withCORS(HttpHandleDm)) - http.HandleFunc("/msg/group", withCORS(HttpHandleGroupMessage)) http.HandleFunc("/ws", ServeWsConnection) log.Println("listening on :8080") diff --git a/structs.go b/structs.go index a3705d0..cdfc5dc 100644 --- a/structs.go +++ b/structs.go @@ -19,7 +19,6 @@ type User struct { CreatedAt time.Time WsConn *websocket.Conn Id uuid.UUID - Groups map[uuid.UUID]struct{} Connections map[uuid.UUID]*Connection Color [3]uint8 } @@ -77,48 +76,6 @@ type Message struct { Receiver uuid.UUID `json:"receiver"` } -type Group struct { - Mu sync.RWMutex `json:"-"` - Name string `json:"name"` - CreatedAt time.Time `json:"createdAt"` - MessagesBuff [MaxGroupMsgCache]*Message `json:"-"` - NextBuffIdx uint32 `json:"-"` - Id uuid.UUID `json:"-"` - CreatorId uuid.UUID `json:"creatorId"` - OwnerId uuid.UUID `json:"ownerId"` - Users map[uuid.UUID]struct{} `json:"-"` - Color [3]uint8 `json:"color"` - EnableUserColors bool `json:"enableUserColors"` - HaveMessageBuffOverflowed bool `json:"-"` -} - -func (g *Group) AddMessageToBuff(message *Message) { - g.Mu.Lock() - defer g.Mu.Unlock() - - g.MessagesBuff[g.NextBuffIdx%MaxGroupMsgCache] = message - g.NextBuffIdx++ - if g.NextBuffIdx >= MaxGroupMsgCache { - g.HaveMessageBuffOverflowed = true - } -} - -// GetSortedMessagesBuff returns arr, length -func (g *Group) GetSortedMessagesBuff() (*[MaxGroupMsgCache]*Message, uint32) { - g.Mu.RLock() - defer g.Mu.RUnlock() - - if !g.HaveMessageBuffOverflowed { - return &g.MessagesBuff, g.NextBuffIdx - } - - sorted := new([MaxGroupMsgCache]*Message) - for i := uint32(0); i < MaxGroupMsgCache; i++ { - sorted[i] = g.MessagesBuff[(g.NextBuffIdx+i)%MaxGroupMsgCache] - } - return sorted, MaxGroupMsgCache -} - type LoginReturn struct { Token string `json:"token"` UserId uuid.UUID `json:"userId"` diff --git a/tests/01_create_accounts.sh b/tests/01_create_accounts.sh deleted file mode 100755 index 92ed3d0..0000000 --- a/tests/01_create_accounts.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -# Create two user accounts -source "$(dirname "$0")/config.sh" - -echo "=== Creating account: $USER1_NAME ===" -RESP1=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/new/client" \ - -d "username=$USER1_NAME" \ - -d "password=$USER1_PASS" \ - -d "color=$USER1_COLOR") -BODY1=$(echo "$RESP1" | head -1) -CODE1=$(echo "$RESP1" | tail -1) -echo "Response: $BODY1 (HTTP $CODE1)" - -echo "" -echo "=== Creating account: $USER2_NAME ===" -RESP2=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/new/client" \ - -d "username=$USER2_NAME" \ - -d "password=$USER2_PASS" \ - -d "color=$USER2_COLOR") -BODY2=$(echo "$RESP2" | head -1) -CODE2=$(echo "$RESP2" | tail -1) -echo "Response: $BODY2 (HTTP $CODE2)" diff --git a/tests/02_login.sh b/tests/02_login.sh deleted file mode 100755 index 57d57aa..0000000 --- a/tests/02_login.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Login both users and save tokens -source "$(dirname "$0")/config.sh" - -echo "=== Logging in as $USER1_NAME ===" -TOKEN1=$(curl -s -X POST "$BASE_URL/new/token" \ - -d "username=$USER1_NAME" \ - -d "password=$USER1_PASS") -echo "Token1: $TOKEN1" -save_state "TOKEN1" "$TOKEN1" - -USER1_ID=$(decode_jwt_sub "$TOKEN1") -echo "User1 ID: $USER1_ID" -save_state "USER1_ID" "$USER1_ID" - -echo "" -echo "=== Logging in as $USER2_NAME ===" -TOKEN2=$(curl -s -X POST "$BASE_URL/new/token" \ - -d "username=$USER2_NAME" \ - -d "password=$USER2_PASS") -echo "Token2: $TOKEN2" -save_state "TOKEN2" "$TOKEN2" - -USER2_ID=$(decode_jwt_sub "$TOKEN2") -echo "User2 ID: $USER2_ID" -save_state "USER2_ID" "$USER2_ID" diff --git a/tests/03_create_group.sh b/tests/03_create_group.sh deleted file mode 100755 index ae1d5cd..0000000 --- a/tests/03_create_group.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -# Create a group as user1 -source "$(dirname "$0")/config.sh" - -TOKEN1=$(load_state "TOKEN1") -if [[ -z "$TOKEN1" ]]; then - echo "ERROR: No token found. Run 02_login.sh first." - exit 1 -fi - -echo "=== Creating group: $GROUP_NAME ===" -# Pipe curl directly to od to avoid null bytes being lost in bash variables -GROUP_ID=$(curl -s -X POST "$BASE_URL/new/group" \ - -d "token=$TOKEN1" \ - -d "name=$GROUP_NAME" \ - -d "color=$GROUP_COLOR" \ - | od -An -tu4 -N4 --endian=big | tr -d ' ') -echo "Group ID: $GROUP_ID" -save_state "GROUP_ID" "$GROUP_ID" diff --git a/tests/04_add_user_to_group.sh b/tests/04_add_user_to_group.sh deleted file mode 100755 index b55387a..0000000 --- a/tests/04_add_user_to_group.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -# Add user2 to the group (user1 is owner) -source "$(dirname "$0")/config.sh" - -TOKEN1=$(load_state "TOKEN1") -GROUP_ID=$(load_state "GROUP_ID") -USER2_ID=$(load_state "USER2_ID") - -if [[ -z "$TOKEN1" || -z "$GROUP_ID" || -z "$USER2_ID" ]]; then - echo "ERROR: Missing state. Run previous scripts first." - echo " TOKEN1=$TOKEN1" - echo " GROUP_ID=$GROUP_ID" - echo " USER2_ID=$USER2_ID" - exit 1 -fi - -echo "=== Adding user2 (ID: $USER2_ID) to group $GROUP_ID ===" -RESP=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/mod/group/addclients" \ - -d "token=$TOKEN1" \ - -d "groupid=$GROUP_ID" \ - -d "clients=$USER2_ID") -BODY=$(echo "$RESP" | head -1) -CODE=$(echo "$RESP" | tail -1) -echo "Response: $BODY (HTTP $CODE)" diff --git a/tests/05_send_message.sh b/tests/05_send_message.sh deleted file mode 100755 index 31c7e63..0000000 --- a/tests/05_send_message.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/bash -# Send message from user_one to the group via HTTP, verify user_two receives it -source "$(dirname "$0")/config.sh" - -TOKEN1=$(load_state "TOKEN1") -TOKEN2=$(load_state "TOKEN2") -GROUP_ID=$(load_state "GROUP_ID") - -if [[ -z "$TOKEN1" || -z "$TOKEN2" || -z "$GROUP_ID" ]]; then - echo "ERROR: Missing state. Run previous scripts first." - echo " TOKEN1=$TOKEN1" - echo " TOKEN2=$TOKEN2" - echo " GROUP_ID=$GROUP_ID" - exit 1 -fi - -MESSAGE="Hello from user_one!" - -echo "=== Sending message from user_one to group $GROUP_ID via HTTP ===" - -TMPDIR=$(mktemp -d) -trap 'rm -rf "$TMPDIR"' EXIT - -echo "[DEBUG] tmpdir: $TMPDIR" -echo "[DEBUG] TOKEN1: ${TOKEN1:0:20}..." -echo "[DEBUG] TOKEN2: ${TOKEN2:0:20}..." -echo "[DEBUG] GROUP_ID: $GROUP_ID" - -# Receiver (user_two): authenticate via WebSocket then wait for messages -echo "[DEBUG] starting receiver (user_two)..." -{ echo '{"token":"'"$TOKEN2"'"}'; sleep 5; } \ - | stdbuf -oL websocat ws://localhost:8080/ws > "$TMPDIR/received" 2>"$TMPDIR/recv_err" & -RECV_PID=$! -echo "[DEBUG] receiver PID: $RECV_PID" -sleep 0.5 - -# Sender (user_one): send message via HTTP POST -echo "[DEBUG] sending message via HTTP..." -SEND_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/new/message" \ - -d "token=$TOKEN1" \ - -d "subject=$GROUP_ID" \ - -d "content=$MESSAGE") -SEND_HTTP_CODE=$(echo "$SEND_RESPONSE" | tail -1) -SEND_BODY=$(echo "$SEND_RESPONSE" | sed '$d') -echo "[DEBUG] send HTTP status: $SEND_HTTP_CODE" -echo "[DEBUG] send response body: $SEND_BODY" - -if [[ "$SEND_HTTP_CODE" != "202" ]]; then - echo "FAIL: HTTP send failed with status $SEND_HTTP_CODE" - kill $RECV_PID 2>/dev/null - wait $RECV_PID 2>/dev/null - exit 1 -fi - -echo "[DEBUG] waiting 2s for message delivery..." -sleep 2 - -echo "[DEBUG] killing receiver..." -kill $RECV_PID 2>/dev/null -wait $RECV_PID 2>/dev/null - -RECV_ERR=$(cat "$TMPDIR/recv_err") -[[ -n "$RECV_ERR" ]] && echo "[DEBUG] receiver stderr: $RECV_ERR" - -RESPONSE=$(cat "$TMPDIR/received") -echo "user_two received: $RESPONSE" - -if [[ -z "$RESPONSE" ]]; then - echo "[DEBUG] EMPTY response - no data received by user_two" - echo "FAIL: message not received" - exit 1 -fi - -if echo "$RESPONSE" | grep -q "$MESSAGE"; then - echo "PASS" -else - echo "FAIL: message not received" - exit 1 -fi diff --git a/tests/06_send_message_reverse.sh b/tests/06_send_message_reverse.sh deleted file mode 100755 index 0810553..0000000 --- a/tests/06_send_message_reverse.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/bash -# Send message from user_two to the group via HTTP, verify user_one receives it -source "$(dirname "$0")/config.sh" - -TOKEN1=$(load_state "TOKEN1") -TOKEN2=$(load_state "TOKEN2") -GROUP_ID=$(load_state "GROUP_ID") - -if [[ -z "$TOKEN1" || -z "$TOKEN2" || -z "$GROUP_ID" ]]; then - echo "ERROR: Missing state. Run previous scripts first." - echo " TOKEN1=$TOKEN1" - echo " TOKEN2=$TOKEN2" - echo " GROUP_ID=$GROUP_ID" - exit 1 -fi - -MESSAGE="Hello from user_two!" - -echo "=== Sending message from user_two to group $GROUP_ID via HTTP ===" - -TMPDIR=$(mktemp -d) -trap 'rm -rf "$TMPDIR"' EXIT - -echo "[DEBUG] tmpdir: $TMPDIR" -echo "[DEBUG] TOKEN1: ${TOKEN1:0:20}..." -echo "[DEBUG] TOKEN2: ${TOKEN2:0:20}..." -echo "[DEBUG] GROUP_ID: $GROUP_ID" - -# Receiver (user_one): authenticate via WebSocket then wait for messages -echo "[DEBUG] starting receiver (user_one)..." -{ echo '{"token":"'"$TOKEN1"'"}'; sleep 5; } \ - | stdbuf -oL websocat ws://localhost:8080/ws > "$TMPDIR/received" 2>"$TMPDIR/recv_err" & -RECV_PID=$! -echo "[DEBUG] receiver PID: $RECV_PID" -sleep 0.5 - -# Sender (user_two): send message via HTTP POST -echo "[DEBUG] sending message via HTTP..." -SEND_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/new/message" \ - -d "token=$TOKEN2" \ - -d "subject=$GROUP_ID" \ - -d "content=$MESSAGE") -SEND_HTTP_CODE=$(echo "$SEND_RESPONSE" | tail -1) -SEND_BODY=$(echo "$SEND_RESPONSE" | sed '$d') -echo "[DEBUG] send HTTP status: $SEND_HTTP_CODE" -echo "[DEBUG] send response body: $SEND_BODY" - -if [[ "$SEND_HTTP_CODE" != "202" ]]; then - echo "FAIL: HTTP send failed with status $SEND_HTTP_CODE" - kill $RECV_PID 2>/dev/null - wait $RECV_PID 2>/dev/null - exit 1 -fi - -echo "[DEBUG] waiting 2s for message delivery..." -sleep 2 - -echo "[DEBUG] killing receiver..." -kill $RECV_PID 2>/dev/null -wait $RECV_PID 2>/dev/null - -RECV_ERR=$(cat "$TMPDIR/recv_err") -[[ -n "$RECV_ERR" ]] && echo "[DEBUG] receiver stderr: $RECV_ERR" - -RESPONSE=$(cat "$TMPDIR/received") -echo "user_one received: $RESPONSE" - -if [[ -z "$RESPONSE" ]]; then - echo "[DEBUG] EMPTY response - no data received by user_one" - echo "FAIL: message not received" - exit 1 -fi - -if echo "$RESPONSE" | grep -q "$MESSAGE"; then - echo "PASS" -else - echo "FAIL: message not received" - exit 1 -fi diff --git a/tests/07_get_groups.sh b/tests/07_get_groups.sh deleted file mode 100755 index f304250..0000000 --- a/tests/07_get_groups.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/bash -# Get groups for both users -source "$(dirname "$0")/config.sh" - -TOKEN1=$(load_state "TOKEN1") -TOKEN2=$(load_state "TOKEN2") - -if [[ -z "$TOKEN1" || -z "$TOKEN2" ]]; then - echo "ERROR: Missing tokens. Run 02_login.sh first." - exit 1 -fi - -echo "=== Getting groups for user1 ===" -RESP=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/get/groups" \ - -d "token=$TOKEN1") -BODY=$(echo "$RESP" | head -1) -CODE=$(echo "$RESP" | tail -1) -echo "Response: $BODY (HTTP $CODE)" - -if [[ "$CODE" != "202" ]]; then - echo "FAIL: Expected HTTP 202, got $CODE" - exit 1 -fi - -echo "" -echo "=== Getting groups for user2 ===" -RESP=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/get/groups" \ - -d "token=$TOKEN2") -BODY=$(echo "$RESP" | head -1) -CODE=$(echo "$RESP" | tail -1) -echo "Response: $BODY (HTTP $CODE)" - -if [[ "$CODE" != "202" ]]; then - echo "FAIL: Expected HTTP 202, got $CODE" - exit 1 -fi - -echo "" -echo "=== Getting groups with invalid token ===" -RESP=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/get/groups" \ - -d "token=invalid_token") -BODY=$(echo "$RESP" | head -1) -CODE=$(echo "$RESP" | tail -1) -echo "Response: $BODY (HTTP $CODE)" - -if [[ "$CODE" != "401" ]]; then - echo "FAIL: Expected HTTP 401, got $CODE" - exit 1 -fi - -echo "" -echo "ALL PASSED" diff --git a/tests/08_get_group_members.sh b/tests/08_get_group_members.sh deleted file mode 100755 index bdecf0f..0000000 --- a/tests/08_get_group_members.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/bash -# Get group members -source "$(dirname "$0")/config.sh" - -TOKEN1=$(load_state "TOKEN1") -TOKEN2=$(load_state "TOKEN2") -GROUP_ID=$(load_state "GROUP_ID") - -if [[ -z "$TOKEN1" || -z "$TOKEN2" || -z "$GROUP_ID" ]]; then - echo "ERROR: Missing state. Run previous scripts first." - echo " TOKEN1=$TOKEN1" - echo " TOKEN2=$TOKEN2" - echo " GROUP_ID=$GROUP_ID" - exit 1 -fi - -echo "=== Getting members of group $GROUP_ID as user1 (owner) ===" -RESP=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/get/group/members" \ - -d "token=$TOKEN1" \ - -d "group=$GROUP_ID") -BODY=$(echo "$RESP" | head -1) -CODE=$(echo "$RESP" | tail -1) -echo "Response: $BODY (HTTP $CODE)" - -if [[ "$CODE" != "202" ]]; then - echo "FAIL: Expected HTTP 202, got $CODE" - exit 1 -fi - -echo "" -echo "=== Getting members of group $GROUP_ID as user2 (member) ===" -RESP=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/get/group/members" \ - -d "token=$TOKEN2" \ - -d "group=$GROUP_ID") -BODY=$(echo "$RESP" | head -1) -CODE=$(echo "$RESP" | tail -1) -echo "Response: $BODY (HTTP $CODE)" - -if [[ "$CODE" != "202" ]]; then - echo "FAIL: Expected HTTP 202, got $CODE" - exit 1 -fi - -echo "" -echo "=== Getting members with invalid token ===" -RESP=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/get/group/members" \ - -d "token=invalid_token" \ - -d "group=$GROUP_ID") -BODY=$(echo "$RESP" | head -1) -CODE=$(echo "$RESP" | tail -1) -echo "Response: $BODY (HTTP $CODE)" - -if [[ "$CODE" != "401" ]]; then - echo "FAIL: Expected HTTP 401, got $CODE" - exit 1 -fi - -echo "" -echo "=== Getting members with invalid group ID ===" -RESP=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/get/group/members" \ - -d "token=$TOKEN1" \ - -d "group=abc") -BODY=$(echo "$RESP" | head -1) -CODE=$(echo "$RESP" | tail -1) -echo "Response: $BODY (HTTP $CODE)" - -if [[ "$CODE" != "400" ]]; then - echo "FAIL: Expected HTTP 400, got $CODE" - exit 1 -fi - -echo "" -echo "=== Getting members of non-existent group ===" -RESP=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/get/group/members" \ - -d "token=$TOKEN1" \ - -d "group=999999") -BODY=$(echo "$RESP" | head -1) -CODE=$(echo "$RESP" | tail -1) -echo "Response: $BODY (HTTP $CODE)" - -if [[ "$CODE" != "401" ]]; then - echo "FAIL: Expected HTTP 401, got $CODE" - exit 1 -fi - -echo "" -echo "ALL PASSED" diff --git a/tests/cleanup.sh b/tests/cleanup.sh deleted file mode 100755 index efbc338..0000000 --- a/tests/cleanup.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -# Clean up state file left by test scripts -DIR="$(dirname "$0")" -STATE_FILE="$DIR/.state" - -if [[ -f "$STATE_FILE" ]]; then - echo "Removing $STATE_FILE" - rm "$STATE_FILE" - echo "Done." -else - echo "Nothing to clean up." -fi diff --git a/tests/config.sh b/tests/config.sh deleted file mode 100755 index f9f7f94..0000000 --- a/tests/config.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -# Shared config for all test scripts - -BASE_URL="http://localhost:8080" - -USER1_NAME="user_one" -USER1_PASS="password1234" -USER1_COLOR="255,0,0" - -USER2_NAME="user_two" -USER2_PASS="password5678" -USER2_COLOR="0,0,255" - -GROUP_NAME="TestGroup" -GROUP_COLOR="0,255,0" - -# File to persist state between scripts -STATE_FILE="$(dirname "$0")/.state" - -save_state() { - local key="$1" value="$2" - touch "$STATE_FILE" - # Remove existing key if present, then append - sed -i "/^${key}=/d" "$STATE_FILE" - echo "${key}=${value}" >> "$STATE_FILE" -} - -load_state() { - local key="$1" - if [[ -f "$STATE_FILE" ]]; then - grep "^${key}=" "$STATE_FILE" | cut -d'=' -f2- - fi -} - -decode_jwt_sub() { - local token="$1" - echo "$token" | cut -d'.' -f2 | base64 -d 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin)['sub'])" -} diff --git a/tests/run_all.sh b/tests/run_all.sh deleted file mode 100755 index e6f2d84..0000000 --- a/tests/run_all.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/bash -# Run the full test flow end-to-end -set -e - -DIR="$(dirname "$0")" - -# Clean previous state -rm -f "$DIR/.state" - -echo "=============================" -echo " Step 1: Create accounts" -echo "=============================" -bash "$DIR/01_create_accounts.sh" -echo "" - -echo "=============================" -echo " Step 2: Login" -echo "=============================" -bash "$DIR/02_login.sh" -echo "" - -echo "=============================" -echo " Step 3: Create group" -echo "=============================" -bash "$DIR/03_create_group.sh" -echo "" - -echo "=============================" -echo " Step 4: Add user2 to group" -echo "=============================" -bash "$DIR/04_add_user_to_group.sh" -echo "" - -echo "=============================" -echo " Step 5: Send message" -echo "=============================" -bash "$DIR/05_send_message.sh" -echo "" - -echo "=============================" -echo " Step 6: Send message reverse" -echo "=============================" -bash "$DIR/06_send_message_reverse.sh" -echo "" - -echo "=============================" -echo " Step 7: Get groups" -echo "=============================" -bash "$DIR/07_get_groups.sh" -echo "" - -echo "=============================" -echo " Step 8: Get group members" -echo "=============================" -bash "$DIR/08_get_group_members.sh" -echo "" - -echo "=============================" -echo " Cleanup" -echo "=============================" -bash "$DIR/cleanup.sh" diff --git a/wsServer.go b/wsServer.go index 494aa69..bfca2bc 100644 --- a/wsServer.go +++ b/wsServer.go @@ -84,25 +84,6 @@ func sendToAllMessageCloseIfTimeout(message *map[string]any) { } } -func WsSendToGroupAsUser(group *Group, sender *User, message string) error { - for groupUserId := range group.Users { - groupUser, err := CacheGetUserById(groupUserId) - if err != nil || groupUser.Id == sender.Id { - continue - } - - // TODO update on groups rework - var msg = map[string]any{ - // "type": WsEventType.Group, - "from": group.Id, - "sender": sender.Id, - "content": message, - } - WsSendMessageCloseIfTimeout(groupUser, &msg) - } - return nil -} - func handleAuthenticatedMessage(user *User, userMessage *map[string]any) bool { WsSendMessageCloseIfTimeout(user, userMessage) return true @@ -144,33 +125,6 @@ func handleUnauthenticatedMessage(ctx context.Context, user *User, userMessage * userFromCache.WsConn = user.WsConn *user = *userFromCache - for groupId, _ := range userFromCache.Groups { - _, err = CacheGetGroup(groupId) - if err != nil { - dbGroup := &Group{Id: groupId} - - err = DbGroupGetById(ctx, dbGroup) - if err != nil { - response.Event = WsAuthMessage{ - Success: false, - Error: "invalid user data", - } - WsSendMessageCloseIfTimeout(user, response) - return false - } - err = DbGroupGetMembers(ctx, dbGroup) - if err != nil { - response.Event = WsAuthMessage{ - Success: false, - Error: "invalid user data", - } - WsSendMessageCloseIfTimeout(user, response) - return false - } - CacheSaveGroup(dbGroup) - } - } - response.Event = WsAuthMessage{ Success: true, Error: "",