Files
go-socket/http.go
T
2026-04-11 22:49:05 +02:00

1006 lines
24 KiB
Go

package main
import (
"context"
json2 "encoding/json"
"errors"
"fmt"
"maps"
"net/http"
"slices"
"strconv"
"strings"
"time"
"go-socket/Enums/ConnectionState"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
)
func isMethodAllowed(response *http.ResponseWriter, request *http.Request) bool {
if request.Method != http.MethodPost {
http.Error(*response, "POST only", http.StatusMethodNotAllowed)
return false
}
return true
}
func getWholeUserFromDb(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
}
CacheSaveUser(user)
return nil
}
func getUserById(ctx context.Context, userId uint32) (*User, error) {
user, err := CacheGetUserById(userId)
if err != nil {
user = &User{Id: userId}
err = getWholeUserFromDb(ctx, user)
if err != nil {
return nil, err
}
}
return user, nil
}
func getUserByToken(ctx context.Context, token string) (*User, error) {
userId, err := TokenValidateGetId(token)
if err != nil {
return nil, err
}
return getUserById(ctx, userId)
}
func getGroup(ctx context.Context, groupId uint32) (*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
}
func isOwner(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 := ConvertStringUint32(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 !isOwner(user, group) {
http.Error(*response, "no such group", http.StatusUnauthorized)
return nil, nil, errors.New("not an owner")
}
return user, group, nil
}
func getUserByTokenGetUserByIdStrHandleHttp(ctx context.Context, response *http.ResponseWriter, token string, userIdStr string) (*User, *User, error) {
user, err := getUserByToken(ctx, token)
if err != nil {
http.Error(*response, "invalid token", http.StatusUnauthorized)
return nil, nil, err
}
affectedUserId, err := ConvertStringUint32(userIdStr)
if err != nil {
http.Error(*response, "no such user", http.StatusUnauthorized)
return nil, nil, err
}
user2, err := getUserById(ctx, affectedUserId)
if err != nil {
http.Error(*response, "no such user", http.StatusUnauthorized)
return nil, nil, err
}
return user, user2, nil
}
func HttpHandleUserNew(response http.ResponseWriter, request *http.Request) {
if !isMethodAllowed(&response, request) {
return
}
username := request.FormValue("username")
if len(username) < 4 {
http.Error(response, "no or short username", http.StatusBadRequest)
return
}
password := request.FormValue("password")
if len(password) < 8 {
http.Error(response, "no or short password", http.StatusBadRequest)
return
}
color, err := ConvertStringToRgb(request.FormValue("color"))
if err != nil {
http.Error(response, "bad color", http.StatusBadRequest)
return
}
hashedPassword, err := PasswordHash(password)
if err != nil {
http.Error(response, "internal server error", http.StatusInternalServerError)
return
}
newUser := &User{
Name: username,
PasswordHash: hashedPassword,
Color: color,
CreatedAt: time.Now(),
}
ctx := request.Context()
err = DbUserSave(ctx, newUser)
if err != nil {
http.Error(response, "name taken", http.StatusUnauthorized)
return
}
response.WriteHeader(http.StatusCreated)
}
func HttpHandleUserDelete(response http.ResponseWriter, request *http.Request) {
if !isMethodAllowed(&response, request) {
return
}
ctx := request.Context()
userId, err := TokenValidateGetId(request.FormValue("token"))
if err != nil {
http.Error(response, "invalid token", http.StatusUnauthorized)
return
}
err = DbUserDelete(ctx, userId)
if err != nil {
http.Error(response, "internal server error", http.StatusInternalServerError)
return
}
CacheDeleteUser(userId)
response.WriteHeader(http.StatusAccepted)
}
// HttpHandleUserModifyAppearance currently just color
func HttpHandleUserModifyAppearance(response http.ResponseWriter, request *http.Request) {
if !isMethodAllowed(&response, request) {
return
}
ctx := request.Context()
user, err := getUserByToken(ctx, request.FormValue("token"))
if err != nil {
http.Error(response, "invalid token", http.StatusUnauthorized)
return
}
color, err := ConvertStringToRgb(request.FormValue("color"))
if err != nil {
http.Error(response, "invalid color", http.StatusBadRequest)
return
}
user.Color = color
err = DbUserSetColor(ctx, user)
if err != nil {
http.Error(response, "internal server error", http.StatusInternalServerError)
return
}
response.WriteHeader(http.StatusAccepted)
}
// HttpHandleUserModifyAbout currently just pronouns
func HttpHandleUserModifyAbout(response http.ResponseWriter, request *http.Request) {
if !isMethodAllowed(&response, request) {
return
}
ctx := request.Context()
user, err := getUserByToken(ctx, request.FormValue("token"))
if err != nil {
http.Error(response, "invalid token", http.StatusUnauthorized)
return
}
pronouns := request.FormValue("pronouns")
if len(pronouns) > 25 || len(pronouns) < 2 {
http.Error(response, "invalid pronouns", http.StatusBadRequest)
return
}
user.Pronouns = pronouns
err = DbUserSetPronouns(ctx, user)
if err != nil {
http.Error(response, "internal server error", http.StatusInternalServerError)
return
}
response.WriteHeader(http.StatusAccepted)
}
func HttpHandleUserMessage(response http.ResponseWriter, request *http.Request) {
if !isMethodAllowed(&response, request) {
return
}
ctx := request.Context()
user, err := getUserByToken(ctx, request.FormValue("token"))
if err != nil {
http.Error(response, "invalid token", http.StatusUnauthorized)
return
}
targetConnection, err := ConvertStringUuid(request.FormValue("connectionid"))
if err != nil {
http.Error(response, "invalid connectionid", http.StatusBadRequest)
return
}
conn, ok := CacheGetConnection(user, targetConnection)
if !ok {
http.Error(response, "invalid connectionid", http.StatusBadRequest)
return
}
msgContent := request.FormValue("msgContent")
if msgContent == "" {
http.Error(response, "empty msgContent", http.StatusBadRequest)
return
}
var target *User
if user.Id == conn.RequestorId {
target, err = getUserById(ctx, conn.RecipientId)
} else if user.Id == conn.RecipientId {
target, err = getUserById(ctx, conn.RequestorId)
} else {
http.Error(response, "invalid connectionid", http.StatusBadRequest)
return
}
if err != nil {
http.Error(response, "internal server error", http.StatusInternalServerError)
return
}
message := &Message{
Id: uuid.New(),
Content: msgContent,
CreatedAt: time.Now(),
Sender: user.Id,
Receiver: conn.Id,
IsGroupMessage: false,
}
WsMessageSendToUser(target, message)
err = DbMessageSave(ctx, message)
if err != nil {
http.Error(response, "internal server error", http.StatusInternalServerError)
return
}
response.WriteHeader(http.StatusAccepted)
}
func HttpHandleUserGetConnectionMessages(response http.ResponseWriter, request *http.Request) {
if !isMethodAllowed(&response, request) {
return
}
ctx := request.Context()
user, err := getUserByToken(ctx, request.FormValue("token"))
if err != nil {
http.Error(response, "invalid token", http.StatusUnauthorized)
return
}
connectionId, err := ConvertStringUuid(request.FormValue("connectionid"))
if err != nil {
http.Error(response, "invalid connectionid", http.StatusBadRequest)
return
}
before, err := ConvertStringTimestamp(request.FormValue("before"))
if err != nil {
before = time.Now()
}
messagesCap, err := ConvertStringUint32(request.FormValue("messages"))
if err != nil {
messagesCap = MaxDirectMsgCache
}
conn, ok := CacheGetConnection(user, connectionId)
if !ok {
http.Error(response, "invalid connectionid", http.StatusBadRequest)
return
}
buffer, bufferSize := conn.GetSortedMessagesBuff()
var validBufCount uint32
for validBufCount < bufferSize && buffer[validBufCount].CreatedAt.Before(before) {
validBufCount++
}
var messages []*Message
if validBufCount >= messagesCap {
start := validBufCount - messagesCap
messages = make([]*Message, messagesCap)
for i := uint32(0); i < messagesCap; i++ {
messages[i] = buffer[start+i]
}
} else {
remaining := messagesCap - validBufCount
cutoff := before
if validBufCount > 0 {
cutoff = buffer[0].CreatedAt
}
dbMessages, err := DbConnectionGetMessagesBefore(ctx, cutoff, connectionId, remaining)
if err != nil {
http.Error(response, "internal server error", http.StatusInternalServerError)
return
}
messages = make([]*Message, 0, uint32(len(dbMessages))+validBufCount)
messages = append(messages, dbMessages...)
for i := uint32(0); i < validBufCount; i++ {
messages = append(messages, buffer[i])
}
}
json, err := json2.Marshal(messages)
if err != nil {
http.Error(response, "internal server error", http.StatusInternalServerError)
return
}
response.WriteHeader(http.StatusOK)
response.Write(json)
}
func HttpHandleUserNewConnection(response http.ResponseWriter, request *http.Request) {
if !isMethodAllowed(&response, request) {
return
}
ctx := request.Context()
requestor, recipient, err := getUserByTokenGetUserByIdStrHandleHttp(ctx, &response, request.FormValue("token"), request.FormValue("recipient"))
if err != nil {
return
}
if requestor.Id == recipient.Id {
http.Error(response, "cannot connect to yourself", http.StatusBadRequest)
return
}
requestor.Mu.RLock()
for _, connection := range requestor.Connections {
if (connection.RequestorId == requestor.Id && connection.RecipientId == recipient.Id) ||
(connection.RecipientId == requestor.Id && connection.RequestorId == recipient.Id) {
requestor.Mu.RUnlock()
http.Error(response, "connection already exists", http.StatusBadRequest)
return
}
}
requestor.Mu.RUnlock()
connection := &Connection{
CreatedAt: time.Now(),
RequestorId: requestor.Id,
RecipientId: recipient.Id,
State: ConnectionState.Stranger,
}
err = DbConnectionSave(ctx, connection)
if err != nil {
http.Error(response, "internal server error", http.StatusInternalServerError)
return
}
CacheAddConnection(requestor, recipient, connection)
response.WriteHeader(http.StatusCreated)
return
}
func HttpHandleUserDeleteConnection(response http.ResponseWriter, request *http.Request) {
if !isMethodAllowed(&response, request) {
return
}
ctx := request.Context()
user, err := getUserByToken(ctx, request.FormValue("token"))
if err != nil {
http.Error(response, "invalid token", http.StatusUnauthorized)
return
}
connectionId, err := ConvertStringUuid(request.FormValue("connectionid"))
if err != nil {
http.Error(response, "invalid connectionid", http.StatusBadRequest)
return
}
conn, ok := CacheGetConnection(user, connectionId)
if !ok {
http.Error(response, "invalid connectionid", http.StatusBadRequest)
return
}
var user2 *User
if conn.RequestorId == user.Id {
recipient, err := getUserById(ctx, conn.RecipientId)
if err != nil {
http.Error(response, "internal server error", http.StatusInternalServerError)
return
}
user2 = recipient
} else if conn.RecipientId == user.Id {
requestor, err := getUserById(ctx, conn.RequestorId)
if err != nil {
http.Error(response, "internal server error", http.StatusInternalServerError)
return
}
user2 = requestor
} else {
http.Error(response, "invalid connectionid", http.StatusBadRequest)
return
}
err = DbConnectionDelete(ctx, conn)
if err != nil {
http.Error(response, "internal server error", http.StatusInternalServerError)
return
}
CacheDeleteConnection(user, user2, connectionId)
response.WriteHeader(http.StatusAccepted)
}
func HttpHandleUserElevateConnection(response http.ResponseWriter, request *http.Request) {
if !isMethodAllowed(&response, request) {
return
}
ctx := request.Context()
user, err := getUserByToken(ctx, request.FormValue("token"))
if err != nil {
http.Error(response, "invalid token", http.StatusUnauthorized)
return
}
connectionId, err := ConvertStringUuid(request.FormValue("connectionid"))
if err != nil {
http.Error(response, "invalid connectionid", http.StatusBadRequest)
return
}
conn, ok := CacheGetConnection(user, connectionId)
if !ok {
http.Error(response, "invalid connectionid", http.StatusBadRequest)
return
}
if conn.RecipientId != user.Id {
http.Error(response, "invalid connectionid", http.StatusBadRequest)
return
}
switch conn.State {
case ConnectionState.Stranger:
conn.State = ConnectionState.Friend
break
case ConnectionState.GroupFellow:
conn.State = ConnectionState.Stranger
break
default:
http.Error(response, "cannot elevate further", http.StatusBadRequest)
return
}
err = DbConnectionUpdateState(ctx, conn)
if err != nil {
http.Error(response, "internal server error", http.StatusInternalServerError)
return
}
response.WriteHeader(http.StatusAccepted)
}
func HttpHandleUserGetConnections(response http.ResponseWriter, request *http.Request) {
if !isMethodAllowed(&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()
connections := slices.Collect(maps.Values(user.Connections))
user.Mu.RUnlock()
json, err := json2.Marshal(connections)
if err != nil {
http.Error(response, "internal server error", http.StatusInternalServerError)
return
}
response.WriteHeader(http.StatusOK)
response.Write(json)
}
func HttpHandleTokenNew(response http.ResponseWriter, request *http.Request) {
if !isMethodAllowed(&response, request) {
return
}
username := request.FormValue("username")
if len(username) < 4 {
http.Error(response, "no or short username", http.StatusBadRequest)
return
}
password := request.FormValue("password")
if len(password) < 8 {
http.Error(response, "no or short password", http.StatusBadRequest)
return
}
var (
user *User
err error
ctx = request.Context()
)
user, err = CacheGetUserByName(username)
if err != nil {
user = &User{Name: username}
if err = DbUserGetStandardInfoByName(ctx, user); err != nil {
http.Error(response, "bad login", http.StatusUnauthorized)
return
}
if err = getWholeUserFromDb(ctx, user); err != nil {
http.Error(response, err.Error(), http.StatusInternalServerError)
return
}
}
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))
if err != nil {
http.Error(response, "bad login", http.StatusUnauthorized)
return
}
token, err := TokenCreate(user.Id)
if err != nil {
http.Error(response, "internal server error2", http.StatusInternalServerError)
return
}
json, err := json2.Marshal(LoginReturn{Token: token, UserId: user.Id})
if err != nil {
http.Error(response, "internal server error3", http.StatusInternalServerError)
return
}
response.WriteHeader(http.StatusCreated)
response.Write(json)
}
func HttpHandeGroupCreate(response http.ResponseWriter, request *http.Request) {
if !isMethodAllowed(&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[uint32]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, "%d", group.Id)
}
func HttpHandleGroupDelete(response http.ResponseWriter, request *http.Request) {
if !isMethodAllowed(&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 !isMethodAllowed(&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]uint32
var idx uint32 = 0
for _, s := range usersStringSlice {
if idx >= MaxUsersInGroup {
break
}
id, err := ConvertStringUint32(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 !isMethodAllowed(&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]uint32
var idx uint32 = 0
for _, s := range usersStringSlice {
if idx >= MaxUsersInGroup {
break
}
id, err := ConvertStringUint32(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 !isMethodAllowed(&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 !isMethodAllowed(&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 = getWholeUserFromDb(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 !isMethodAllowed(&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 := ConvertStringUint32(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 = WsSendToGroup(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 !isMethodAllowed(&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([]uint32, 0, len(user.Groups))
for groupId := range user.Groups {
groupIds = append(groupIds, groupId)
}
user.Mu.RUnlock()
groups := make(map[uint32]*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 !isMethodAllowed(&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 := ConvertStringUint32(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)
}