feat: 重构消息处理模块,引入统一的消息接口和类型安全的消息解析
This commit is contained in:
parent
e0637ab81f
commit
13ea5d7f98
@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"git.lxtend.com/qqbot/message"
|
||||||
"git.lxtend.com/qqbot/model"
|
"git.lxtend.com/qqbot/model"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
@ -31,7 +32,13 @@ func (am *actionManager) SendAction(action string) error {
|
|||||||
|
|
||||||
func (am *actionManager) SendMsg(reply model.Reply) error {
|
func (am *actionManager) SendMsg(reply model.Reply) error {
|
||||||
if reply.ReferOriginMsg {
|
if reply.ReferOriginMsg {
|
||||||
reply.ReplyMsg = fmt.Sprintf("%s%s", GenReplyCQ(reply.FromMsg.OriginMsgId, "", 0, 0, 0), reply.ReplyMsg)
|
replyMsg := message.ReplyMessage{
|
||||||
|
Type: "reply",
|
||||||
|
Data: message.ReplyData{
|
||||||
|
ID: int(reply.FromMsg.OriginMsgId),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
reply.ReplyMsg = fmt.Sprintf("%s%s", replyMsg.ToCQString(), reply.ReplyMsg)
|
||||||
}
|
}
|
||||||
sendPkg := model.GenSendPkg(reply.FromMsg.UserId, reply.FromMsg.GroupInfo.GroupId, reply.ReplyMsg, false)
|
sendPkg := model.GenSendPkg(reply.FromMsg.UserId, reply.FromMsg.GroupInfo.GroupId, reply.ReplyMsg, false)
|
||||||
sendPkgJson, err := json.Marshal(sendPkg)
|
sendPkgJson, err := json.Marshal(sendPkg)
|
||||||
|
1
go.mod
1
go.mod
@ -13,6 +13,7 @@ require (
|
|||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/jmoiron/sqlx v1.4.0
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
github.com/mattn/go-sqlite3 v1.14.23
|
github.com/mattn/go-sqlite3 v1.14.23
|
||||||
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||||
github.com/sashabaranov/go-openai v1.36.1
|
github.com/sashabaranov/go-openai v1.36.1
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
||||||
golang.org/x/image v0.21.0
|
golang.org/x/image v0.21.0
|
||||||
|
2
go.sum
2
go.sum
@ -123,6 +123,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
|||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||||
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"git.lxtend.com/qqbot/auth"
|
"git.lxtend.com/qqbot/auth"
|
||||||
"git.lxtend.com/qqbot/constants"
|
"git.lxtend.com/qqbot/constants"
|
||||||
"git.lxtend.com/qqbot/handler"
|
"git.lxtend.com/qqbot/handler"
|
||||||
|
"git.lxtend.com/qqbot/message"
|
||||||
"git.lxtend.com/qqbot/model"
|
"git.lxtend.com/qqbot/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -36,8 +37,9 @@ func setUserLevel(msg model.Message) (reply model.Reply) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
userText := tokens[1]
|
userText := tokens[1]
|
||||||
if userId, err := model.ParseAtMessage(userText); err == nil {
|
atMsg := message.AtMessage{}
|
||||||
userText = userId.Data.QQ
|
if err := atMsg.ParseMessage(userText); err == nil {
|
||||||
|
userText = atMsg.Data.QQ
|
||||||
}
|
}
|
||||||
log.Println(userText)
|
log.Println(userText)
|
||||||
user, err := strconv.Atoi(userText)
|
user, err := strconv.Atoi(userText)
|
||||||
|
@ -2,12 +2,16 @@ package beatleader
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"git.lxtend.com/qqbot/constants"
|
"git.lxtend.com/qqbot/constants"
|
||||||
"git.lxtend.com/qqbot/handler"
|
"git.lxtend.com/qqbot/handler"
|
||||||
|
"git.lxtend.com/qqbot/message"
|
||||||
"git.lxtend.com/qqbot/model"
|
"git.lxtend.com/qqbot/model"
|
||||||
"git.lxtend.com/qqbot/service/beatleader"
|
"git.lxtend.com/qqbot/service/beatleader"
|
||||||
|
"git.lxtend.com/qqbot/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -135,9 +139,31 @@ func getMyRecentScore(msg model.Message) (reply model.Reply) {
|
|||||||
FromMsg: msg,
|
FromMsg: msg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, v := range records {
|
// 单独线程下载封面图片
|
||||||
scoreMsg += v.ToString() + "\n\n"
|
coverImageMap := make(map[string]string)
|
||||||
userName = v.Name
|
wg := sync.WaitGroup{}
|
||||||
|
for _, record := range records {
|
||||||
|
coverImageMap[record.SongHash] = record.CoverImage
|
||||||
|
wg.Add(1)
|
||||||
|
go func(songHash string) {
|
||||||
|
defer wg.Done()
|
||||||
|
//文件存在则跳过
|
||||||
|
if _, err := os.Stat(util.GetResizedIamgePathByOrgPath(util.GenTempFilePath(songHash + ".png"))); err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
util.DownloadFile(coverImageMap[songHash], util.GenTempFilePath(songHash+".png"))
|
||||||
|
newPath, err := util.ResizeImageByMaxHeight(util.GenTempFilePath(songHash+".png"), 20)
|
||||||
|
os.Remove(util.GenTempFilePath(songHash + ".png"))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("缩放图片失败: %v", err)
|
||||||
|
}
|
||||||
|
coverImageMap[songHash] = newPath
|
||||||
|
}(record.SongHash)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
for _, record := range records {
|
||||||
|
scoreMsg += record.ToString() + "\n\n"
|
||||||
|
userName = record.Name
|
||||||
recordCount++
|
recordCount++
|
||||||
}
|
}
|
||||||
if len(scoreMsg) > 0 {
|
if len(scoreMsg) > 0 {
|
||||||
@ -157,8 +183,14 @@ func getMyRecentScore(msg model.Message) (reply model.Reply) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func screenShotBL(msg model.Message) (reply model.Reply) {
|
func screenShotBL(msg model.Message) (reply model.Reply) {
|
||||||
|
imageMsg := message.ImageMessage{
|
||||||
|
Type: "image",
|
||||||
|
Data: message.ImageMessageData{
|
||||||
|
File: "file:///tmp/qqbot/" + beatleader.GetBLPicture(strconv.Itoa(int(msg.UserId))),
|
||||||
|
},
|
||||||
|
}
|
||||||
return model.Reply{
|
return model.Reply{
|
||||||
ReplyMsg: "[CQ:image,file=file:///tmp/qqbot/" + beatleader.GetBLPicture(strconv.Itoa(int(msg.UserId))) + "]",
|
ReplyMsg: imageMsg.ToCQString(),
|
||||||
ReferOriginMsg: true,
|
ReferOriginMsg: true,
|
||||||
FromMsg: msg,
|
FromMsg: msg,
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
package drawback
|
package drawback
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
|
|
||||||
"git.lxtend.com/qqbot/action"
|
"git.lxtend.com/qqbot/action"
|
||||||
"git.lxtend.com/qqbot/constants"
|
"git.lxtend.com/qqbot/constants"
|
||||||
"git.lxtend.com/qqbot/handler"
|
"git.lxtend.com/qqbot/handler"
|
||||||
|
"git.lxtend.com/qqbot/message"
|
||||||
"git.lxtend.com/qqbot/model"
|
"git.lxtend.com/qqbot/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -15,9 +14,8 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func drawback(msg model.Message) model.Reply {
|
func drawback(msg model.Message) model.Reply {
|
||||||
msgIdToDrawback, err := model.ParseReplyData(msg.RawMsg)
|
msgIdToDrawback := message.ReplyMessage{}
|
||||||
log.Printf("Drawback message %d", msgIdToDrawback.Data.ID)
|
if err := msgIdToDrawback.ParseMessage(msg.RawMsg); err != nil {
|
||||||
if err != nil {
|
|
||||||
return model.Reply{
|
return model.Reply{
|
||||||
ReplyMsg: "",
|
ReplyMsg: "",
|
||||||
ReferOriginMsg: false,
|
ReferOriginMsg: false,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package getweb
|
package getweb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.lxtend.com/qqbot/message"
|
||||||
"git.lxtend.com/qqbot/model"
|
"git.lxtend.com/qqbot/model"
|
||||||
"git.lxtend.com/qqbot/util"
|
"git.lxtend.com/qqbot/util"
|
||||||
)
|
)
|
||||||
@ -24,8 +25,14 @@ func getweb(msg model.Message) (reply model.Reply) {
|
|||||||
FromMsg: msg,
|
FromMsg: msg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
imageMsg := message.ImageMessage{
|
||||||
|
Type: "image",
|
||||||
|
Data: message.ImageMessageData{
|
||||||
|
File: "file:///tmp/qqbot/getweb/url.png",
|
||||||
|
},
|
||||||
|
}
|
||||||
return model.Reply{
|
return model.Reply{
|
||||||
ReplyMsg: "[CQ:image,file=file:///tmp/qqbot/getweb/url.png]",
|
ReplyMsg: imageMsg.ToCQString(),
|
||||||
ReferOriginMsg: true,
|
ReferOriginMsg: true,
|
||||||
FromMsg: msg,
|
FromMsg: msg,
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"git.lxtend.com/qqbot/constants"
|
"git.lxtend.com/qqbot/constants"
|
||||||
"git.lxtend.com/qqbot/handler"
|
"git.lxtend.com/qqbot/handler"
|
||||||
|
"git.lxtend.com/qqbot/message"
|
||||||
"git.lxtend.com/qqbot/model"
|
"git.lxtend.com/qqbot/model"
|
||||||
"git.lxtend.com/qqbot/service/xibao"
|
"git.lxtend.com/qqbot/service/xibao"
|
||||||
"git.lxtend.com/qqbot/util"
|
"git.lxtend.com/qqbot/util"
|
||||||
@ -33,8 +34,14 @@ func xiBao(msg model.Message) (reply model.Reply) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
xibao.GenerateCongratulationImage(tokens[1], "./resource/xibao_background.png", filePath, true)
|
xibao.GenerateCongratulationImage(tokens[1], "./resource/xibao_background.png", filePath, true)
|
||||||
|
imageMsg := message.ImageMessage{
|
||||||
|
Type: "image",
|
||||||
|
Data: message.ImageMessageData{
|
||||||
|
File: "file:///tmp/qqbot/" + fileName + ".png",
|
||||||
|
},
|
||||||
|
}
|
||||||
return model.Reply{
|
return model.Reply{
|
||||||
ReplyMsg: fmt.Sprintf("[CQ:image,file=file:///tmp/qqbot/%s]", fileName+".png"),
|
ReplyMsg: imageMsg.ToCQString(),
|
||||||
ReferOriginMsg: true,
|
ReferOriginMsg: true,
|
||||||
FromMsg: msg,
|
FromMsg: msg,
|
||||||
}
|
}
|
||||||
@ -45,8 +52,14 @@ func xiBaoTemp(msg model.Message) (reply model.Reply, isTrigger bool) {
|
|||||||
fileName := uuid.New().String()
|
fileName := uuid.New().String()
|
||||||
filePath := util.GenTempFilePath(fmt.Sprintf("%s.png", fileName))
|
filePath := util.GenTempFilePath(fmt.Sprintf("%s.png", fileName))
|
||||||
xibao.GenerateCongratulationImage(msg.RawMsg, "./resource/xibao_background.png", filePath, true)
|
xibao.GenerateCongratulationImage(msg.RawMsg, "./resource/xibao_background.png", filePath, true)
|
||||||
|
imageMsg := message.ImageMessage{
|
||||||
|
Type: "image",
|
||||||
|
Data: message.ImageMessageData{
|
||||||
|
File: "file:///tmp/qqbot/" + fileName + ".png",
|
||||||
|
},
|
||||||
|
}
|
||||||
return model.Reply{
|
return model.Reply{
|
||||||
ReplyMsg: fmt.Sprintf("[CQ:image,file=file:///tmp/qqbot/%s]", fileName+".png"),
|
ReplyMsg: imageMsg.ToCQString(),
|
||||||
ReferOriginMsg: true,
|
ReferOriginMsg: true,
|
||||||
FromMsg: msg,
|
FromMsg: msg,
|
||||||
}, true
|
}, true
|
||||||
@ -66,8 +79,14 @@ func beiBao(msg model.Message) (reply model.Reply) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
xibao.GenerateCongratulationImage(tokens[1], "./resource/beibao_background.png", filePath, false)
|
xibao.GenerateCongratulationImage(tokens[1], "./resource/beibao_background.png", filePath, false)
|
||||||
|
imageMsg := message.ImageMessage{
|
||||||
|
Type: "image",
|
||||||
|
Data: message.ImageMessageData{
|
||||||
|
File: "file:///tmp/qqbot/" + fileName + ".png",
|
||||||
|
},
|
||||||
|
}
|
||||||
return model.Reply{
|
return model.Reply{
|
||||||
ReplyMsg: fmt.Sprintf("[CQ:image,file=file:///tmp/qqbot/%s]", fileName+".png"),
|
ReplyMsg: imageMsg.ToCQString(),
|
||||||
ReferOriginMsg: true,
|
ReferOriginMsg: true,
|
||||||
FromMsg: msg,
|
FromMsg: msg,
|
||||||
}
|
}
|
||||||
@ -78,8 +97,14 @@ func beiBaoTemp(msg model.Message) (reply model.Reply, isTrigger bool) {
|
|||||||
fileName := uuid.New().String()
|
fileName := uuid.New().String()
|
||||||
filePath := util.GenTempFilePath(fmt.Sprintf("%s.png", fileName))
|
filePath := util.GenTempFilePath(fmt.Sprintf("%s.png", fileName))
|
||||||
xibao.GenerateCongratulationImage(msg.RawMsg, "./resource/beibao_background.png", filePath, false)
|
xibao.GenerateCongratulationImage(msg.RawMsg, "./resource/beibao_background.png", filePath, false)
|
||||||
|
imageMsg := message.ImageMessage{
|
||||||
|
Type: "image",
|
||||||
|
Data: message.ImageMessageData{
|
||||||
|
File: "file:///tmp/qqbot/" + fileName + ".png",
|
||||||
|
},
|
||||||
|
}
|
||||||
return model.Reply{
|
return model.Reply{
|
||||||
ReplyMsg: fmt.Sprintf("[CQ:image,file=file:///tmp/qqbot/%s]", fileName+".png"),
|
ReplyMsg: imageMsg.ToCQString(),
|
||||||
ReferOriginMsg: true,
|
ReferOriginMsg: true,
|
||||||
FromMsg: msg,
|
FromMsg: msg,
|
||||||
}, true
|
}, true
|
||||||
|
36
message/at.go
Normal file
36
message/at.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @某个QQ用户的结构体
|
||||||
|
type AtMessage struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Data AtData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// @消息数据结构体
|
||||||
|
type AtData struct {
|
||||||
|
QQ string `json:"qq"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *AtMessage) ToCQString() string {
|
||||||
|
return fmt.Sprintf(`\[CQ:at,qq=%s\]`, msg.Data.QQ)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *AtMessage) ParseMessage(data string) error {
|
||||||
|
// 使用正则表达式提取QQ号
|
||||||
|
re := regexp.MustCompile(`\[CQ:at,qq=(\d+)\]`)
|
||||||
|
matches := re.FindStringSubmatch(data)
|
||||||
|
if len(matches) < 2 {
|
||||||
|
return fmt.Errorf("数据格式不正确")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回解析后的结构体
|
||||||
|
msg.Data = AtData{
|
||||||
|
QQ: matches[1],
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
6
message/cq_message.go
Normal file
6
message/cq_message.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
type CQMessage interface {
|
||||||
|
ToCQString() string
|
||||||
|
ParseMessage(data string) error
|
||||||
|
}
|
69
message/image.go
Normal file
69
message/image.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ImageMessage struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Data ImageMessageData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageMessageData struct {
|
||||||
|
File string
|
||||||
|
SubType string
|
||||||
|
FileID string
|
||||||
|
URL string
|
||||||
|
FileSize int
|
||||||
|
FileUnique string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *ImageMessage) ToCQString() string {
|
||||||
|
// URL 转义
|
||||||
|
escapedURL := url.QueryEscape(msg.Data.URL)
|
||||||
|
|
||||||
|
// 构造 CQ:image 字符串
|
||||||
|
cqString := fmt.Sprintf("[CQ:image,file=%s,sub_type=%s,file_id=%s,url=%s,file_size=%d,file_unique=%s]",
|
||||||
|
msg.Data.File, msg.Data.SubType, msg.Data.FileID, escapedURL, msg.Data.FileSize, msg.Data.FileUnique)
|
||||||
|
|
||||||
|
return cqString
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *ImageMessage) ParseMessage(data string) error {
|
||||||
|
// 使用正则表达式提取各个字段
|
||||||
|
re := regexp.MustCompile(`\[CQ:image,file=(.*?),sub_type=(.*?),file_id=(.*?),url=(.*?),file_size=(\d+),file_unique=(.*?)\]`)
|
||||||
|
matches := re.FindStringSubmatch(data)
|
||||||
|
if len(matches) < 7 {
|
||||||
|
return fmt.Errorf("数据格式不正确")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换file_size为整数
|
||||||
|
fileSize, err := strconv.Atoi(matches[5])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("解析 file_size 出错: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理URL转义
|
||||||
|
decodedURL, err := url.QueryUnescape(matches[4])
|
||||||
|
|
||||||
|
decodedURL = strings.ReplaceAll(decodedURL, ",", ",")
|
||||||
|
decodedURL = strings.ReplaceAll(decodedURL, "[", "[")
|
||||||
|
decodedURL = strings.ReplaceAll(decodedURL, "]", "]")
|
||||||
|
decodedURL = strings.ReplaceAll(decodedURL, "&", "&")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("URL 转义失败: %v", err)
|
||||||
|
}
|
||||||
|
msg.Data = ImageMessageData{
|
||||||
|
File: matches[1],
|
||||||
|
SubType: matches[2],
|
||||||
|
FileID: matches[3],
|
||||||
|
URL: decodedURL,
|
||||||
|
FileSize: fileSize,
|
||||||
|
FileUnique: matches[6],
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
39
message/reply.go
Normal file
39
message/reply.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package message
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 回复消息结构体
|
||||||
|
type ReplyMessage struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Data ReplyData `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回复消息数据结构体
|
||||||
|
type ReplyData struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *ReplyMessage) ToCQString() string {
|
||||||
|
return fmt.Sprintf(`\[CQ:reply,id=%d\]`, msg.Data.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *ReplyMessage) ParseMessage(data string) error {
|
||||||
|
// 使用正则表达式提取ID
|
||||||
|
re := regexp.MustCompile(`\[CQ:reply,id=(\d+)\]`)
|
||||||
|
matches := re.FindStringSubmatch(data)
|
||||||
|
if len(matches) < 2 {
|
||||||
|
return fmt.Errorf("数据格式不正确")
|
||||||
|
}
|
||||||
|
|
||||||
|
id, _ := strconv.Atoi(matches[1])
|
||||||
|
|
||||||
|
// 返回解析后的结构体
|
||||||
|
msg.Data = ReplyData{
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,143 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// @某个QQ用户的结构体
|
|
||||||
type AtMessage struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Data AtData `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// @消息数据结构体
|
|
||||||
type AtData struct {
|
|
||||||
QQ string `json:"qq"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msg *AtMessage) ToCQString() string {
|
|
||||||
return fmt.Sprintf(`\[CQ:at,qq=%s\]`, msg.Data.QQ)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseAtMessage(data string) (*AtMessage, error) {
|
|
||||||
// 使用正则表达式提取QQ号
|
|
||||||
re := regexp.MustCompile(`\[CQ:at,qq=(\d+)\]`)
|
|
||||||
matches := re.FindStringSubmatch(data)
|
|
||||||
if len(matches) < 2 {
|
|
||||||
return nil, fmt.Errorf("数据格式不正确")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回解析后的结构体
|
|
||||||
return &AtMessage{
|
|
||||||
Type: "at",
|
|
||||||
Data: AtData{
|
|
||||||
QQ: matches[1],
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 回复消息结构体
|
|
||||||
type ReplyMessage struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Data ReplyData `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 回复消息数据结构体
|
|
||||||
type ReplyData struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseReplyData(data string) (*ReplyMessage, error) {
|
|
||||||
// 使用正则表达式提取ID
|
|
||||||
re := regexp.MustCompile(`\[CQ:reply,id=(\d+)\]`)
|
|
||||||
matches := re.FindStringSubmatch(data)
|
|
||||||
if len(matches) < 2 {
|
|
||||||
return nil, fmt.Errorf("数据格式不正确")
|
|
||||||
}
|
|
||||||
|
|
||||||
id, _ := strconv.Atoi(matches[1])
|
|
||||||
|
|
||||||
// 返回解析后的结构体
|
|
||||||
return &ReplyMessage{
|
|
||||||
Type: "reply",
|
|
||||||
Data: ReplyData{
|
|
||||||
ID: id,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ImageMessage struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Data ImageMessageData `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ImageMessageData struct {
|
|
||||||
File string
|
|
||||||
SubType string
|
|
||||||
FileID string
|
|
||||||
URL string
|
|
||||||
FileSize int
|
|
||||||
FileUnique string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msg *ImageMessage) ToCQString() (string, error) {
|
|
||||||
// URL 转义
|
|
||||||
escapedURL := url.QueryEscape(msg.Data.URL)
|
|
||||||
|
|
||||||
// 构造 CQ:image 字符串
|
|
||||||
cqString := fmt.Sprintf("[CQ:image,file=%s,sub_type=%s,file_id=%s,url=%s,file_size=%d,file_unique=%s]",
|
|
||||||
msg.Data.File, msg.Data.SubType, msg.Data.FileID, escapedURL, msg.Data.FileSize, msg.Data.FileUnique)
|
|
||||||
|
|
||||||
return cqString, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseCQImageMessage(data string) (*ImageMessage, error) {
|
|
||||||
// 使用正则表达式提取各个字段
|
|
||||||
re := regexp.MustCompile(`\[CQ:image,file=(.*?),sub_type=(.*?),file_id=(.*?),url=(.*?),file_size=(\d+),file_unique=(.*?)\]`)
|
|
||||||
matches := re.FindStringSubmatch(data)
|
|
||||||
if len(matches) < 7 {
|
|
||||||
return nil, fmt.Errorf("数据格式不正确")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换file_size为整数
|
|
||||||
fileSize, err := strconv.Atoi(matches[5])
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("解析 file_size 出错: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理URL转义
|
|
||||||
decodedURL, err := url.QueryUnescape(matches[4])
|
|
||||||
|
|
||||||
decodedURL = strings.ReplaceAll(decodedURL, ",", ",")
|
|
||||||
decodedURL = strings.ReplaceAll(decodedURL, "[", "[")
|
|
||||||
decodedURL = strings.ReplaceAll(decodedURL, "]", "]")
|
|
||||||
decodedURL = strings.ReplaceAll(decodedURL, "&", "&")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("URL 转义失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回解析后的结构体
|
|
||||||
// return &ImageMessageData{
|
|
||||||
// File: matches[1],
|
|
||||||
// SubType: matches[2],
|
|
||||||
// FileID: matches[3],
|
|
||||||
// URL: decodedURL,
|
|
||||||
// FileSize: fileSize,
|
|
||||||
// FileUnique: matches[6],
|
|
||||||
// }, nil
|
|
||||||
return &ImageMessage{
|
|
||||||
Type: "image",
|
|
||||||
Data: ImageMessageData{
|
|
||||||
File: matches[1],
|
|
||||||
SubType: matches[2],
|
|
||||||
FileID: matches[3],
|
|
||||||
URL: decodedURL,
|
|
||||||
FileSize: fileSize,
|
|
||||||
FileUnique: matches[6],
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
@ -5,7 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.lxtend.com/qqbot/model"
|
"git.lxtend.com/qqbot/message"
|
||||||
"git.lxtend.com/qqbot/util"
|
"git.lxtend.com/qqbot/util"
|
||||||
"github.com/fogleman/gg"
|
"github.com/fogleman/gg"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -157,8 +157,9 @@ func GenerateCongratulationImage(text string, inputFile, outputFile string, isGo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isImageCQ(text string) (string, bool) {
|
func isImageCQ(text string) (string, bool) {
|
||||||
if img, err := model.ParseCQImageMessage(text); err == nil {
|
imgMsg := message.ImageMessage{}
|
||||||
return img.Data.URL, true
|
if err := imgMsg.ParseMessage(text); err == nil {
|
||||||
|
return imgMsg.Data.URL, true
|
||||||
}
|
}
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
166
util/picture.go
Normal file
166
util/picture.go
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"image"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nfnt/resize"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ResizeImage(imagePath string, width int, height int) (outputPath string, err error) {
|
||||||
|
// 打开源图片文件
|
||||||
|
file, err := os.Open(imagePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", 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 "", errors.New("unsupported image format")
|
||||||
|
}
|
||||||
|
|
||||||
|
if decodeErr != nil {
|
||||||
|
return "", decodeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调整图片大小
|
||||||
|
resized := resize.Resize(uint(width), uint(height), img, resize.Lanczos3)
|
||||||
|
|
||||||
|
// 创建输出文件
|
||||||
|
outputPath = strings.TrimSuffix(imagePath, ext) + "_resized" + ext
|
||||||
|
out, err := os.Create(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
// 根据文件扩展名选择编码方式
|
||||||
|
switch ext {
|
||||||
|
case ".jpg", ".jpeg":
|
||||||
|
return outputPath, jpeg.Encode(out, resized, nil)
|
||||||
|
case ".png":
|
||||||
|
return outputPath, png.Encode(out, resized)
|
||||||
|
default:
|
||||||
|
return "", errors.New("unsupported image format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResizeImageByMaxWidth 按最大宽度缩放图片,保持宽高比
|
||||||
|
func ResizeImageByMaxWidth(imagePath string, maxWidth uint) (outputPath string, err error) {
|
||||||
|
// 打开源图片文件
|
||||||
|
file, err := os.Open(imagePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", 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 "", errors.New("unsupported image format")
|
||||||
|
}
|
||||||
|
|
||||||
|
if decodeErr != nil {
|
||||||
|
return "", decodeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算缩放后的尺寸,保持宽高比
|
||||||
|
// 传入0作为高度,resize包会自动计算等比例的高度
|
||||||
|
resized := resize.Resize(maxWidth, 0, img, resize.Lanczos3)
|
||||||
|
|
||||||
|
// 创建输出文件
|
||||||
|
outputPath = strings.TrimSuffix(imagePath, ext) + "_resized" + ext
|
||||||
|
out, err := os.Create(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
// 根据文件扩展名选择编码方式
|
||||||
|
switch ext {
|
||||||
|
case ".jpg", ".jpeg":
|
||||||
|
return outputPath, jpeg.Encode(out, resized, nil)
|
||||||
|
case ".png":
|
||||||
|
return outputPath, png.Encode(out, resized)
|
||||||
|
default:
|
||||||
|
return "", errors.New("unsupported image format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResizeImageByMaxHeight 按最大高度缩放图片,保持宽高比
|
||||||
|
func ResizeImageByMaxHeight(imagePath string, maxHeight uint) (outputPath string, err error) {
|
||||||
|
// 打开源图片文件
|
||||||
|
file, err := os.Open(imagePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", 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 "", errors.New("unsupported image format")
|
||||||
|
}
|
||||||
|
|
||||||
|
if decodeErr != nil {
|
||||||
|
return "", decodeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算缩放后的尺寸,保持宽高比
|
||||||
|
// 传入0作为宽度,resize包会自动计算等比例的宽度
|
||||||
|
resized := resize.Resize(0, maxHeight, img, resize.Lanczos3)
|
||||||
|
|
||||||
|
// 创建输出文件
|
||||||
|
outputPath = strings.TrimSuffix(imagePath, ext) + "_resized" + ext
|
||||||
|
out, err := os.Create(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
// 根据文件扩展名选择编码方式
|
||||||
|
switch ext {
|
||||||
|
case ".jpg", ".jpeg":
|
||||||
|
return outputPath, jpeg.Encode(out, resized, nil)
|
||||||
|
case ".png":
|
||||||
|
return outputPath, png.Encode(out, resized)
|
||||||
|
default:
|
||||||
|
return "", errors.New("unsupported image format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetResizedIamgePathByOrgPath(orgPath string) string {
|
||||||
|
ext := strings.ToLower(filepath.Ext(orgPath))
|
||||||
|
return strings.TrimSuffix(orgPath, ext) + "_resized" + ext
|
||||||
|
}
|
@ -36,11 +36,10 @@ func normalizeURL(rawURL string) string {
|
|||||||
return u.String()
|
return u.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func DownloadFile(url string, filepath string) error {
|
func DownloadFile(url string, filepath string) (err error) {
|
||||||
// 发送 HTTP GET 请求
|
// 发送 HTTP GET 请求
|
||||||
// resp, err := http.Get(url)
|
// resp, err := http.Get(url)
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
var err error
|
|
||||||
var maxRetry = 100
|
var maxRetry = 100
|
||||||
var retry = 0
|
var retry = 0
|
||||||
for resp, err = http.Get(url); err != nil && retry < maxRetry; resp, err = http.Get(url) {
|
for resp, err = http.Get(url); err != nil && retry < maxRetry; resp, err = http.Get(url) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user