From 616407bdbfb4d60ab399a5b51cc8c1e19c700eb9 Mon Sep 17 00:00:00 2001 From: Blazico Date: Sat, 21 Mar 2026 23:44:37 +0100 Subject: [PATCH] Table bot changes --- api/pinfo.go | 105 +++++++++++++++++++++++++++++++++++++++++++++++ database/user.go | 98 +++++++++++++++++++++++++++++++++++++++++-- nas/main.go | 6 +++ qr2/group.go | 12 +++++- 4 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 api/pinfo.go diff --git a/api/pinfo.go b/api/pinfo.go new file mode 100644 index 0000000..ccaf265 --- /dev/null +++ b/api/pinfo.go @@ -0,0 +1,105 @@ +package api + +import ( + "encoding/json" + "io" + "net/http" + "strconv" + "wwfc/database" +) + +type PinfoRequestSpec struct { + Secret string `json:"secret"` + ProfileID uint32 `json:"pid"` +} + +type PinfoResponse struct { + User database.User `json:"User"` + Success bool `json:"Success"` + Error string `json:"Error"` +} + +func HandlePinfo(w http.ResponseWriter, r *http.Request) { + var response PinfoResponse + var statusCode int + + if r.Method == http.MethodPost { + response, statusCode = handlePinfoImpl(r) + } else if r.Method == http.MethodOptions { + statusCode = http.StatusNoContent + w.Header().Set("Access-Control-Allow-Methods", "POST") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + } else { + statusCode = http.StatusMethodNotAllowed + w.Header().Set("Allow", "POST") + response = PinfoResponse{ + Success: false, + Error: "Incorrect request. POST only.", + } + } + + w.Header().Set("Access-Control-Allow-Origin", "*") + + var jsonData []byte + if statusCode != http.StatusNoContent { + w.Header().Set("Content-Type", "application/json") + jsonData, _ = json.Marshal(response) + } + + w.Header().Set("Content-Length", strconv.Itoa(len(jsonData))) + w.WriteHeader(statusCode) + w.Write(jsonData) +} + +func handlePinfoImpl(r *http.Request) (PinfoResponse, int) { + body, err := io.ReadAll(r.Body) + if err != nil { + return PinfoResponse{ + Success: false, + Error: "Unable to read request body", + }, http.StatusBadRequest + } + + var req PinfoRequestSpec + err = json.Unmarshal(body, &req) + if err != nil { + return PinfoResponse{ + Success: false, + Error: err.Error(), + }, http.StatusBadRequest + } + + if req.ProfileID == 0 { + return PinfoResponse{ + Success: false, + Error: "Profile ID missing or 0 in request", + }, http.StatusBadRequest + } + + realUser, ok := database.GetProfile(pool, ctx, req.ProfileID) + if !ok { + return PinfoResponse{ + User: database.User{}, + Success: false, + Error: "Failed to find user in the database", + }, http.StatusInternalServerError + } + + user := realUser + if apiSecret == "" || req.Secret != apiSecret { + // Invalid or missing secret: return only the public-safe subset. + user = database.User{ + ProfileId: realUser.ProfileId, + Restricted: realUser.Restricted, + BanReason: realUser.BanReason, + OpenHost: realUser.OpenHost, + LastInGameSn: realUser.LastInGameSn, + } + } + + return PinfoResponse{ + User: user, + Success: true, + Error: "", + }, http.StatusOK +} diff --git a/database/user.go b/database/user.go index 3757c27..fe1c77f 100644 --- a/database/user.go +++ b/database/user.go @@ -6,7 +6,10 @@ import ( "math/rand" "time" + "wwfc/logging" + "github.com/jackc/pgx/v4/pgxpool" + "github.com/logrusorgru/aurora/v3" ) const ( @@ -15,13 +18,14 @@ const ( UpdateUserTable = `UPDATE users SET firstname = CASE WHEN $3 THEN $2 ELSE firstname END, lastname = CASE WHEN $5 THEN $4 ELSE lastname END, open_host = CASE WHEN $7 THEN $6 ELSE open_host END WHERE profile_id = $1` UpdateUserProfileID = `UPDATE users SET profile_id = $3 WHERE user_id = $1 AND gsbrcd = $2` UpdateUserNGDeviceID = `UPDATE users SET ng_device_id = $2 WHERE profile_id = $1` - GetUser = `SELECT user_id, gsbrcd, email, unique_nick, firstname, lastname, open_host, last_ip_address, last_ingamesn FROM users WHERE profile_id = $1` + GetUser = `SELECT user_id, gsbrcd, ng_device_id, email, unique_nick, firstname, lastname, has_ban, ban_reason, open_host, last_ingamesn, last_ip_address, discord_id, ban_moderator, ban_reason_hidden, ban_issued, ban_expires FROM users WHERE profile_id = $1` ClearProfileQuery = `DELETE FROM users WHERE profile_id = $1 RETURNING user_id, gsbrcd, email, unique_nick, firstname, lastname, open_host, last_ip_address, last_ingamesn` DoesUserExist = `SELECT EXISTS(SELECT 1 FROM users WHERE user_id = $1 AND gsbrcd = $2)` IsProfileIDInUse = `SELECT EXISTS(SELECT 1 FROM users WHERE profile_id = $1)` DeleteUserSession = `DELETE FROM sessions WHERE profile_id = $1` - GetUserProfileID = `SELECT profile_id, ng_device_id, email, unique_nick, firstname, lastname, open_host, last_ip_address FROM users WHERE user_id = $1 AND gsbrcd = $2` + GetUserProfileID = `SELECT profile_id, ng_device_id, email, unique_nick, firstname, lastname, open_host, discord_id, last_ip_address 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` 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` @@ -29,6 +33,15 @@ const ( UpdateMKWFriendInfoQuery = `UPDATE users SET mariokartwii_friend_info = $2 WHERE profile_id = $1` ) +type LinkStage byte + +const ( + LS_NONE LinkStage = iota + LS_STARTED + LS_FRIENDED + LS_FINISHED +) + type User struct { ProfileId uint32 UserId uint64 @@ -44,6 +57,12 @@ type User struct { OpenHost bool LastInGameSn string LastIPAddress string + DiscordID string + LinkStage LinkStage + BanModerator string + BanReasonHidden string + BanIssued *time.Time + BanExpires *time.Time } var ( @@ -93,6 +112,17 @@ func (user *User) UpdateProfileID(pool *pgxpool.Pool, ctx context.Context, newPr return err } +func (user *User) UpdateDiscordID(pool *pgxpool.Pool, ctx context.Context, discordID string) error { + _, err := pool.Exec(ctx, UpdateDiscordID, user.ProfileId, discordID) + if err == nil { + user.DiscordID = discordID + } else { + logging.Error("DB", "Failed to persist DiscordID", aurora.Cyan(discordID), "for profile", aurora.Cyan(user.ProfileId), "error:", aurora.Cyan(err)) + } + + return err +} + 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)) @@ -128,12 +158,74 @@ func (user *User) UpdateProfile(pool *pgxpool.Pool, ctx context.Context, data ma func GetProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32) (User, bool) { user := User{} row := pool.QueryRow(ctx, GetUser, profileId) - err := row.Scan(&user.UserId, &user.GsbrCode, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName, &user.OpenHost, &user.LastIPAddress, &user.LastInGameSn) + + var firstName *string + var lastName *string + var banReason *string + var lastInGameSn *string + var lastIPAddress *string + var discordID *string + var banModerator *string + var banReasonHidden *string + + err := row.Scan( + &user.UserId, + &user.GsbrCode, + &user.NgDeviceId, + &user.Email, + &user.UniqueNick, + &firstName, + &lastName, + &user.Restricted, + &banReason, + &user.OpenHost, + &lastInGameSn, + &lastIPAddress, + &discordID, + &banModerator, + &banReasonHidden, + &user.BanIssued, + &user.BanExpires, + ) if err != nil { return User{}, false } user.ProfileId = profileId + + if firstName != nil { + user.FirstName = *firstName + } + + if lastName != nil { + user.LastName = *lastName + } + + if banReason != nil { + user.BanReason = *banReason + } + + if lastInGameSn != nil { + user.LastInGameSn = *lastInGameSn + } + + if lastIPAddress != nil { + user.LastIPAddress = *lastIPAddress + } + + if discordID != nil { + user.DiscordID = *discordID + user.LinkStage = LS_FINISHED + } + + if banModerator != nil { + user.BanModerator = *banModerator + } + + if banReasonHidden != nil { + user.BanReasonHidden = *banReasonHidden + } + return user, true } diff --git a/nas/main.go b/nas/main.go index 722675d..366ef54 100644 --- a/nas/main.go +++ b/nas/main.go @@ -153,6 +153,12 @@ func handleRequest(w http.ResponseWriter, r *http.Request) { return } + // Check for /api/pinfo + if r.URL.Path == "/api/pinfo" { + api.HandlePinfo(w, r) + return + } + // Check for /api/stats if r.URL.Path == "/api/stats" { api.HandleStats(w, r) diff --git a/qr2/group.go b/qr2/group.go index bf6a6b1..bdc2e9c 100644 --- a/qr2/group.go +++ b/qr2/group.go @@ -605,12 +605,20 @@ func ProcessMKWRaceResult(profileId uint32, playerPid int, finishTimeMs int, cha // Calculate delta using start/finish times var delta int - if startTiming, exists := raceStartTimings[profileId]; exists { - if finishTiming, exists := raceFinishTimings[profileId]; exists { + if startTiming, startExists := raceStartTimings[profileId]; startExists { + if finishTiming, finishExists := raceFinishTimings[profileId]; finishExists { clientElapsedTime := finishTiming.ClientTime - startTiming.ClientTime serverElapsedTime := finishTiming.ServerTime - startTiming.ServerTime delta = int(serverElapsedTime - clientElapsedTime) + + logging.Info(moduleName, "Delta calculated:", aurora.Cyan(strconv.Itoa(delta)), + "Client elapsed:", aurora.Cyan(strconv.FormatInt(clientElapsedTime, 10)), + "Server elapsed:", aurora.Cyan(strconv.FormatInt(serverElapsedTime, 10))) + } else { + logging.Warn(moduleName, "Missing finish timing data for profile", aurora.Cyan(strconv.FormatUint(uint64(profileId), 10))) } + } else { + logging.Warn(moduleName, "Missing start timing data for profile", aurora.Cyan(strconv.FormatUint(uint64(profileId), 10))) } // Calculate finish position based on current race results