diff --git a/api/main.go b/api/main.go index be07159..4f98c93 100644 --- a/api/main.go +++ b/api/main.go @@ -106,6 +106,7 @@ func StartServer(reload bool) { PinfoRoute, QueryRoute, RemoveHashRoute, + SetMKWRatingRoute, SetHashRoute, UnbanRoute, } diff --git a/api/mkw_rr_ratings.go b/api/mkw_rr_ratings.go new file mode 100644 index 0000000..7e84212 --- /dev/null +++ b/api/mkw_rr_ratings.go @@ -0,0 +1,64 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/url" + "strconv" + "wwfc/database" +) + +type MKWRatingResponse struct { + Found int32 `json:"found"` + VR int32 `json:"vr"` + BR int32 `json:"br"` +} + +func HandleMKWRatings(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 + } + + pids := query["pid"] + if len(pids) != 1 { + w.WriteHeader(http.StatusBadRequest) + return + } + + pid64, err := strconv.ParseUint(pids[0], 10, 32) + if err != nil || pid64 == 0 { + w.WriteHeader(http.StatusBadRequest) + return + } + + vr, br, found := database.GetMKWVRBR(pool, ctx, uint32(pid64)) + response := MKWRatingResponse{ + Found: 0, + VR: 0, + BR: 0, + } + if found { + response.Found = 1 + response.VR = vr + response.BR = br + } + + jsonData, err := json.Marshal(response) + 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) +} diff --git a/api/pinfo.go b/api/pinfo.go index 7dab1ed..541389a 100644 --- a/api/pinfo.go +++ b/api/pinfo.go @@ -31,6 +31,12 @@ func handlePinfoImpl(req PinfoRequest, validSecret bool) (*database.User, int, e return &database.User{}, http.StatusInternalServerError, err } + vr, br, ratingErr := database.GetMKWRawVRBR(pool, ctx, req.ProfileID) + if ratingErr == nil { + realUser.VR = vr + realUser.BR = br + } + if !validSecret { // Invalid secret, only report normal user info ret = &database.User{ @@ -42,6 +48,8 @@ func handlePinfoImpl(req PinfoRequest, validSecret bool) (*database.User, int, e BanIssued: realUser.BanIssued, BanExpires: realUser.BanExpires, DiscordID: realUser.DiscordID, + VR: realUser.VR, + BR: realUser.BR, } } else { ret = &realUser diff --git a/api/set_mkw_rating.go b/api/set_mkw_rating.go new file mode 100644 index 0000000..f199a91 --- /dev/null +++ b/api/set_mkw_rating.go @@ -0,0 +1,117 @@ +package api + +import ( + "errors" + "net/http" + "strings" + "wwfc/database" + "wwfc/gpcm" +) + +const ( + defaultMKWManualRating = 5000 + minMKWManualRating = 100 + maxMKWManualRating = 1000000 +) + +var ( + ErrRatingType = errors.New("rating type must be either 'vr' or 'br'") + ErrRatingValue = errors.New("rating value must be between 100 and 1000000") +) + +type SetMKWRatingRequest struct { + Secret string `json:"secret"` + ProfileID uint32 `json:"pid"` + RatingType string `json:"rating_type"` + Reason string `json:"reason"` + Value int32 `json:"value"` +} + +type SetMKWRatingResponse struct { + User database.User `json:"user"` + RatingType string `json:"rating_type"` + PreviousValue int32 `json:"previous_value"` + Value int32 `json:"value"` + VR int32 `json:"vr"` + BR int32 `json:"br"` + Success bool `json:"success"` + Error string `json:"error"` +} + +var SetMKWRatingRoute = MakeRouteSpec[SetMKWRatingRequest, SetMKWRatingResponse]( + true, + "/api/set_mkw_rating", + func(req any, _ bool, _ *http.Request) (any, int, error) { + return handleSetMKWRatingImpl(req.(SetMKWRatingRequest)) + }, + http.MethodPost, +) + +func handleSetMKWRatingImpl(req SetMKWRatingRequest) (SetMKWRatingResponse, int, error) { + if req.ProfileID == 0 { + return SetMKWRatingResponse{}, http.StatusBadRequest, ErrPIDMissing + } + + if req.Value < minMKWManualRating || req.Value > maxMKWManualRating { + return SetMKWRatingResponse{}, http.StatusBadRequest, ErrRatingValue + } + + currentVR := int32(defaultMKWManualRating) + currentBR := int32(defaultMKWManualRating) + + storedVR, storedBR, err := database.GetMKWRawVRBR(pool, ctx, req.ProfileID) + if err != nil { + return SetMKWRatingResponse{}, http.StatusNotFound, ErrUserQuery + } + + if storedVR != nil { + currentVR = *storedVR + } + + if storedBR != nil { + currentBR = *storedBR + } + + ratingType := strings.ToLower(req.RatingType) + reason := strings.TrimSpace(req.Reason) + previousValue := int32(0) + + switch ratingType { + case "vr": + previousValue = currentVR + currentVR = req.Value + case "br": + previousValue = currentBR + currentBR = req.Value + default: + return SetMKWRatingResponse{}, http.StatusBadRequest, ErrRatingType + } + + if err := database.UpdateMKWVRBR(pool, ctx, req.ProfileID, currentVR, currentBR); err != nil { + return SetMKWRatingResponse{}, http.StatusInternalServerError, ErrTransaction + } + + user, err := database.GetProfile(pool, ctx, req.ProfileID) + if err != nil { + return SetMKWRatingResponse{}, http.StatusInternalServerError, ErrUserQueryTransaction + } + + kickReason := "Your VR/BR was updated." + if reason != "" { + kickReason = "Your VR/BR was updated. - " + reason + } + + err = gpcm.KickPlayer(req.ProfileID, kickReason, gpcm.WWFCMsgKickedCustom) + if err != nil { + return SetMKWRatingResponse{}, http.StatusInternalServerError, err + } + + return SetMKWRatingResponse{ + User: user, + RatingType: ratingType, + PreviousValue: previousValue, + Value: req.Value, + VR: currentVR, + BR: currentBR, + }, http.StatusOK, nil +} diff --git a/database/schema.go b/database/schema.go index 43ad4ea..7fcd58d 100644 --- a/database/schema.go +++ b/database/schema.go @@ -19,8 +19,16 @@ func UpdateTables(pool *pgxpool.Pool, ctx context.Context) { ADD IF NOT EXISTS ban_reason_hidden character varying, ADD IF NOT EXISTS ban_moderator character varying, ADD IF NOT EXISTS ban_tos boolean, - ADD IF NOT EXISTS open_host boolean DEFAULT false; - ADD IF NOT EXISTS discord_id character varying + ADD IF NOT EXISTS open_host boolean DEFAULT false, + ADD IF NOT EXISTS discord_id character varying; + + `) + + pool.Exec(ctx, ` + + ALTER TABLE ONLY public.users + ADD IF NOT EXISTS mariokartwii_vr integer, + ADD IF NOT EXISTS mariokartwii_br integer; `) diff --git a/database/user.go b/database/user.go index c698a27..b1adeb5 100644 --- a/database/user.go +++ b/database/user.go @@ -33,6 +33,9 @@ const ( GetMKWFriendInfoQuery = `SELECT mariokartwii_friend_info FROM users WHERE profile_id = $1` UpdateMKWFriendInfoQuery = `UPDATE users SET mariokartwii_friend_info = $2 WHERE profile_id = $1` CountTotalUsersQuery = `SELECT COUNT(DISTINCT csnum) FROM users` + GetMKWVRBRQuery = `SELECT COALESCE(mariokartwii_vr, 0), COALESCE(mariokartwii_br, 0), (mariokartwii_vr IS NOT NULL AND mariokartwii_br IS NOT NULL) FROM users WHERE profile_id = $1` + GetMKWRawVRBRQuery = `SELECT mariokartwii_vr, mariokartwii_br FROM users WHERE profile_id = $1` + UpdateMKWVRBRQuery = `UPDATE users SET mariokartwii_vr = $2, mariokartwii_br = $3 WHERE profile_id = $1` ) type LinkStage byte @@ -69,6 +72,8 @@ type User struct { BanReasonHidden string BanIssued *time.Time BanExpires *time.Time + VR *int32 + BR *int32 } var ( @@ -298,6 +303,36 @@ func UpdateMKWFriendInfo(pool *pgxpool.Pool, ctx context.Context, profileId uint } } +func GetMKWVRBR(pool *pgxpool.Pool, ctx context.Context, profileId uint32) (int32, int32, bool) { + var vr int32 + var br int32 + var found bool + + err := pool.QueryRow(ctx, GetMKWVRBRQuery, profileId).Scan(&vr, &br, &found) + if err != nil { + return 0, 0, false + } + + return vr, br, found +} + +func GetMKWRawVRBR(pool *pgxpool.Pool, ctx context.Context, profileId uint32) (*int32, *int32, error) { + var vr *int32 + var br *int32 + + err := pool.QueryRow(ctx, GetMKWRawVRBRQuery, profileId).Scan(&vr, &br) + if err != nil { + return nil, nil, err + } + + return vr, br, nil +} + +func UpdateMKWVRBR(pool *pgxpool.Pool, ctx context.Context, profileId uint32, vr int32, br int32) error { + _, err := pool.Exec(ctx, UpdateMKWVRBRQuery, profileId, vr, br) + return err +} + // ScanUsers takes a query returning pids and collect the matching users func ScanUsers(pool *pgxpool.Pool, ctx context.Context, query string) ([]User, error) { logging.Info("QUERY", "Executing query", aurora.Cyan(query)) diff --git a/gpcm/report.go b/gpcm/report.go index 569fcf2..c3c568f 100644 --- a/gpcm/report.go +++ b/gpcm/report.go @@ -2,13 +2,48 @@ package gpcm import ( "strconv" + "strings" "wwfc/common" + "wwfc/database" "wwfc/logging" "wwfc/qr2" "github.com/logrusorgru/aurora/v3" ) +func parseMKWVRBRRecord(value string) (int32, int32, bool) { + parts := strings.Split(value, "|") + var vr int64 = -1 + var br int64 = -1 + + for _, part := range parts { + keyValue := strings.SplitN(part, "=", 2) + if len(keyValue) != 2 { + return 0, 0, false + } + + parsed, err := strconv.ParseInt(keyValue[1], 10, 32) + if err != nil || parsed < 1 || parsed > 1000000 { + return 0, 0, false + } + + switch keyValue[0] { + case "vr": + vr = parsed + case "br": + br = parsed + default: + return 0, 0, false + } + } + + if vr < 0 || br < 0 { + return 0, 0, false + } + + return int32(vr), int32(br), true +} + func (g *GameSpySession) handleWWFCReport(command common.GameSpyCommand) { for key, value := range command.OtherValues { logging.Info(g.ModuleName, "WiiLink Report:", aurora.Yellow(key)) @@ -87,6 +122,23 @@ func (g *GameSpySession) handleWWFCReport(command common.GameSpyCommand) { } qr2.ProcessMKWRaceResult(g.User.ProfileId, value) + + case "wl:mkw_vrbr": + if g.GameName != "mariokartwii" { + logging.Warn(g.ModuleName, "Ignoring", keyColored, "from wrong game") + continue + } + + vr, br, ok := parseMKWVRBRRecord(value) + if !ok { + logging.Error(g.ModuleName, "Invalid", keyColored, "record:", aurora.Cyan(value)) + continue + } + + err := database.UpdateMKWVRBR(pool, ctx, g.User.ProfileId, vr, br) + if err != nil { + logging.Error(g.ModuleName, "Failed to persist", keyColored, "for", aurora.Cyan(g.User.ProfileId), ":", err) + } } } } diff --git a/schema.sql b/schema.sql index ca9815e..14616d8 100644 --- a/schema.sql +++ b/schema.sql @@ -34,7 +34,9 @@ CREATE TABLE IF NOT EXISTS public.users ( unique_nick character varying NOT NULL, firstname character varying, lastname character varying DEFAULT ''::character varying, - mariokartwii_friend_info character varying + mariokartwii_friend_info character varying, + mariokartwii_vr integer, + mariokartwii_br integer ); @@ -50,7 +52,9 @@ ALTER TABLE ONLY public.users ADD IF NOT EXISTS ban_tos boolean, ADD IF NOT EXISTS open_host boolean DEFAULT false, ADD IF NOT EXISTS csnum character varying[], - ADD IF NOT EXISTS discord_id character varying; + ADD IF NOT EXISTS discord_id character varying, + ADD IF NOT EXISTS mariokartwii_vr integer, + ADD IF NOT EXISTS mariokartwii_br integer; -- -- Change ng_device_id from bigint to bigint[]