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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ state
# Executables
*.exe
*.exe~
wwfc

# Editor files
.vscode
.github
60 changes: 60 additions & 0 deletions api/mkw_rr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package api

import (
"encoding/json"
"net/http"
"net/url"
"strconv"
"wwfc/qr2"
)

type RaceResultInfo struct {
Players map[uint32]qr2.PlayerInfo `json:"players"`
Results map[int][]qr2.RaceResult `json:"results"`
}

func HandleMKWRR(w http.ResponseWriter, r *http.Request) {
u, err := url.Parse(r.URL.String())
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}

query, err := url.ParseQuery(u.RawQuery)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}

groupNames := query["id"]
if len(groupNames) != 1 {
w.WriteHeader(http.StatusBadRequest)
return
}

groupName := query["id"][0]
players := qr2.GetRacePlayersForGroup(groupName)
results := qr2.GetRaceResultsForGroup(groupName)
if results == nil {
w.WriteHeader(http.StatusNotFound)
return
}
if players == nil {
players = map[uint32]qr2.PlayerInfo{}
}

var jsonData []byte
jsonData, err = json.Marshal(RaceResultInfo{
Players: players,
Results: results,
})
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Length", strconv.Itoa(len(jsonData)))
w.Write(jsonData)
}
106 changes: 106 additions & 0 deletions api/pinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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,
DiscordID: realUser.DiscordID,
}
}

return PinfoResponse{
User: user,
Success: true,
Error: "",
}, http.StatusOK
}
16 changes: 14 additions & 2 deletions database/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsb
} else {
var firstName *string
var lastName *string
var discordID *string

err := pool.QueryRow(ctx, GetUserProfileID, userId, gsbrcd).Scan(&user.ProfileId, &user.NgDeviceId, &user.Email, &user.UniqueNick, &firstName, &lastName, &user.OpenHost, &lastIPAddress)
err := pool.QueryRow(ctx, GetUserProfileID, userId, gsbrcd).Scan(&user.ProfileId, &user.NgDeviceId, &user.Email, &user.UniqueNick, &firstName, &lastName, &user.OpenHost, &discordID, &lastIPAddress)
if err != nil {
return User{}, err
}
Expand All @@ -90,6 +91,11 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsb
user.LastName = *lastName
}

if discordID != nil {
user.DiscordID = *discordID
user.LinkStage = LS_FINISHED
}

validDeviceId := false
deviceIdList := ""
for index, id := range user.NgDeviceId {
Expand Down Expand Up @@ -226,7 +232,8 @@ func LoginUserToGameStats(pool *pgxpool.Pool, ctx context.Context, userId uint64
var firstName *string
var lastName *string
var lastIPAddress *string
err := pool.QueryRow(ctx, GetUserProfileID, userId, gsbrcd).Scan(&user.ProfileId, &user.NgDeviceId, &user.Email, &user.UniqueNick, &firstName, &lastName, &user.OpenHost, &lastIPAddress)
var discordID *string
err := pool.QueryRow(ctx, GetUserProfileID, userId, gsbrcd).Scan(&user.ProfileId, &user.NgDeviceId, &user.Email, &user.UniqueNick, &firstName, &lastName, &user.OpenHost, &discordID, &lastIPAddress)
if err != nil {
return User{}, err
}
Expand All @@ -239,5 +246,10 @@ func LoginUserToGameStats(pool *pgxpool.Pool, ctx context.Context, userId uint64
user.LastName = *lastName
}

if discordID != nil {
user.DiscordID = *discordID
user.LinkStage = LS_FINISHED
}

return user, nil
}
106 changes: 97 additions & 9 deletions database/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import (
"math/rand"
"time"

"wwfc/logging"

"github.com/jackc/pgx/v4/pgxpool"
"github.com/logrusorgru/aurora/v3"
)

const (
Expand All @@ -15,20 +18,30 @@ 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`

GetMKWFriendInfoQuery = `SELECT mariokartwii_friend_info FROM users WHERE profile_id = $1`
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
Expand All @@ -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 (
Expand All @@ -56,9 +75,7 @@ func (user *User) CreateUser(pool *pgxpool.Pool, ctx context.Context) error {
return pool.QueryRow(ctx, InsertUser, user.UserId, user.GsbrCode, "", user.NgDeviceId, user.Email, user.UniqueNick).Scan(&user.ProfileId)
}

if user.ProfileId >= 1000000000 {
return ErrReservedProfileIDRange
}
// Reserved profile ID check removed; all profile IDs allowed

var exists bool
err := pool.QueryRow(ctx, IsProfileIDInUse, user.ProfileId).Scan(&exists)
Expand All @@ -75,9 +92,7 @@ func (user *User) CreateUser(pool *pgxpool.Pool, ctx context.Context) error {
}

func (user *User) UpdateProfileID(pool *pgxpool.Pool, ctx context.Context, newProfileId uint32) error {
if newProfileId >= 1000000000 {
return ErrReservedProfileIDRange
}
// Reserved profile ID check removed; all profile IDs allowed

var exists bool
err := pool.QueryRow(ctx, IsProfileIDInUse, newProfileId).Scan(&exists)
Expand All @@ -97,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))
Expand Down Expand Up @@ -132,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
}

Expand Down
Loading