Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions api/self.go
Original file line number Diff line number Diff line change
@@ -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
}
29 changes: 29 additions & 0 deletions database/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
Expand Down