From 74b92f675bcd37699d9976a345ccc1fe36d8186f Mon Sep 17 00:00:00 2001 From: lixiangwuxian Date: Sat, 10 May 2025 14:33:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=9F=A5bl=E5=9B=BE?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E4=BB=A5=E8=8E=B7=E5=8F=96=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E5=88=86=E6=95=B0=E6=88=AA=E5=9B=BE=EF=BC=8C=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=A4=B4=E5=83=8F=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E4=BD=BF=E7=94=A8=E6=96=B0=E7=9A=84=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E7=BC=A9=E6=94=BE=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 7 +- go.sum | 14 ++-- handler/beatleader/beatleader.go | 51 +++++++++++++- handler/scoresaber/score.go | 2 +- service/beatleader/model.go | 116 ++++++++++++++++++++++++++++++- service/scoresaber/model.go | 4 +- util/picture.go | 37 +++++++++- 7 files changed, 217 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index d500cd7..214d58b 100644 --- a/go.mod +++ b/go.mod @@ -22,12 +22,13 @@ require ( github.com/valyala/fasthttp v1.60.0 github.com/yuin/goldmark v1.7.8 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c - golang.org/x/image v0.21.0 + golang.org/x/image v0.27.0 golang.org/x/net v0.38.0 gopkg.in/yaml.v3 v3.0.1 ) require ( + git.lxtend.com/lixiangwuxian/imagedd v0.0.0-20250510061940-c492839691e4 // indirect github.com/Microsoft/go-winio v0.4.14 // indirect github.com/andybalholm/brotli v1.1.1 // indirect github.com/bytedance/sonic v1.11.6 // indirect @@ -72,6 +73,8 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/smarty/assertions v1.15.0 // indirect + github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect + github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect @@ -84,7 +87,7 @@ require ( golang.org/x/arch v0.11.0 // indirect golang.org/x/crypto v0.36.0 // indirect golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/text v0.25.0 // indirect golang.org/x/time v0.7.0 // indirect google.golang.org/protobuf v1.35.1 // indirect gotest.tools/v3 v3.5.1 // indirect diff --git a/go.sum b/go.sum index c856278..bfa84ee 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +git.lxtend.com/lixiangwuxian/imagedd v0.0.0-20250510061940-c492839691e4 h1:yDPaEFsQ7zj9NqLhl2iOY2bswn8+oNcGTl/1geJgeAs= +git.lxtend.com/lixiangwuxian/imagedd v0.0.0-20250510061940-c492839691e4/go.mod h1:luas4p32Wtsywcz+8HsxIB3gf65FDDBa+3XYhm0S2b8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= @@ -159,6 +161,10 @@ github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGB github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= +github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE= +github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= +github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ= +github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -213,8 +219,8 @@ golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= -golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= -golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= +golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= +golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -238,8 +244,8 @@ golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/handler/beatleader/beatleader.go b/handler/beatleader/beatleader.go index f708099..6f4dbd2 100644 --- a/handler/beatleader/beatleader.go +++ b/handler/beatleader/beatleader.go @@ -20,6 +20,8 @@ import ( func init() { handler.RegisterHandler("查bl", getMyBL, constants.LEVEL_USER) handler.RegisterHelpInform("查bl", "beatleader", "查bl 查看您的最新分数") + handler.RegisterHandler("查bl图", getMyBLPic, constants.LEVEL_USER) + // handler.RegisterHelpInform("查bl图", "beatleader", "查bl图 查看您的最新分数截图") handler.RegisterHandler("绑定bl", bindBL, constants.LEVEL_USER) handler.RegisterHelpInform("绑定bl", "beatleader", "绑定bl 绑定您的beatleader账号") handler.RegisterHandler("解绑bl", unbindBL, constants.LEVEL_USER) @@ -189,6 +191,7 @@ func getMyBL(msg model.Message) (reply *model.Reply) { if err != nil { resultStr = "获取您的分数时出现问题,请稍后重试。" } + if lastData != nil { resultStr = data.LastDiffToString(*lastData) } else { @@ -201,6 +204,52 @@ func getMyBL(msg model.Message) (reply *model.Reply) { FromMsg: msg, } } + +func getMyBLPic(msg model.Message) (reply *model.Reply) { + var ( + resultStr string + err error + maxRetries = 3 // 最大重试次数 + attempts = 0 + ) + + userIdStr := strconv.Itoa(int(msg.UserId)) + var data *beatleader.PlayerDataLite + var lastData *beatleader.PlayerDataLite + for attempts < maxRetries { + data, lastData, err = beatleader.BLQuery.GetScore(userIdStr) + if err == nil { + break // 成功时退出循环 + } + attempts++ + log.Printf("获取分数时出错,第 %d 次重试: %v", attempts, err) + } + + // 如果所有尝试都失败,返回适当的错误消息 + if err != nil { + resultStr = "获取您的分数时出现问题,请稍后重试。" + } + + if lastData != nil { + resultStr = data.LastDiffToImage(*lastData) + } else { + resultStr = data.LastDiffToImage(*data) + } + + imageMsg := message.ImageMessage{ + Type: "image", + Data: message.ImageMessageData{ + File: resultStr, + }, + } + + return &model.Reply{ + ReplyMsg: imageMsg.ToCQString(), + ReferOriginMsg: true, + FromMsg: msg, + } +} + func bindBL(msg model.Message) (reply *model.Reply) { if len(msg.RawMsg) <= len("绑定bl ") { return &model.Reply{ @@ -264,7 +313,7 @@ func getMyRecentScore(msg model.Message) (reply *model.Reply) { log.Printf("下载图片失败: %v", err) return } - newPath, err := util.ResizeImageByMaxHeight(filePath, 20) + newPath, err := util.ResizeImageByMaxHeight2File(filePath, 20) if err != nil { log.Printf("缩放图片失败: %v", err) } diff --git a/handler/scoresaber/score.go b/handler/scoresaber/score.go index 1488142..1adba06 100644 --- a/handler/scoresaber/score.go +++ b/handler/scoresaber/score.go @@ -270,7 +270,7 @@ func getMyRecentScore(msg model.Message) (reply *model.Reply) { log.Printf("下载图片失败: %v", err) return } - newPath, err := util.ResizeImageByMaxHeight(filePath, 20) + newPath, err := util.ResizeImageByMaxHeight2File(filePath, 20) if err != nil { log.Printf("缩放图片失败: %v", err) } diff --git a/service/beatleader/model.go b/service/beatleader/model.go index 1d4d7fd..58e0c97 100644 --- a/service/beatleader/model.go +++ b/service/beatleader/model.go @@ -2,11 +2,16 @@ package beatleader import ( "fmt" + "image" + "image/color" + "image/draw" "log" "os" "strings" "time" + "git.lxtend.com/lixiangwuxian/imagedd/font2img" + "git.lxtend.com/lixiangwuxian/imagedd/sprite" "git.lxtend.com/qqbot/message" "git.lxtend.com/qqbot/util" ) @@ -415,7 +420,7 @@ func (p PlayerData) ToString() string { log.Default().Printf("下载头像失败,url:%s,err:%v", p.Avatar, err) } defer os.Remove(filePath) - outFile, err := util.ResizeImageByMaxHeight(filePath, 20) + outFile, err := util.ResizeImageByMaxHeight2File(filePath, 20) if err != nil { log.Default().Printf("缩放头像失败,url:%s,err:%v", p.Avatar, err) } @@ -453,7 +458,7 @@ func (p PlayerDataLite) LastDiffToString(lastDayQueryData PlayerDataLite) string log.Default().Printf("下载头像失败,url:%s,err:%v", p.Avatar, err) } defer os.Remove(filePath) - outFile, err := util.ResizeImageByMaxHeight(filePath, 20) + outFile, err := util.ResizeImageByMaxHeight2File(filePath, 20) if err != nil { log.Default().Printf("缩放头像失败,url:%s,err:%v", p.Avatar, err) } @@ -519,6 +524,113 @@ func (p PlayerDataLite) LastDiffToString(lastDayQueryData PlayerDataLite) string return sb.String() } +func (p PlayerDataLite) LastDiffToImage(lastDayQueryData PlayerDataLite) string { + filePath, err := util.DownloadFile(p.Avatar, "/tmp/qqbot", false) + if err != nil { + log.Default().Printf("下载头像失败,url:%s,err:%v", p.Avatar, err) + } + defer os.Remove(filePath) + // outFile, err := util.ResizeImageByMaxHeight2File(filePath, 20) + // if err != nil { + // log.Default().Printf("缩放头像失败,url:%s,err:%v", p.Avatar, err) + // } + // picMsg := message.ImageMessage{ + // Type: message.TypeImage, + // Data: message.ImageMessageData{ + // File: outFile, + // }, + // } + + baseboard := sprite.NewNamedSpriteBoard() + background := image.NewRGBA(image.Rect(0, 0, 500, 1000)) + draw.Draw(background, background.Bounds(), image.White, image.Point{}, draw.Src) + backgroundSpirit := sprite.Sprite{ + Name: "background", + Image: background, + Index: 0, + } + baseboard.AddSprite(&backgroundSpirit) + avatar, err := util.ResizeImageByMaxHeight2Image(filePath, 20) + if err != nil { + log.Default().Printf("缩放头像失败,url:%s,err:%v", p.Avatar, err) + } + avatarSpirit := sprite.Sprite{ + Name: "avatar", + Image: avatar, + Index: 1, + } + baseboard.AddSprite(&avatarSpirit) + + var sb strings.Builder + sb.WriteString(fmt.Sprintf("玩家 %s\n", p.Name)) + sb.WriteString(fmt.Sprintf("区域 %s\n", p.Country)) + + // PP值 + ppDiff := p.PP - lastDayQueryData.PP + if ppDiff == 0 { + sb.WriteString(fmt.Sprintf("PP %.1f\n", p.PP)) + } else { + sb.WriteString(fmt.Sprintf("PP %.1f(%+.1f)\n", p.PP, ppDiff)) + } // 全球排名 + rankDiff := lastDayQueryData.Rank - p.Rank + if rankDiff == 0 { + sb.WriteString(fmt.Sprintf("全球排名 %d\n", p.Rank)) + } else { + sb.WriteString(fmt.Sprintf("全球排名 %d(%+d)\n", p.Rank, rankDiff)) + } + + // 区域排名 + countryRankDiff := lastDayQueryData.CountryRank - p.CountryRank + if countryRankDiff == 0 { + sb.WriteString(fmt.Sprintf("区域排名 %d\n", p.CountryRank)) + } else { + sb.WriteString(fmt.Sprintf("区域排名 %d(%+d)\n", p.CountryRank, countryRankDiff)) + } + + // Ranked谱面均准 + accDiff := (p.AverageRankedAccuracy - lastDayQueryData.AverageRankedAccuracy) * 100 + if accDiff == 0 { + sb.WriteString(fmt.Sprintf("Ranked谱面均准 %.2f%%\n", p.AverageRankedAccuracy*100)) + } else { + sb.WriteString(fmt.Sprintf("Ranked谱面均准 %.2f%%(%+.2f%%)\n", p.AverageRankedAccuracy*100, accDiff)) + } + // 总游玩记数 + totalPlayDiff := p.TotalPlayCount - lastDayQueryData.TotalPlayCount + if totalPlayDiff == 0 { + sb.WriteString(fmt.Sprintf("总游玩记数 %d\n", p.TotalPlayCount)) + } else { + sb.WriteString(fmt.Sprintf("总游玩记数 %d(%+d)\n", p.TotalPlayCount, totalPlayDiff)) + } + + // Ranked谱面游玩记数 + rankedPlayDiff := p.RankedPlayCount - lastDayQueryData.RankedPlayCount + if rankedPlayDiff == 0 { + sb.WriteString(fmt.Sprintf("Ranked谱面游玩记数 %d\n", p.RankedPlayCount)) + } else { + sb.WriteString(fmt.Sprintf("Ranked谱面游玩记数 %d(%+d)\n", p.RankedPlayCount, rankedPlayDiff)) + } + // 回放被观看次数 + sb.WriteString(fmt.Sprintf("回放被观看次数 %d", p.ReplaysWatched)) + + text := sb.String() + textImg, err := font2img.RenderTextToTrimmedImage(nil, text, 12, color.Black, 0, 0) + if err != nil { + log.Default().Printf("渲染文字失败,err:%v", err) + } + textSpirit := sprite.Sprite{ + Name: "text", + Image: textImg, + Index: 2, + Position: image.Point{X: 23, Y: 23}, + } + baseboard.AddSprite(&textSpirit) + + if err := baseboard.SaveToPng(util.GenTempFilePath("cbl.png")); err != nil { + log.Default().Printf("保存图片失败,err:%v", err) + } + return util.GenTempFilePath("cbl.png") +} + func GetControllerStr(controller int) string { switch controller { case 1: diff --git a/service/scoresaber/model.go b/service/scoresaber/model.go index bf5df24..47dfce8 100644 --- a/service/scoresaber/model.go +++ b/service/scoresaber/model.go @@ -229,7 +229,7 @@ func (p PlayerData) ToString() string { log.Default().Printf("下载头像失败,url:%s,err:%v", p.ProfilePicture, err) } defer os.Remove(filePath) - outFile, err := util.ResizeImageByMaxHeight(filePath, 20) + outFile, err := util.ResizeImageByMaxHeight2File(filePath, 20) if err != nil { log.Default().Printf("缩放头像失败,url:%s,err:%v", p.ProfilePicture, err) } @@ -289,7 +289,7 @@ func (p PlayerData) LastDiffToString(lastDayQueryData PlayerDataLite) string { log.Default().Printf("下载头像失败,url:%s,err:%v", p.ProfilePicture, err) } defer os.Remove(filePath) - outFile, err := util.ResizeImageByMaxHeight(filePath, 20) + outFile, err := util.ResizeImageByMaxHeight2File(filePath, 20) if err != nil { log.Default().Printf("缩放头像失败,url:%s,err:%v", p.ProfilePicture, err) } diff --git a/util/picture.go b/util/picture.go index 75498be..e362ba0 100644 --- a/util/picture.go +++ b/util/picture.go @@ -110,8 +110,8 @@ func ResizeImageByMaxWidth(imagePath string, maxWidth uint) (outputPath string, } } -// ResizeImageByMaxHeight 按最大高度缩放图片,保持宽高比 -func ResizeImageByMaxHeight(imagePath string, maxHeight uint) (outputPath string, err error) { +// ResizeImageByMaxHeight2File 按最大高度缩放图片,保持宽高比 +func ResizeImageByMaxHeight2File(imagePath string, maxHeight uint) (outputPath string, err error) { // 打开源图片文件 file, err := os.Open(imagePath) if err != nil { @@ -160,6 +160,39 @@ func ResizeImageByMaxHeight(imagePath string, maxHeight uint) (outputPath string } } +func ResizeImageByMaxHeight2Image(imagePath string, maxHeight uint) (output image.Image, err error) { + // 打开源图片文件 + file, err := os.Open(imagePath) + if err != nil { + return nil, err + } + defer file.Close() + + // 解码图片 + var img image.Image + var decodeErr error + + ext := strings.ToLower(filepath.Ext(imagePath)) + switch ext { + case ".jpg", ".jpeg": + img, decodeErr = jpeg.Decode(file) + case ".png": + img, decodeErr = png.Decode(file) + default: + return nil, errors.New("unsupported image format") + } + + if decodeErr != nil { + return nil, decodeErr + } + + // 计算缩放后的尺寸,保持宽高比 + // 传入0作为宽度,resize包会自动计算等比例的宽度 + resized := resize.Resize(0, maxHeight, img, resize.Lanczos3) + + return resized, nil +} + func GetResizedIamgePathByOrgPath(orgPath string) string { ext := strings.ToLower(filepath.Ext(orgPath)) return strings.TrimSuffix(orgPath, ext) + "_resized" + ext