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) }