delete groups

This commit is contained in:
2026-04-12 19:29:10 +02:00
parent 09a6255213
commit 4f181b2d5c
21 changed files with 4 additions and 1258 deletions
-25
View File
@@ -10,7 +10,6 @@ import (
var ( var (
mu sync.RWMutex mu sync.RWMutex
CacheUsers = make(map[uuid.UUID]*User) CacheUsers = make(map[uuid.UUID]*User)
CacheGroups = make(map[uuid.UUID]*Group)
) )
func CacheGetUserById(id uuid.UUID) (*User, error) { func CacheGetUserById(id uuid.UUID) (*User, error) {
@@ -50,19 +49,6 @@ func CacheDeleteUser(id uuid.UUID) {
delete(CacheUsers, id) 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) { func CacheAddConnection(a, b *User, conn *Connection) {
first, second := a, b first, second := a, b
if a.Id.String() > b.Id.String() { if a.Id.String() > b.Id.String() {
@@ -96,14 +82,3 @@ func CacheGetConnection(user *User, id uuid.UUID) (*Connection, bool) {
return conn, ok 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
}
+1 -181
View File
@@ -6,7 +6,6 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
) )
@@ -66,44 +65,6 @@ func DbInit(ctx context.Context) {
panic(err) 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 { func DbUserSave(ctx context.Context, user *User) error {
@@ -137,26 +98,6 @@ func DbUserGetById(ctx context.Context, user *User) error {
return err 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 { func DbUserSetColor(ctx context.Context, user *User) error {
_, err := dbConn.Exec(ctx, ` _, err := dbConn.Exec(ctx, `
UPDATE users SET color_red = $1, color_green = $2, color_blue = $3 WHERE id = $4 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 { if err := DbUserGetById(ctx, user); err != nil {
return err return err
} }
if err := DbUserGetGroups(ctx, user); err != nil {
return err
}
if err := DbConnectionsGetBelongingToUser(ctx, user); err != nil { if err := DbConnectionsGetBelongingToUser(ctx, user); err != nil {
return err return err
} }
@@ -186,14 +124,6 @@ func DbGetWholeUser(ctx context.Context, user *User) error {
return nil 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 { func DbConnectionSave(ctx context.Context, conn *Connection) error {
return dbConn.QueryRow(ctx, ` return dbConn.QueryRow(ctx, `
INSERT INTO user_connections (requestor_id, recipient_id, state, created_at) VALUES ($1, $2, $3, $4) 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 return err
} }
func DbMessageSave(ctx context.Context, message *Message) error { func DbConnectionMessageSave(ctx context.Context, message *Message) error {
if message.Id != (uuid.UUID{}) { if message.Id != (uuid.UUID{}) {
_, err := dbConn.Exec(ctx, ` _, err := dbConn.Exec(ctx, `
INSERT INTO direct_messages (id, sender_id, receiver_id, created_at, content) VALUES ($1, $2, $3, $4, $5) 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() 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
-15
View File
@@ -26,18 +26,3 @@ func GetUserByToken(ctx context.Context, token string) (*User, error) {
} }
return GetUserById(ctx, userId) 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
}
-3
View File
@@ -1,9 +1,6 @@
package main package main
const ( const (
MaxGroupsForUser uint32 = 8
MaxUsersInGroup uint32 = 12
MaxDirectMsgCache uint32 = 12 MaxDirectMsgCache uint32 = 12
MaxGroupMsgCache uint32 = 8
MessagesPartitions uint8 = 2 MessagesPartitions uint8 = 2
) )
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -70,7 +70,7 @@ func HttpHandleDm(response http.ResponseWriter, request *http.Request) {
Event: message, Event: message,
}) })
err = DbMessageSave(ctx, message) err = DbConnectionMessageSave(ctx, message)
if err != nil { if err != nil {
http.Error(response, "internal server error", http.StatusInternalServerError) http.Error(response, "internal server error", http.StatusInternalServerError)
return return
-433
View File
@@ -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)
}
-10
View File
@@ -20,27 +20,17 @@ func main() {
http.HandleFunc("/new/user", withCORS(HttpHandleUserNew)) http.HandleFunc("/new/user", withCORS(HttpHandleUserNew))
http.HandleFunc("/new/connection", withCORS(HttpHandleUserNewConnection)) http.HandleFunc("/new/connection", withCORS(HttpHandleUserNewConnection))
http.HandleFunc("/new/token", withCORS(HttpHandleUserNewToken)) http.HandleFunc("/new/token", withCORS(HttpHandleUserNewToken))
http.HandleFunc("/new/group", withCORS(HttpHandeGroupCreate))
http.HandleFunc("/mod/user/appearence", withCORS(HttpHandleUserModifyAppearance)) http.HandleFunc("/mod/user/appearence", withCORS(HttpHandleUserModifyAppearance))
http.HandleFunc("/mod/user/about", withCORS(HttpHandleUserModifyAbout)) http.HandleFunc("/mod/user/about", withCORS(HttpHandleUserModifyAbout))
http.HandleFunc("/mod/connection/accept", withCORS(HttpHandleUserElevateConnection)) 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/connections", withCORS(HttpHandleUserGetConnections))
http.HandleFunc("/get/connection/messages", withCORS(HttpHandleUserGetConnectionMessages)) http.HandleFunc("/get/connection/messages", withCORS(HttpHandleUserGetConnectionMessages))
http.HandleFunc("/get/group/members", withCORS(HttpHandleGroupMembersGet))
http.HandleFunc("/del/user", withCORS(HttpHandleUserDelete)) http.HandleFunc("/del/user", withCORS(HttpHandleUserDelete))
http.HandleFunc("/del/group", withCORS(HttpHandleGroupDelete))
http.HandleFunc("/del/connection", withCORS(HttpHandleUserDeleteConnection)) http.HandleFunc("/del/connection", withCORS(HttpHandleUserDeleteConnection))
http.HandleFunc("/msg/user", withCORS(HttpHandleDm)) http.HandleFunc("/msg/user", withCORS(HttpHandleDm))
http.HandleFunc("/msg/group", withCORS(HttpHandleGroupMessage))
http.HandleFunc("/ws", ServeWsConnection) http.HandleFunc("/ws", ServeWsConnection)
log.Println("listening on :8080") log.Println("listening on :8080")
-43
View File
@@ -19,7 +19,6 @@ type User struct {
CreatedAt time.Time CreatedAt time.Time
WsConn *websocket.Conn WsConn *websocket.Conn
Id uuid.UUID Id uuid.UUID
Groups map[uuid.UUID]struct{}
Connections map[uuid.UUID]*Connection Connections map[uuid.UUID]*Connection
Color [3]uint8 Color [3]uint8
} }
@@ -77,48 +76,6 @@ type Message struct {
Receiver uuid.UUID `json:"receiver"` 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 { type LoginReturn struct {
Token string `json:"token"` Token string `json:"token"`
UserId uuid.UUID `json:"userId"` UserId uuid.UUID `json:"userId"`
-22
View File
@@ -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)"
-26
View File
@@ -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"
-19
View File
@@ -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"
-24
View File
@@ -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)"
-79
View File
@@ -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
-79
View File
@@ -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
-52
View File
@@ -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"
-87
View File
@@ -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"
-12
View File
@@ -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
-38
View File
@@ -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'])"
}
-61
View File
@@ -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"
-46
View File
@@ -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 { func handleAuthenticatedMessage(user *User, userMessage *map[string]any) bool {
WsSendMessageCloseIfTimeout(user, userMessage) WsSendMessageCloseIfTimeout(user, userMessage)
return true return true
@@ -144,33 +125,6 @@ func handleUnauthenticatedMessage(ctx context.Context, user *User, userMessage *
userFromCache.WsConn = user.WsConn userFromCache.WsConn = user.WsConn
*user = *userFromCache *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{ response.Event = WsAuthMessage{
Success: true, Success: true,
Error: "", Error: "",