diff --git a/api/self.go b/api/self.go new file mode 100644 index 0000000..dd07f8b --- /dev/null +++ b/api/self.go @@ -0,0 +1,135 @@ +package api + +import ( + "errors" + "fmt" + "net/http" + "slices" + "strconv" + "wwfc/database" + "wwfc/gpcm" + "wwfc/qr2" +) + +type SelfRequest struct { + Secret string `json:"secret"` + // kick or kick_froom + Command string `json:"command"` + // Discord ID of the command sender + DiscordID string `json:"discordID"` + // pid to kick if using kick_froom + ProfileID uint32 `json:"pid"` +} + +var SelfRoute = MakeRouteSpec[SelfRequest, UserActionResponse]( + true, + "/api/self", + func(req any, v bool, _ *http.Request) (any, int, error) { + return handleUserAction(req.(SelfRequest), v, handleSelfImpl) + }, + http.MethodPost, +) + +var ( + ErrUserNotFoundOnline = errors.New("no linked profile was not found online") + ErrNotHostingAnyRoom = errors.New("no linked profiles are hosting any rooms") +) + +func handleSelfImpl(req SelfRequest, _ bool) (*database.User, int, error) { + pids, err := database.GetUsersByDiscordID(pool, ctx, req.DiscordID) + if err != nil { + return nil, http.StatusInternalServerError, err + } + + groups := qr2.GetGroups(nil, nil, false) + + switch req.Command { + case "kick": + return handleSelfKick(pids, groups) + case "kick_froom": + return handleFroomKick(pids, groups, req.ProfileID) + default: + return nil, http.StatusBadRequest, fmt.Errorf("unknown command '%s'", req.Command) + } +} +func handleSelfKick(pids []uint32, groups []qr2.GroupInfo) (*database.User, int, error) { + // Attempt to find a matching user that is online. Assume only one user is + // online at a time which is linked to a specific profile + for _, group := range groups { + for _, player := range group.Players { + pid64, err := strconv.ParseInt(player.ProfileID, 10, 32) + if err != nil { + continue + } + + pid := uint32(pid64) + + if slices.Contains(pids, pid) { + err := gpcm.KickPlayer(pid, "Self Kick", gpcm.WWFCMsgKickedCustom) + if err != nil { + return nil, http.StatusInternalServerError, err + } + + user, err := database.GetProfile(pool, ctx, pid) + if err != nil { + return nil, http.StatusInternalServerError, ErrUserQueryTransaction + } + + return &user, http.StatusOK, nil + } + } + } + + return nil, http.StatusInternalServerError, ErrUserNotFoundOnline +} + +func findHostForPids(pids []uint32, groups []qr2.GroupInfo) (qr2.GroupInfo, error) { + for _, group := range groups { + // Only consider private matches + if group.MatchType == "anybody" { + continue + } + + hostIdx := group.ServerIndex + host := group.Players[hostIdx] + + pid64, err := strconv.ParseInt(host.ProfileID, 10, 32) + if err != nil { + return qr2.GroupInfo{}, err + } + + pid := uint32(pid64) + + if slices.Contains(pids, pid) { + return group, nil + } + } + + return qr2.GroupInfo{}, ErrNotHostingAnyRoom +} + +func handleFroomKick(pids []uint32, groups []qr2.GroupInfo, target uint32) (*database.User, int, error) { + froom, err := findHostForPids(pids, groups) + if err != nil { + return nil, http.StatusInternalServerError, err + } + + targetStr := strconv.FormatUint(uint64(target), 10) + for _, player := range froom.Players { + if player.ProfileID == targetStr { + err := gpcm.KickPlayer(target, "Froom Kick", gpcm.WWFCMsgKickedCustom) + if err != nil { + return nil, http.StatusInternalServerError, err + } + + user, err := database.GetProfile(pool, ctx, target) + if err != nil { + return nil, http.StatusInternalServerError, ErrUserQueryTransaction + } + + return &user, http.StatusOK, nil + } + } + + return nil, http.StatusInternalServerError, ErrUserNotFoundOnline +} diff --git a/database/user.go b/database/user.go index c698a27..8ea3eb7 100644 --- a/database/user.go +++ b/database/user.go @@ -27,6 +27,7 @@ const ( GetUserProfileID = `SELECT profile_id, ng_device_id, email, unique_nick, firstname, lastname, open_host, discord_id, last_ip_address, csnum FROM users WHERE user_id = $1 AND gsbrcd = $2` UpdateUserLastIPAddress = `UPDATE users SET last_ip_address = $2, last_ingamesn = $3 WHERE profile_id = $1` UpdateDiscordID = `UPDATE users SET discord_id = $2 WHERE profile_id = $1` + SearchDiscordID = `SELECT profile_id FROM users WHERE discord_id = $1` UpdateUserBan = `UPDATE users SET has_ban = true, ban_issued = $2, ban_expires = $3, ban_reason = $4, ban_reason_hidden = $5, ban_moderator = $6, ban_tos = $7 WHERE profile_id = $1` DisableUserBan = `UPDATE users SET has_ban = false WHERE profile_id = $1` @@ -76,6 +77,7 @@ var ( ErrReservedProfileIDRange = errors.New("profile ID is in reserved range") ErrFailedToGetMKWFriend = errors.New("failed to get MKW friend info") ErrCountHasNoRows = errors.New("failed to count active users, result has no rows") + ErrNoLinkedProfiles = errors.New("no profiles found with the associated discord id") ) func (user *User) CreateUser(pool *pgxpool.Pool, ctx context.Context) error { @@ -134,6 +136,33 @@ func (user *User) UpdateDiscordID(pool *pgxpool.Pool, ctx context.Context, disco return err } +func GetUsersByDiscordID(pool *pgxpool.Pool, ctx context.Context, discordID string) ([]uint32, error) { + rows, err := pool.Query(ctx, SearchDiscordID, discordID) + + if err != nil { + return nil, err + } + + defer rows.Close() + pids := []uint32{} + + for rows.Next() { + var pid uint32 + err := rows.Scan(&pid) + if err != nil { + return nil, err + } + + pids = append(pids, pid) + } + + if len(pids) == 0 { + return nil, ErrNoLinkedProfiles + } + + return pids, nil +} + func GetUniqueUserID() uint64 { // Not guaranteed unique but doesn't matter in practice if multiple people have the same user ID. return uint64(rand.Int63n(0x80000000000))