Table bot changes

This commit is contained in:
Blazico
2026-03-21 23:44:37 +01:00
parent 81c6045f02
commit 616407bdbf
4 changed files with 216 additions and 5 deletions

105
api/pinfo.go Normal file
View File

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

View File

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

View File

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

View File

@@ -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