From 6454b55a9058ce104d6da6df3cd89dd8becc6a0e Mon Sep 17 00:00:00 2001 From: lixiangwuxian Date: Mon, 31 Mar 2025 02:28:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9C=A8=20scoresaber=20=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E4=B8=AD=E6=B7=BB=E5=8A=A0=20ss+n=20=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=EF=BC=8C=E5=85=81=E8=AE=B8=E7=94=A8=E6=88=B7=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E6=8F=90=E5=8D=87=E6=8E=92=E5=90=8D=E6=89=80=E9=9C=80?= =?UTF-8?q?=E7=9A=84=E5=88=86=E6=95=B0=EF=BC=8C=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=20FetchPlayerData=20=E5=87=BD=E6=95=B0=E7=9A=84=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E7=B1=BB=E5=9E=8B=E4=BB=A5=E6=8F=90=E9=AB=98=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=A4=84=E7=90=86=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handler/scoresaber/score.go | 72 +++++++++++++++- service/scoresaber/bind_ss.go | 15 ++-- service/scoresaber/user_info.go | 145 ++++++++++++++++++++++++++++++-- 3 files changed, 219 insertions(+), 13 deletions(-) diff --git a/handler/scoresaber/score.go b/handler/scoresaber/score.go index 18b5867..0427160 100644 --- a/handler/scoresaber/score.go +++ b/handler/scoresaber/score.go @@ -1,6 +1,7 @@ package scoresaber import ( + "fmt" "log" "os" "strconv" @@ -25,8 +26,75 @@ func init() { handler.RegisterHandler("最新ss", getMyRecentScore, constants.LEVEL_USER) handler.RegisterHelpInform("最新ss", "scoresaber", "查看您的最新游戏记录") handler.RegisterHandler("截ss", screenshotSS, constants.LEVEL_USER) - handler.RegisterHelpInform("截ss", "scoresaber", "scoresaber主页截图") - handler.RegisterHandler("jss", screenshotSS, constants.LEVEL_USER) + handler.RegisterHelpInform("ss+n", "scoresaber", "区排名升高n位还需要打出多少pp") + handler.RegisterHandler("ss+n", ssPlusN, constants.LEVEL_USER) + // handler.RegisterHelpInform("截ss", "scoresaber", "scoresaber主页截图") + // handler.RegisterHandler("jss", screenshotSS, constants.LEVEL_USER) +} + +func ssPlusN(msg model.Message) (reply model.Reply) { + var ( + resultStr strings.Builder + err error + maxRetries = 5 // 最大重试次数 + attempts = 0 + ) + var N int + if len(msg.RawMsg) > len("ss+") { + N, err = strconv.Atoi(msg.RawMsg[len("ss+"):]) + if err != nil { + return model.Reply{ + ReplyMsg: "请输入一个整数", + ReferOriginMsg: true, + FromMsg: msg, + } + } + } + // 获取当前用户在区中的排名 + userIdStr := strconv.Itoa(int(msg.UserId)) + var userInfo scoresaber.PlayerData + for attempts < maxRetries { + err = nil + userInfo, err = scoresaber.FetchPlayerData(userIdStr) + if err != nil { + break + } + attempts++ + } + if err != nil { + return model.Reply{ + ReplyMsg: "获取您的分数时出现问题,请稍后重试。" + err.Error(), + ReferOriginMsg: true, + FromMsg: msg, + } + } + resultStr.WriteString(fmt.Sprintf("您当前的区排名为:%d\n", userInfo.CountryRank)) + // 获取当前用户所在区对应+N位的玩家列表 + leaderboard, err := scoresaber.FetchCountryLeaderboard(userInfo.Country, userInfo.CountryRank-N, userInfo.ID) + if err != nil { + return model.Reply{ + ReplyMsg: "获取您的分数时出现问题,请稍后重试。" + err.Error(), + ReferOriginMsg: true, + FromMsg: msg, + } + } + if userInfo.CountryRank-N < 0 { + resultStr.WriteString(fmt.Sprintf("注意:你最多只需要提升%d名就是%s区Top1了\n", N, userInfo.Country)) + } + //寻找leaderboard中排名为userInfo.CountryRank-N的玩家 + var targetPlayer scoresaber.PlayerData + for _, player := range leaderboard.Players { + if player.CountryRank == userInfo.CountryRank-N { + targetPlayer = player + break + } + } + resultStr.WriteString(fmt.Sprintf("您只需要再打出%.2fpp就能达到%s区第%d名了", targetPlayer.PP-userInfo.PP, userInfo.Country, targetPlayer.CountryRank)) + return model.Reply{ + ReplyMsg: resultStr.String(), + ReferOriginMsg: true, + FromMsg: msg, + } } func getSSProfile(msg model.Message) (reply model.Reply) { diff --git a/service/scoresaber/bind_ss.go b/service/scoresaber/bind_ss.go index ad6f2dc..e5b7c3d 100644 --- a/service/scoresaber/bind_ss.go +++ b/service/scoresaber/bind_ss.go @@ -90,7 +90,7 @@ func (ss *ssQuery) BindSS(qqId string, ssId string) (reply string) { return "ssId格式错误,应当为一串数字(是您的scoresaber主页链接中的末尾数字部分,一般和您的steamID相同)" } data, err := FetchPlayerData(ssId) - if data == nil { + if data.ID == "" { if err != nil { return "请求出错,报错如下,如果确定命令没问题可以重新试试:" + err.Error() } @@ -106,7 +106,7 @@ func (ss *ssQuery) BindSS(qqId string, ssId string) (reply string) { } rows.Close() // 获取当前绑定账号的信息 - if currentData, err := FetchPlayerData(currentSsId); err == nil && currentData != nil { + if currentData, err := FetchPlayerData(currentSsId); err == nil && currentData.ID != "" { return fmt.Sprintf("您已绑定至ss账号:%s,请先输入\"解绑ss\"解绑", currentData.Name) } return "您已绑定过ss账号,请先输入\"解绑ss\"解绑" @@ -167,10 +167,12 @@ func (ss *ssQuery) GetScore(ssId string) (reply string, err error) { // 查询玩家数据 data, err := FetchPlayerData(ssId) - if data == nil { + if err != nil { return "查询出错,报错如下" + err.Error(), errors.New("查询出错,报错如下" + err.Error()) } - + if data.ID == "" { + return "未找到玩家,请检查ID后重试", errors.New("未找到玩家,请检查ID后重试") + } // 构建 PlayerDataLite 结构体 dataLite := PlayerDataLite{ ID: data.ID, @@ -232,9 +234,12 @@ func (ss *ssQuery) GetScore(ssId string) (reply string, err error) { func (ss *ssQuery) GetScoreWithoutUpdate(ssId string) (reply string, err error) { // 查询玩家数据 data, err := FetchPlayerData(ssId) - if data == nil { + if err != nil { return "查询出错,报错如下" + err.Error(), errors.New("查询出错,报错如下" + err.Error()) } + if data.ID == "" { + return "未找到玩家,请检查ID后重试", errors.New("未找到玩家,请检查ID后重试") + } // 返回当前数据的字符串表示 return data.ToString(), nil } diff --git a/service/scoresaber/user_info.go b/service/scoresaber/user_info.go index 51e2b35..e78235a 100644 --- a/service/scoresaber/user_info.go +++ b/service/scoresaber/user_info.go @@ -9,13 +9,13 @@ import ( ) // fetchPlayerData 函数请求 Scoresaber API,并解析完整的玩家信息 -func FetchPlayerData(ssID string) (*PlayerData, error) { +func FetchPlayerData(ssID string) (PlayerData, error) { url := fmt.Sprintf("https://scoresaber.com/api/player/%s/full", ssID) // 创建请求 req, err := http.NewRequest("GET", url, nil) if err != nil { - return nil, err + return PlayerData{}, err } // 设置请求头 @@ -41,7 +41,7 @@ func FetchPlayerData(ssID string) (*PlayerData, error) { resp, err = client.Do(req) } if err != nil { - return nil, err + return PlayerData{}, err } defer resp.Body.Close() @@ -50,7 +50,7 @@ func FetchPlayerData(ssID string) (*PlayerData, error) { if resp.Header.Get("Content-Encoding") == "gzip" { reader, err = gzip.NewReader(resp.Body) if err != nil { - return nil, err + return PlayerData{}, err } defer reader.(*gzip.Reader).Close() } else { @@ -62,8 +62,141 @@ func FetchPlayerData(ssID string) (*PlayerData, error) { err = json.NewDecoder(reader).Decode(&playerData) if err != nil { // log.Printf("got body %v", reader.) - return nil, err + return PlayerData{}, err } - return &playerData, nil + return playerData, nil +} + +// { +// "players": [ +// { +// "id": "76561199001767132", +// "name": "ViSi", +// "profilePicture": "https://cdn.scoresaber.com/avatars/76561199001767132.jpg", +// "bio": "

a girl using index cons

\n

7/1/2022 i beat ov sacrament

\n

7/28/2022 i hit 1k hours, i suppose im the worst 1k hours player atm

\n

BiliBili

\n

YouTube

\n

Twitch

\n

Twitter

", +// "country": "CN", +// "pp": 15113.41, +// "rank": 120, +// "countryRank": 1, +// "role": null, +// "badges": null, +// "histories": "103,103,104,104,104,104,104,105,104,104,104,104,105,105,105,106,107,107,107,108,111,112,113,113,113,113,113,113,113,115,114,115,115,114,114,114,118,118,118,118,119,119,119,119,119,119,121,121,121", +// "permissions": 0, +// "banned": false, +// "inactive": false, +// "scoreStats": { +// "totalScore": 4525384588, +// "totalRankedScore": 1648718461, +// "averageRankedAccuracy": 96.73215, +// "totalPlayCount": 4001, +// "rankedPlayCount": 1351, +// "replaysWatched": 482 +// }, +// "firstSeen": "2021-04-15T06:56:45.000Z" +// }, +// ... +// { +// "id": "76561198796873048", +// "name": "Jia_Yue 甩尾の嘉玥", +// "profilePicture": "https://cdn.scoresaber.com/avatars/76561198796873048.jpg", +// "bio": null, +// "country": "CN", +// "pp": 9984.614, +// "rank": 2127, +// "countryRank": 50, +// "role": null, +// "badges": null, +// "histories": "2202,2170,2176,2182,2184,2183,2183,2224,2196,2197,2192,2210,2206,2210,2197,2195,2190,2186,2187,2187,2190,2191,2187,2190,2186,2186,2184,2183,2178,2202,2167,2163,2163,2151,2150,2145,2164,2132,2135,2130,2126,2128,2124,2120,2122,2125,2125,2129,2125", +// "permissions": 0, +// "banned": false, +// "inactive": false, +// "scoreStats": { +// "totalScore": 2377399561, +// "totalRankedScore": 1304174761, +// "averageRankedAccuracy": 82.02609, +// "totalPlayCount": 3001, +// "rankedPlayCount": 1410, +// "replaysWatched": 19 +// }, +// "firstSeen": "2022-03-24T06:11:27.000Z" +// } +// ], +// "metadata": { +// "total": 1927, +// "page": 1, +// "itemsPerPage": 50 +// } +// } + +type LeaderboardData struct { + Players []PlayerData `json:"players"` + Metadata struct { + Total int `json:"total"` + Page int `json:"page"` + ItemsPerPage int `json:"itemsPerPage"` + } `json:"metadata"` +} + +func FetchCountryLeaderboard(country string, offset int, ssID string) (LeaderboardData, error) { + // 根据偏移量计算页数 + if offset < 0 { + offset = 0 + } + page := offset/50 + 1 + url := fmt.Sprintf("https://scoresaber.com/api/players?countries=%s&page=%d", country, page) + + // 创建请求 + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return LeaderboardData{}, err + } + + // 设置请求头 + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0") + req.Header.Set("Accept", "application/json, text/plain, */*") + req.Header.Set("Accept-Language", "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2") + req.Header.Set("Accept-Encoding", "gzip") + req.Header.Set("DNT", "1") + req.Header.Set("Connection", "keep-alive") + req.Header.Set("Referer", fmt.Sprintf("https://scoresaber.com/u/%s", ssID)) + req.Header.Set("Sec-Fetch-Dest", "empty") + req.Header.Set("Sec-Fetch-Mode", "cors") + req.Header.Set("Sec-Fetch-Site", "same-origin") + req.Header.Set("Sec-GPC", "1") + req.Header.Set("Pragma", "no-cache") + req.Header.Set("Cache-Control", "no-cache") + req.Header.Set("TE", "trailers") + + // 发送请求,失败则重试至多3次 + client := &http.Client{} + resp, err := client.Do(req) + for i := 0; i < 3 && err != nil; i++ { + resp, err = client.Do(req) + } + if err != nil { + return LeaderboardData{}, err + } + defer resp.Body.Close() + + // 处理压缩响应 + var reader io.Reader + if resp.Header.Get("Content-Encoding") == "gzip" { + reader, err = gzip.NewReader(resp.Body) + if err != nil { + return LeaderboardData{}, err + } + defer reader.(*gzip.Reader).Close() + } else { + reader = resp.Body + } + + // 解析响应体 + var leaderboardData LeaderboardData + err = json.NewDecoder(reader).Decode(&leaderboardData) + if err != nil { + return LeaderboardData{}, err + } + + return leaderboardData, nil }