feat: 添加查bl图命令以获取最新分数截图,并优化头像处理逻辑,使用新的图片缩放方法

This commit is contained in:
lixiangwuxian 2025-05-10 14:33:53 +08:00
parent aa9994de17
commit 74b92f675b
7 changed files with 217 additions and 14 deletions

7
go.mod
View File

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

14
go.sum
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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