From 2f00ffdb9c21a66b86a1a444e4b6347ca9dae4c0 Mon Sep 17 00:00:00 2001 From: cosmo Date: Mon, 8 Jan 2024 13:47:06 +0800 Subject: [PATCH 01/13] beta122 --- readme.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/readme.md b/readme.md index d8f7a8ee..90341e90 100644 --- a/readme.md +++ b/readme.md @@ -40,6 +40,9 @@ _✨ 基于 [OneBot](https://github.com/howmanybots/onebot/blob/master/README.md · 参与贡献

+

+ 项目主页:gensokyo.bot +

## 引用 - [`tencent-connect/botgo`](https://github.com/tencent-connect/botgo): 本项目引用了此项目,并做了一点改动. From 2158740465a2f59dd5ae7727c71b8b63158088a1 Mon Sep 17 00:00:00 2001 From: cosmo Date: Thu, 11 Jan 2024 17:13:42 +0800 Subject: [PATCH 02/13] beta123 --- handlers/send_msg.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/handlers/send_msg.go b/handlers/send_msg.go index 1982ae06..b06dc2b5 100644 --- a/handlers/send_msg.go +++ b/handlers/send_msg.go @@ -33,13 +33,15 @@ func HandleSendMsg(client callapi.Client, api openapi.OpenAPI, apiv2 openapi.Ope if msgType == "" { msgType = GetMessageTypeByUserid(config.GetAppIDStr(), message.Params.UserID) } - //新增 内存获取不到从数据库获取 + //顺序有讲究 if msgType == "" { - msgType = GetMessageTypeByUseridV2(message.Params.UserID) + msgType = GetMessageTypeByGroupidV2(message.Params.GroupID) } + //新增 内存获取不到从数据库获取 if msgType == "" { - msgType = GetMessageTypeByGroupidV2(message.Params.GroupID) + msgType = GetMessageTypeByUseridV2(message.Params.UserID) } + var idInt64 int64 var err error From ea41561628f1d01fd60b334592e65bb3cef4c156 Mon Sep 17 00:00:00 2001 From: cosmo Date: Thu, 11 Jan 2024 19:38:41 +0800 Subject: [PATCH 03/13] beta124 --- handlers/send_msg.go | 5 ++++- handlers/send_private_msg.go | 12 ++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/handlers/send_msg.go b/handlers/send_msg.go index b06dc2b5..fa6562f8 100644 --- a/handlers/send_msg.go +++ b/handlers/send_msg.go @@ -42,13 +42,15 @@ func HandleSendMsg(client callapi.Client, api openapi.OpenAPI, apiv2 openapi.Ope msgType = GetMessageTypeByUseridV2(message.Params.UserID) } - var idInt64 int64 + var idInt64, idInt642 int64 var err error if message.Params.GroupID != "" { idInt64, err = ConvertToInt64(message.Params.GroupID) + idInt642, err = ConvertToInt64(message.Params.UserID) } else if message.Params.UserID != "" { idInt64, err = ConvertToInt64(message.Params.UserID) + idInt642, err = ConvertToInt64(message.Params.GroupID) } //设置递归 对直接向gsk发送action时有效果 @@ -101,6 +103,7 @@ func HandleSendMsg(client callapi.Client, api openapi.OpenAPI, apiv2 openapi.Ope //重置递归类型 if echo.GetMapping(idInt64) <= 0 { echo.AddMsgType(config.GetAppIDStr(), idInt64, "") + echo.AddMsgType(config.GetAppIDStr(), idInt642, "") } echo.AddMapping(idInt64, echo.GetMapping(idInt64)-1) diff --git a/handlers/send_private_msg.go b/handlers/send_private_msg.go index 43bcba67..6dcefe7d 100644 --- a/handlers/send_private_msg.go +++ b/handlers/send_private_msg.go @@ -29,14 +29,14 @@ func HandleSendPrivateMsg(client callapi.Client, api openapi.OpenAPI, apiv2 open msgType = echo.GetMsgTypeByKey(echoStr) } - //如果获取不到 就用group_id获取信息类型 - if msgType == "" { - msgType = GetMessageTypeByGroupid(config.GetAppIDStr(), message.Params.GroupID) - } //如果获取不到 就用user_id获取信息类型 if msgType == "" { msgType = GetMessageTypeByUserid(config.GetAppIDStr(), message.Params.UserID) } + //顺序,私聊优先从UserID推断类型会更准确 + if msgType == "" { + msgType = GetMessageTypeByGroupid(config.GetAppIDStr(), message.Params.GroupID) + } //新增 内存获取不到从数据库获取 if msgType == "" { msgType = GetMessageTypeByUseridV2(message.Params.UserID) @@ -68,7 +68,7 @@ func HandleSendPrivateMsg(client callapi.Client, api openapi.OpenAPI, apiv2 open } switch msgType { - case "group_private","group": + case "group_private", "group": //私聊信息 var UserID string if config.GetIdmapPro() { @@ -266,7 +266,7 @@ func HandleSendPrivateMsg(client callapi.Client, api openapi.OpenAPI, apiv2 open retmsg, _ = SendResponse(client, err, &message) } } - case "guild_private","guild": + case "guild_private", "guild": //当收到发私信调用 并且来源是频道 retmsg, _ = HandleSendGuildChannelPrivateMsg(client, api, apiv2, message, nil, nil) default: From c8561ec14daf76e5daa679773968a9f78465a7d1 Mon Sep 17 00:00:00 2001 From: cosmo Date: Thu, 11 Jan 2024 19:41:19 +0800 Subject: [PATCH 04/13] beta124 --- handlers/send_msg.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/handlers/send_msg.go b/handlers/send_msg.go index fa6562f8..843e744e 100644 --- a/handlers/send_msg.go +++ b/handlers/send_msg.go @@ -46,11 +46,11 @@ func HandleSendMsg(client callapi.Client, api openapi.OpenAPI, apiv2 openapi.Ope var err error if message.Params.GroupID != "" { - idInt64, err = ConvertToInt64(message.Params.GroupID) - idInt642, err = ConvertToInt64(message.Params.UserID) + idInt64, _ = ConvertToInt64(message.Params.GroupID) + idInt642, _ = ConvertToInt64(message.Params.UserID) } else if message.Params.UserID != "" { - idInt64, err = ConvertToInt64(message.Params.UserID) - idInt642, err = ConvertToInt64(message.Params.GroupID) + idInt64, _ = ConvertToInt64(message.Params.UserID) + idInt642, _ = ConvertToInt64(message.Params.GroupID) } //设置递归 对直接向gsk发送action时有效果 From 22865d4b69358b20317304ca685d340b81f03896 Mon Sep 17 00:00:00 2001 From: cosmo Date: Thu, 11 Jan 2024 20:07:19 +0800 Subject: [PATCH 05/13] beta124 --- handlers/send_msg.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/handlers/send_msg.go b/handlers/send_msg.go index 843e744e..00960811 100644 --- a/handlers/send_msg.go +++ b/handlers/send_msg.go @@ -29,18 +29,22 @@ func HandleSendMsg(client callapi.Client, api openapi.OpenAPI, apiv2 openapi.Ope if msgType == "" { msgType = GetMessageTypeByGroupid(config.GetAppIDStr(), message.Params.GroupID) } + mylog.Printf("测试1:%v", msgType) //如果获取不到 就用user_id获取信息类型 if msgType == "" { msgType = GetMessageTypeByUserid(config.GetAppIDStr(), message.Params.UserID) } + mylog.Printf("测试2:%v", msgType) //顺序有讲究 if msgType == "" { msgType = GetMessageTypeByGroupidV2(message.Params.GroupID) } + mylog.Printf("测试3:%v", msgType) //新增 内存获取不到从数据库获取 if msgType == "" { msgType = GetMessageTypeByUseridV2(message.Params.UserID) } + mylog.Printf("测试4:%v", msgType) var idInt64, idInt642 int64 var err error From d51f4190ed57f9052fb2c069e46c35877ab820b9 Mon Sep 17 00:00:00 2001 From: cosmo Date: Thu, 11 Jan 2024 20:25:13 +0800 Subject: [PATCH 06/13] beta127 --- handlers/send_guild_channel_msg.go | 7 ++----- handlers/send_msg.go | 8 -------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/handlers/send_guild_channel_msg.go b/handlers/send_guild_channel_msg.go index c84ed3e5..c625902b 100644 --- a/handlers/send_guild_channel_msg.go +++ b/handlers/send_guild_channel_msg.go @@ -34,20 +34,17 @@ func HandleSendGuildChannelMsg(client callapi.Client, api openapi.OpenAPI, apiv2 // 当 message.Echo 是字符串类型时执行此块 msgType = echo.GetMsgTypeByKey(echoStr) } - //如果获取不到 就用group_id获取信息类型 if msgType == "" { msgType = GetMessageTypeByGroupid(config.GetAppIDStr(), message.Params.GroupID) } - //如果获取不到 就用user_id获取信息类型 if msgType == "" { msgType = GetMessageTypeByUserid(config.GetAppIDStr(), message.Params.UserID) } - //新增 内存获取不到从数据库获取 if msgType == "" { - msgType = GetMessageTypeByUseridV2(message.Params.UserID) + msgType = GetMessageTypeByGroupidV2(message.Params.GroupID) } if msgType == "" { - msgType = GetMessageTypeByGroupidV2(message.Params.GroupID) + msgType = GetMessageTypeByUseridV2(message.Params.UserID) } //当不转换频道信息时(不支持频道私聊) if msgType == "" { diff --git a/handlers/send_msg.go b/handlers/send_msg.go index 00960811..f5ba50e9 100644 --- a/handlers/send_msg.go +++ b/handlers/send_msg.go @@ -25,26 +25,18 @@ func HandleSendMsg(client callapi.Client, api openapi.OpenAPI, apiv2 openapi.Ope // 当 message.Echo 是字符串类型时执行此块 msgType = echo.GetMsgTypeByKey(echoStr) } - //如果获取不到 就用group_id获取信息类型 if msgType == "" { msgType = GetMessageTypeByGroupid(config.GetAppIDStr(), message.Params.GroupID) } - mylog.Printf("测试1:%v", msgType) - //如果获取不到 就用user_id获取信息类型 if msgType == "" { msgType = GetMessageTypeByUserid(config.GetAppIDStr(), message.Params.UserID) } - mylog.Printf("测试2:%v", msgType) - //顺序有讲究 if msgType == "" { msgType = GetMessageTypeByGroupidV2(message.Params.GroupID) } - mylog.Printf("测试3:%v", msgType) - //新增 内存获取不到从数据库获取 if msgType == "" { msgType = GetMessageTypeByUseridV2(message.Params.UserID) } - mylog.Printf("测试4:%v", msgType) var idInt64, idInt642 int64 var err error From d6efb107bdf6660cb93751fd4b611655a172b105 Mon Sep 17 00:00:00 2001 From: cosmo Date: Fri, 12 Jan 2024 22:27:57 +0800 Subject: [PATCH 07/13] beta128 --- Processor/ProcessC2CMessage.go | 4 + Processor/ProcessChannelDirectMessage.go | 6 + Processor/ProcessGroupMessage.go | 2 + Processor/ProcessGuildATMessage.go | 9 +- Processor/ProcessGuildNormalMessage.go | 4 + botgo/dto/keyboard/keyboard.go | 4 + config/config.go | 26 ++++ echo/echo.go | 9 ++ handlers/send_group_msg.go | 185 +++++++++++++++++++---- images/format.go | 180 ++++++++++++++++++++++ template/config_template.go | 2 + 11 files changed, 404 insertions(+), 27 deletions(-) create mode 100644 images/format.go diff --git a/Processor/ProcessC2CMessage.go b/Processor/ProcessC2CMessage.go index 2b734017..465ae443 100644 --- a/Processor/ProcessC2CMessage.go +++ b/Processor/ProcessC2CMessage.go @@ -118,6 +118,8 @@ func (p *Processors) ProcessC2CMessage(data *dto.WSC2CMessageData) error { // 根据条件判断是否添加Echo字段 if config.GetTwoWayEcho() { privateMsg.Echo = echostr + //用向应用端(如果支持)发送echo,来确定客户端的send_msg对应的触发词原文 + echo.AddMsgIDv3(AppIDString, echostr, messageText) } // 将当前s和appid和message进行映射 echo.AddMsgID(AppIDString, s, data.ID) @@ -220,6 +222,8 @@ func (p *Processors) ProcessC2CMessage(data *dto.WSC2CMessageData) error { // 根据条件判断是否添加Echo字段 if config.GetTwoWayEcho() { groupMsg.Echo = echostr + //用向应用端(如果支持)发送echo,来确定客户端的send_msg对应的触发词原文 + echo.AddMsgIDv3(AppIDString, echostr, messageText) } // 获取MasterID数组 masterIDs := config.GetMasterID() diff --git a/Processor/ProcessChannelDirectMessage.go b/Processor/ProcessChannelDirectMessage.go index 53eb6ee7..e27faa94 100644 --- a/Processor/ProcessChannelDirectMessage.go +++ b/Processor/ProcessChannelDirectMessage.go @@ -141,6 +141,8 @@ func (p *Processors) ProcessChannelDirectMessage(data *dto.WSDirectMessageData) // 根据条件判断是否添加Echo字段 if config.GetTwoWayEcho() { privateMsg.Echo = echostr + //用向应用端(如果支持)发送echo,来确定客户端的send_msg对应的触发词原文 + echo.AddMsgIDv3(AppIDString, echostr, messageText) } // 将当前s和appid和message进行映射 echo.AddMsgID(AppIDString, s, data.ID) @@ -218,6 +220,8 @@ func (p *Processors) ProcessChannelDirectMessage(data *dto.WSDirectMessageData) // 根据条件判断是否添加Echo字段 if config.GetTwoWayEcho() { onebotMsg.Echo = echostr + //用向应用端(如果支持)发送echo,来确定客户端的send_msg对应的触发词原文 + echo.AddMsgIDv3(AppIDString, echostr, messageText) } // 获取MasterID数组 masterIDs := config.GetMasterID() @@ -368,6 +372,8 @@ func (p *Processors) ProcessChannelDirectMessage(data *dto.WSDirectMessageData) // 根据条件判断是否添加Echo字段 if config.GetTwoWayEcho() { groupMsg.Echo = echostr + //用向应用端(如果支持)发送echo,来确定客户端的send_msg对应的触发词原文 + echo.AddMsgIDv3(AppIDString, echostr, messageText) } // 获取MasterID数组 masterIDs := config.GetMasterID() diff --git a/Processor/ProcessGroupMessage.go b/Processor/ProcessGroupMessage.go index 28387413..87d2e8a1 100644 --- a/Processor/ProcessGroupMessage.go +++ b/Processor/ProcessGroupMessage.go @@ -135,6 +135,8 @@ func (p *Processors) ProcessGroupMessage(data *dto.WSGroupATMessageData) error { // 根据条件判断是否添加Echo字段 if config.GetTwoWayEcho() { groupMsg.Echo = echostr + //用向应用端(如果支持)发送echo,来确定客户端的send_msg对应的触发词原文 + echo.AddMsgIDv3(AppIDString, echostr, messageText) } // 获取MasterID数组 masterIDs := config.GetMasterID() diff --git a/Processor/ProcessGuildATMessage.go b/Processor/ProcessGuildATMessage.go index e043dfba..c9426919 100644 --- a/Processor/ProcessGuildATMessage.go +++ b/Processor/ProcessGuildATMessage.go @@ -75,7 +75,12 @@ func (p *Processors) ProcessGuildATMessage(data *dto.WSATMessageData) error { SubType: "channel", Time: t.Unix(), Avatar: data.Author.Avatar, - Echo: echostr, + } + // 根据条件判断是否添加Echo字段 + if config.GetTwoWayEcho() { + onebotMsg.Echo = echostr + //用向应用端(如果支持)发送echo,来确定客户端的send_msg对应的触发词原文 + echo.AddMsgIDv3(AppIDString, echostr, messageText) } // 获取MasterID数组 masterIDs := config.GetMasterID() @@ -226,6 +231,8 @@ func (p *Processors) ProcessGuildATMessage(data *dto.WSATMessageData) error { // 根据条件判断是否添加Echo字段 if config.GetTwoWayEcho() { groupMsg.Echo = echostr + //用向应用端(如果支持)发送echo,来确定客户端的send_msg对应的触发词原文 + echo.AddMsgIDv3(AppIDString, echostr, messageText) } // 获取MasterID数组 masterIDs := config.GetMasterID() diff --git a/Processor/ProcessGuildNormalMessage.go b/Processor/ProcessGuildNormalMessage.go index 71b66abc..da7c32a6 100644 --- a/Processor/ProcessGuildNormalMessage.go +++ b/Processor/ProcessGuildNormalMessage.go @@ -78,6 +78,8 @@ func (p *Processors) ProcessGuildNormalMessage(data *dto.WSMessageData) error { // 根据条件判断是否添加Echo字段 if config.GetTwoWayEcho() { onebotMsg.Echo = echostr + //用向应用端(如果支持)发送echo,来确定客户端的send_msg对应的触发词原文 + echo.AddMsgIDv3(AppIDString, echostr, messageText) } // 获取MasterID数组 masterIDs := config.GetMasterID() @@ -225,6 +227,8 @@ func (p *Processors) ProcessGuildNormalMessage(data *dto.WSMessageData) error { // 根据条件判断是否添加Echo字段 if config.GetTwoWayEcho() { groupMsg.Echo = echostr + //用向应用端(如果支持)发送echo,来确定客户端的send_msg对应的触发词原文 + echo.AddMsgIDv3(AppIDString, echostr, messageText) } // 获取MasterID数组 masterIDs := config.GetMasterID() diff --git a/botgo/dto/keyboard/keyboard.go b/botgo/dto/keyboard/keyboard.go index f53b4a74..b6c26149 100644 --- a/botgo/dto/keyboard/keyboard.go +++ b/botgo/dto/keyboard/keyboard.go @@ -60,6 +60,10 @@ type Action struct { ClickLimit uint32 `json:"click_limit,omitempty"` // 可点击的次数, 默认不限 Data string `json:"data,omitempty"` // 操作相关数据 AtBotShowChannelList bool `json:"at_bot_show_channel_list,omitempty"` // false:当前 true:弹出展示子频道选择器 + UnsupportTips string `json:"unsupport_tips"` //2024-1-12 新增字段 + AnChor int `json:"anchor"` //本字段仅在指令按钮下有效,设置后后会忽略 action.enter 配置。设置为 1 时 ,点击按钮自动唤起启手Q选图器,其他值暂无效果。(仅支持手机端版本 8983+ 的单聊场景,桌面端不支持) + Enter bool `json:"enter"` //指令按钮可用,点击按钮后直接自动发送 data,默认 false。支持版本 8983 + Reply bool `json:"reply"` //指令按钮可用,指令是否带引用回复本消息,默认 false。支持版本 8983 } // Permission 按纽操作权限 diff --git a/config/config.go b/config/config.go index 021ed06f..9f8a7940 100644 --- a/config/config.go +++ b/config/config.go @@ -131,6 +131,8 @@ type Settings struct { WhiteEnable []bool `yaml:"white_enable"` IdentifyAppids []int64 `yaml:"identify_appids"` TransFormApiIds bool `yaml:"transform_api_ids"` + CustomTemplateID string `yaml:"custom_template_id"` + KeyBoardID string `yaml:"keyboard_id"` } // LoadConfig 从文件中加载配置并初始化单例配置 @@ -1650,3 +1652,27 @@ func GetTransFormApiIds() bool { } return instance.Settings.TransFormApiIds } + +// 获取 CustomTemplateID 的值 +func GetCustomTemplateID() string { + mu.Lock() + defer mu.Unlock() + + if instance == nil { + mylog.Println("Warning: instance is nil when trying to get CustomTemplateID.") + return "" + } + return instance.Settings.CustomTemplateID +} + +// 获取 KeyBoardIDD 的值 +func GetKeyBoardID() string { + mu.Lock() + defer mu.Unlock() + + if instance == nil { + mylog.Println("Warning: instance is nil when trying to get KeyBoardID.") + return "" + } + return instance.Settings.KeyBoardID +} diff --git a/echo/echo.go b/echo/echo.go index c898215a..fc840e4f 100644 --- a/echo/echo.go +++ b/echo/echo.go @@ -106,6 +106,15 @@ func AddMsgIDv3(appid string, s string, msgID string) { globalEchoMapping.msgIDMapping[key] = msgID } +// GetMsgIDv3 返回给定appid和s的msgID +func GetMsgIDv3(appid string, s string) string { + key := globalEchoMapping.GenerateKeyv3(appid, s) + globalEchoMapping.mu.Lock() + defer globalEchoMapping.mu.Unlock() + + return globalEchoMapping.msgIDMapping[key] +} + // 添加group和userid对应的messageid func AddMsgIDv2(appid string, groupid int64, userid int64, msgID string) { key := globalEchoMapping.GenerateKeyv2(appid, groupid, userid) diff --git a/handlers/send_group_msg.go b/handlers/send_group_msg.go index 9905aada..93b869e7 100644 --- a/handlers/send_group_msg.go +++ b/handlers/send_group_msg.go @@ -4,10 +4,12 @@ import ( "bytes" "context" "encoding/base64" + "fmt" "io" "net/http" "os" "strconv" + "strings" "time" "github.com/hoshinonyaruko/gensokyo/callapi" @@ -18,6 +20,7 @@ import ( "github.com/hoshinonyaruko/gensokyo/mylog" "github.com/hoshinonyaruko/gensokyo/silk" "github.com/tencent-connect/botgo/dto" + "github.com/tencent-connect/botgo/dto/keyboard" "github.com/tencent-connect/botgo/openapi" ) @@ -33,22 +36,18 @@ func HandleSendGroupMsg(client callapi.Client, api openapi.OpenAPI, apiv2 openap // 当 message.Echo 是字符串类型时执行此块 msgType = echo.GetMsgTypeByKey(echoStr) } - //如果获取不到 就用user_id获取信息类型 - if msgType == "" { - msgType = GetMessageTypeByUserid(config.GetAppIDStr(), message.Params.UserID) - } - - //如果获取不到 就用group_id获取信息类型 if msgType == "" { msgType = GetMessageTypeByGroupid(config.GetAppIDStr(), message.Params.GroupID) } - //新增 内存获取不到从数据库获取 if msgType == "" { - msgType = GetMessageTypeByUseridV2(message.Params.UserID) + msgType = GetMessageTypeByUserid(config.GetAppIDStr(), message.Params.UserID) } if msgType == "" { msgType = GetMessageTypeByGroupidV2(message.Params.GroupID) } + if msgType == "" { + msgType = GetMessageTypeByUseridV2(message.Params.UserID) + } mylog.Printf("send_group_msg获取到信息类型:%v", msgType) var idInt64 int64 var err error @@ -202,26 +201,52 @@ func HandleSendGroupMsg(client callapi.Client, api openapi.OpenAPI, apiv2 openap mylog.Printf("Error: Expected RichMediaMessage type for key ") return "", nil } - // 上传图片并获取FileInfo - fileInfo, err := uploadMedia(context.TODO(), message.Params.GroupID.(string), richMediaMessage, apiv2) - if err != nil { - mylog.Printf("上传图片失败: %v", err) - return "", nil // 或其他错误处理 - } - // 创建包含文本和图像信息的消息 - msgseq = echo.GetMappingSeq(messageID) - echo.AddMappingSeq(messageID, msgseq+1) - groupMessage := &dto.MessageToCreate{ - Content: messageText, // 添加文本内容 - Media: dto.Media{ - FileInfo: fileInfo, // 添加图像信息 - }, - MsgID: messageID, - MsgSeq: msgseq, - MsgType: 7, // 假设7是组合消息类型 + var groupMessage *dto.MessageToCreate + var transmd bool + var md *dto.Markdown + var kb *keyboard.MessageKeyboard + //判断是否需要自动转换md + if config.GetTwoWayEcho() { + md, kb, transmd = auto_md(message, messageText, richMediaMessage) } - groupMessage.Timestamp = time.Now().Unix() // 设置时间戳 + //如果没有转换成md发送 + if !transmd { + // 上传图片并获取FileInfo + fileInfo, err := uploadMedia(context.TODO(), message.Params.GroupID.(string), richMediaMessage, apiv2) + if err != nil { + mylog.Printf("上传图片失败: %v", err) + return "", nil // 或其他错误处理 + } + // 创建包含文本和图像信息的消息 + msgseq = echo.GetMappingSeq(messageID) + echo.AddMappingSeq(messageID, msgseq+1) + groupMessage = &dto.MessageToCreate{ + Content: messageText, // 添加文本内容 + Media: dto.Media{ + FileInfo: fileInfo, // 添加图像信息 + }, + MsgID: messageID, + MsgSeq: msgseq, + MsgType: 7, // 假设7是组合消息类型 + } + groupMessage.Timestamp = time.Now().Unix() // 设置时间戳 + } else { + //将kb和md组合成groupMessage并用MsgType=2发送 + + msgseq = echo.GetMappingSeq(messageID) + echo.AddMappingSeq(messageID, msgseq+1) + groupMessage = &dto.MessageToCreate{ + Content: "markdown", // 添加文本内容 + MsgID: messageID, + MsgSeq: msgseq, + Markdown: md, + Keyboard: kb, + MsgType: 2, // 假设7是组合消息类型 + } + groupMessage.Timestamp = time.Now().Unix() // 设置时间戳 + + } // 发送组合消息 ret, err = apiv2.PostGroupMessage(context.TODO(), message.Params.GroupID.(string), groupMessage) if err != nil { @@ -1020,3 +1045,111 @@ func SendStackMessages(apiv2 openapi.OpenAPI, messageid string, originalGroupID } } + +func auto_md(message callapi.ActionMessage, messageText string, richMediaMessage *dto.RichMediaMessage) (md *dto.Markdown, kb *keyboard.MessageKeyboard, transmd bool) { + if echoStr, ok := message.Echo.(string); ok { + // 当 message.Echo 是字符串类型时才执行此块 + msg_on_touch := echo.GetMsgIDv3(config.GetAppIDStr(), echoStr) + mylog.Printf("msg_on_touch:%v", msg_on_touch) + // 判断是否是 GetVisualkPrefixs 数组开头的文本 + visualkPrefixs := config.GetVisualkPrefixs() + var matchedPrefix *config.VisualPrefixConfig + // 去掉前缀开头的* + for i, vp := range visualkPrefixs { + if strings.HasPrefix(vp.Prefix, "*") { + visualkPrefixs[i].Prefix = strings.TrimPrefix(vp.Prefix, "*") + } + } + + for _, vp := range visualkPrefixs { + if strings.HasPrefix(msg_on_touch, vp.Prefix) { + if len(msg_on_touch) >= len(vp.Prefix) { + if msg_on_touch != " " { + transmd = true + matchedPrefix = &vp + break // 匹配到了 + } + } + } + } + if transmd { + //将messageText和groupReply组合成一个md + // 处理 Markdown + CustomTemplateID := config.GetCustomTemplateID() + imgURL := richMediaMessage.URL + height, width, err := images.GetImageDimensions(imgURL) + if err != nil { + mylog.Printf("获取图片宽高出错") + } + imgDesc := fmt.Sprintf("图片 #%dpx #%dpx", width, height) + // 检查messageText是否以\r开头 + if !strings.HasPrefix(messageText, "\r") { + messageText = "\r" + messageText + } + // 创建 MarkdownParams 的实例 + mdParams := []*dto.MarkdownParams{ + {Key: "text_start", Values: []string{" "}}, //空着 + {Key: "img_dec", Values: []string{imgDesc}}, + {Key: "img_url", Values: []string{imgURL}}, + {Key: "text_end", Values: []string{messageText}}, + } + // 组合模板 Markdown + md = &dto.Markdown{ + CustomTemplateID: CustomTemplateID, + Params: mdParams, + } + whiteList := matchedPrefix.WhiteList + // 创建 CustomKeyboard + customKeyboard := &keyboard.CustomKeyboard{ + Rows: []*keyboard.Row{}, + } + + var currentRow *keyboard.Row + buttonCount := 0 // 当前行的按钮计数 + + for _, whiteLabel := range whiteList { + // 使用 strconv.Atoi 检查 whiteLabel 是否为纯数字 + if _, err := strconv.Atoi(whiteLabel); err == nil { + // 如果没有错误,表示 whiteLabel 是一个数字,因此忽略这个元素并继续下一个迭代 + continue + } + + // 创建按钮 + button := &keyboard.Button{ + RenderData: &keyboard.RenderData{ + Label: whiteLabel, + VisitedLabel: whiteLabel, + Style: 1, + }, + Action: &keyboard.Action{ + Type: 2, // 帮用户输入二级指令 + Permission: &keyboard.Permission{ + Type: 2, //所有人可操作 + }, + Data: msg_on_touch + " " + whiteLabel, + UnsupportTips: "请升级新版手机QQ", + }, + } + + // 如果当前行为空或已满(4个按钮),则创建一个新行 + if currentRow == nil || buttonCount == 4 { + currentRow = &keyboard.Row{} + customKeyboard.Rows = append(customKeyboard.Rows, currentRow) + buttonCount = 0 // 重置按钮计数 + } + + // 将按钮添加到当前行 + currentRow.Buttons = append(currentRow.Buttons, button) + buttonCount++ + } + + // 在循环结束后,最后一行可能不满4个按钮,但已经被正确处理 + + // 创建 MessageKeyboard 并设置其 Content + kb = &keyboard.MessageKeyboard{ + Content: customKeyboard, + } + } + } + return md, kb, transmd +} diff --git a/images/format.go b/images/format.go new file mode 100644 index 00000000..a5cb61c2 --- /dev/null +++ b/images/format.go @@ -0,0 +1,180 @@ +package images + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +// 宽度 高度 +func GetImageDimensions(url string) (int, int, error) { + if strings.HasSuffix(url, ".png") { + return getPNGDimensions(url) + } else if strings.HasSuffix(url, ".jpg") || strings.HasSuffix(url, ".jpeg") { + return getJpegDimensions(url) + } else if strings.HasSuffix(url, ".gif") { + return getGIFDimensions(url) + } + return 0, 0, fmt.Errorf("unsupported image format") +} + +func getPNGDimensions(url string) (int, int, error) { + resp, err := http.Get(url) + if err != nil { + return 0, 0, fmt.Errorf("error occurred while making request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return 0, 0, fmt.Errorf("failed to download image with status code: %d", resp.StatusCode) + } + + // 只读取前33字节 + buffer := make([]byte, 33) + _, err = io.ReadFull(resp.Body, buffer) + if err != nil { + return 0, 0, fmt.Errorf("error occurred while reading the header: %w", err) + } + + // 检查PNG文件头 + if bytes.HasPrefix(buffer, []byte("\x89PNG\r\n\x1a\n")) { + var width, height int32 + reader := bytes.NewReader(buffer[16:24]) + err := binary.Read(reader, binary.BigEndian, &width) + if err != nil { + return 0, 0, fmt.Errorf("error occurred while reading width: %w", err) + } + err = binary.Read(reader, binary.BigEndian, &height) + if err != nil { + return 0, 0, fmt.Errorf("error occurred while reading height: %w", err) + } + return int(width), int(height), nil + } + + width, height, err := getJpegDimensions(url) + return width, height, err +} + +func getJpegDimensions(url string) (int, int, error) { + response, err := http.Get(url) + if err != nil { + return 0, 0, fmt.Errorf("error occurred while making request: %w", err) + } + defer response.Body.Close() + + if response.StatusCode != 200 { + return 0, 0, fmt.Errorf("failed to download image with status code: %d", response.StatusCode) + } + + bytesRead := 0 // 用于跟踪读取的字节数 + for { + b := make([]byte, 1) + _, err := response.Body.Read(b) + bytesRead++ // 更新读取的字节数 + if err == io.EOF { + return 0, 0, fmt.Errorf("reached end of file before finding dimensions") + } else if err != nil { + return 0, 0, fmt.Errorf("error occurred while reading byte: %w", err) + } + + if b[0] == 0xFF { + nextByte := make([]byte, 1) + _, err := response.Body.Read(nextByte) + if err == io.EOF { + return 0, 0, fmt.Errorf("reached end of file before finding SOI marker") + } else if err != nil { + return 0, 0, fmt.Errorf("error occurred while reading next byte: %w", err) + } + + if nextByte[0] == 0xD8 { // SOI + continue + } else if (nextByte[0] >= 0xC0 && nextByte[0] <= 0xCF) && nextByte[0] != 0xC4 && nextByte[0] != 0xC8 && nextByte[0] != 0xCC { + c := make([]byte, 3) + _, err := response.Body.Read(c) + if err != nil { + return 0, 0, fmt.Errorf("error occurred while skipping bytes: %w", err) + } + heightBytes := make([]byte, 2) + widthBytes := make([]byte, 2) + _, err = response.Body.Read(heightBytes) + if err != nil { + return 0, 0, fmt.Errorf("error occurred while reading height: %w", err) + } + _, err = response.Body.Read(widthBytes) + if err != nil { + return 0, 0, fmt.Errorf("error occurred while reading width: %w", err) + } + // 使用binary.BigEndian.Uint16将字节转换为uint16 + height := binary.BigEndian.Uint16(heightBytes) + width := binary.BigEndian.Uint16(widthBytes) + return int(height), int(width), nil + } else { + time.Sleep(5 * time.Millisecond) + lengthBytes := make([]byte, 2) + _, err := response.Body.Read(lengthBytes) + if err != nil { + return 0, 0, fmt.Errorf("error occurred while reading segment length: %w", err) + } + + length := binary.BigEndian.Uint16(lengthBytes) + bytesToSkip := int(length) - 2 + + // 循环读取并跳过指定的字节数 + for bytesToSkip > 0 { + bufferSize := bytesToSkip + if bufferSize > 512 { // 可以调整这个值以优化性能和内存使用 + bufferSize = 512 //如果成功率低 就继续减少它 + } + buffer := make([]byte, bufferSize) + n, err := response.Body.Read(buffer) + if err != nil { + if err == io.EOF { + break // 数据流结束 + } + return 0, 0, fmt.Errorf("error occurred while skipping segment: %w", err) + } + bytesToSkip -= n + } + } + } + } +} + +func getGIFDimensions(url string) (int, int, error) { + resp, err := http.Get(url) + if err != nil { + return 0, 0, fmt.Errorf("error occurred while making request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return 0, 0, fmt.Errorf("failed to download image with status code: %d", resp.StatusCode) + } + + // 仅读取前10字节 + buffer := make([]byte, 10) + _, err = io.ReadFull(resp.Body, buffer) + if err != nil { + return 0, 0, fmt.Errorf("error occurred while reading the header: %w", err) + } + + if bytes.HasPrefix(buffer, []byte("GIF")) { + var width, height int16 + reader := bytes.NewReader(buffer[6:10]) + err := binary.Read(reader, binary.LittleEndian, &width) + if err != nil { + return 0, 0, fmt.Errorf("error occurred while reading width: %w", err) + } + err = binary.Read(reader, binary.LittleEndian, &height) + if err != nil { + return 0, 0, fmt.Errorf("error occurred while reading height: %w", err) + } + return int(width), int(height), nil + } + + return 0, 0, fmt.Errorf("not a valid GIF file") +} diff --git a/template/config_template.go b/template/config_template.go index f01a81d8..bec2f464 100644 --- a/template/config_template.go +++ b/template/config_template.go @@ -128,6 +128,8 @@ settings: custom_bot_name : "Gensokyo全域机器人" #自定义机器人名字,会在api调用中返回,默认Gensokyo全域机器人 twoway_echo : false #是否采用双向echo,根据机器人选择,獭獭\早苗 true 红色问答\椛椛 或者其他 请使用 false + custom_template_id : "" #自动转换图文信息到md所需要的id *需要应用端支持双方向echo + keyboard_id : "" #自动转换图文信息到md所需要的按钮id *需要应用端支持双方向echo lazy_message_id : false #false=message_id 条条准确对应 true=message_id 按时间范围随机对应(适合主动推送bot)前提,有足够多的活跃信息刷新id池 visible_ip : false #转换url时,如果server_dir是ip true将以ip形式发出url 默认隐藏url 将server_dir配置为自己域名可以转换url From 5aaf4208df4ed0dca3bff80b24bac52e56351208 Mon Sep 17 00:00:00 2001 From: cosmo Date: Mon, 15 Jan 2024 11:54:49 +0800 Subject: [PATCH 08/13] beta129 --- Processor/ProcessGroupMessage.go | 2 + Processor/ProcessGuildATMessage.go | 4 + Processor/ProcessGuildNormalMessage.go | 4 + Processor/Processor.go | 76 ++++++++++-- botgo/dto/interaction.go | 34 ++++-- botgo/dto/keyboard/keyboard.go | 8 +- config/config.go | 35 ++++++ handlers/message_parser.go | 4 +- handlers/send_group_msg.go | 83 +++++++++++-- main.go | 3 +- template/config_template.go | 9 +- wsclient/ws.go | 158 +++++++++++++++++-------- 12 files changed, 329 insertions(+), 91 deletions(-) diff --git a/Processor/ProcessGroupMessage.go b/Processor/ProcessGroupMessage.go index 87d2e8a1..91ece57a 100644 --- a/Processor/ProcessGroupMessage.go +++ b/Processor/ProcessGroupMessage.go @@ -169,6 +169,8 @@ func (p *Processors) ProcessGroupMessage(data *dto.WSGroupATMessageData) error { echo.AddMsgType(AppIDString, GroupID64, "group") //懒message_id池 echo.AddLazyMessageId(strconv.FormatInt(GroupID64, 10), data.ID, time.Now()) + //懒message_id池 + echo.AddLazyMessageId(strconv.FormatInt(userid64, 10), data.ID, time.Now()) // 调试 PrintStructWithFieldNames(groupMsg) diff --git a/Processor/ProcessGuildATMessage.go b/Processor/ProcessGuildATMessage.go index c9426919..6bd646d8 100644 --- a/Processor/ProcessGuildATMessage.go +++ b/Processor/ProcessGuildATMessage.go @@ -111,6 +111,8 @@ func (p *Processors) ProcessGuildATMessage(data *dto.WSATMessageData) error { //todo 完善频道转换 //懒message_id池 echo.AddLazyMessageId(data.ChannelID, data.ID, time.Now()) + //懒message_id池 + echo.AddLazyMessageId(strconv.FormatInt(userid64, 10), data.ID, time.Now()) //调试 PrintStructWithFieldNames(onebotMsg) @@ -264,6 +266,8 @@ func (p *Processors) ProcessGuildATMessage(data *dto.WSATMessageData) error { echo.AddMsgType(AppIDString, ChannelID64, "guild") //懒message_id池 echo.AddLazyMessageId(strconv.FormatInt(ChannelID64, 10), data.ID, time.Now()) + //懒message_id池 + echo.AddLazyMessageId(strconv.FormatInt(userid64, 10), data.ID, time.Now()) //调试 PrintStructWithFieldNames(groupMsg) diff --git a/Processor/ProcessGuildNormalMessage.go b/Processor/ProcessGuildNormalMessage.go index da7c32a6..1c280e7e 100644 --- a/Processor/ProcessGuildNormalMessage.go +++ b/Processor/ProcessGuildNormalMessage.go @@ -111,6 +111,8 @@ func (p *Processors) ProcessGuildNormalMessage(data *dto.WSMessageData) error { //todo 完善频道ob信息 //懒message_id池 echo.AddLazyMessageId(data.ChannelID, data.ID, time.Now()) + //懒message_id池 + echo.AddLazyMessageId(strconv.FormatInt(userid64, 10), data.ID, time.Now()) //调试 PrintStructWithFieldNames(onebotMsg) @@ -272,6 +274,8 @@ func (p *Processors) ProcessGuildNormalMessage(data *dto.WSMessageData) error { echo.AddMsgType(AppIDString, ChannelID64, "guild") //懒message_id池 echo.AddLazyMessageId(strconv.FormatInt(ChannelID64, 10), data.ID, time.Now()) + //懒message_id池 + echo.AddLazyMessageId(strconv.FormatInt(userid64, 10), data.ID, time.Now()) //调试 PrintStructWithFieldNames(groupMsg) diff --git a/Processor/Processor.go b/Processor/Processor.go index b95d58ba..cc104dca 100644 --- a/Processor/Processor.go +++ b/Processor/Processor.go @@ -112,6 +112,18 @@ type OnebotPrivateMessage struct { IsBindedUserId bool `json:"is_binded_user_id,omitempty"` //当前用户号号是否是binded后的 } +// onebotv11标准扩展 +type OnebotInteractionNotice struct { + GroupID int64 `json:"group_id,omitempty"` + NoticeType string `json:"notice_type,omitempty"` + PostType string `json:"post_type,omitempty"` + SelfID int64 `json:"self_id,omitempty"` + SubType string `json:"sub_type,omitempty"` + Time int64 `json:"time,omitempty"` + UserID int64 `json:"user_id,omitempty"` + Data *dto.WSInteractionData `json:"data,omitempty"` +} + type PrivateSender struct { Nickname string `json:"nickname"` UserID int64 `json:"user_id"` // Can be either string or int depending on logic @@ -123,19 +135,63 @@ func FoxTimestamp() int64 { // ProcessInlineSearch 处理内联查询 func (p *Processors) ProcessInlineSearch(data *dto.WSInteractionData) error { - //ctx := context.Background() // 或从更高级别传递一个上下文 + // 转换appid + var userid64 int64 + var GroupID64 int64 + var err error + var fromgid, fromuid string + if data.GroupOpenID != "" { + fromgid = data.GroupOpenID + fromuid = data.GroupMemberOpenID + } else { + fromgid = data.ChannelID + fromuid = "0" + } + if config.GetIdmapPro() { + //将真实id转为int userid64 + GroupID64, userid64, err = idmap.StoreIDv2Pro(fromgid, fromuid) + if err != nil { + mylog.Fatalf("Error storing ID: %v", err) + } + //当参数不全 + _, _ = idmap.StoreIDv2(fromgid) + _, _ = idmap.StoreIDv2(fromuid) + if !config.GetHashIDValue() { + mylog.Fatalf("避坑日志:你开启了高级id转换,请设置hash_id为true,并且删除idmaps并重启") + } + } else { + // 映射str的GroupID到int + GroupID64, err = idmap.StoreIDv2(fromgid) + if err != nil { + mylog.Errorf("failed to convert ChannelID to int: %v", err) + return nil + } + // 映射str的userid到int + userid64, err = idmap.StoreIDv2(fromuid) + if err != nil { + mylog.Printf("Error storing ID: %v", err) + return nil + } + } + notice := &OnebotInteractionNotice{ + GroupID: GroupID64, + NoticeType: "interaction", + PostType: "notice", + SelfID: int64(p.Settings.AppID), + SubType: "create", + Time: time.Now().Unix(), + UserID: userid64, + Data: data, + } - // 在这里处理内联查询 - // 这可能涉及解析查询、调用某些API、获取结果并格式化为响应 - // ... + //调试 + PrintStructWithFieldNames(notice) - // 示例:发送响应 - // response := "Received your interaction!" // 创建响应消息 - // err := p.api.PostInteractionResponse(ctx, response) // 替换为您的OpenAPI方法 - // if err != nil { - // return err - // } + // Convert OnebotGroupMessage to map and send + noticeMap := structToMap(notice) + //上报信息到onebotv11应用端(正反ws) + p.BroadcastMessageToAll(noticeMap) return nil } diff --git a/botgo/dto/interaction.go b/botgo/dto/interaction.go index 9d633c5b..31544046 100644 --- a/botgo/dto/interaction.go +++ b/botgo/dto/interaction.go @@ -1,16 +1,20 @@ package dto -import "encoding/json" - // Interaction 互动行为对象 type Interaction struct { - ID string `json:"id,omitempty"` // 互动行为唯一标识 - ApplicationID string `json:"application_id,omitempty"` // 应用ID - Type InteractionType `json:"type,omitempty"` // 互动类型 - Data *InteractionData `json:"data,omitempty"` // 互动数据 - GuildID string `json:"guild_id,omitempty"` // 频道 ID - ChannelID string `json:"channel_id,omitempty"` // 子频道 ID - Version uint32 `json:"version,omitempty"` // 版本,默认为 1 + ID string `json:"id,omitempty"` // 平台方事件 ID + Type InteractionType `json:"type,omitempty"` // 消息按钮: 11, 单聊快捷菜单: 12 + Scene string `json:"scene,omitempty"` // 事件发生的场景 + ChatType int `json:"chat_type,omitempty"` // 频道场景: 0, 群聊场景: 1, 单聊场景: 2 + Timestamp string `json:"timestamp,omitempty"` // 触发时间 RFC 3339 格式 + GuildID string `json:"guild_id,omitempty"` // 频道的 openid + ChannelID string `json:"channel_id,omitempty"` // 文字子频道的 openid + UserOpenID string `json:"user_openid,omitempty"` // 单聊按钮触发的用户 openid + GroupOpenID string `json:"group_openid,omitempty"` // 群的 openid + GroupMemberOpenID string `json:"group_member_openid,omitempty"` // 群成员 openid + Data *InteractionData `json:"data,omitempty"` // 互动数据 + Version uint32 `json:"version,omitempty"` // 版本,默认为 1 + ApplicationID string `json:"application_id,omitempty"` // 机器人的 appid } // InteractionType 互动类型 @@ -25,9 +29,15 @@ const ( // InteractionData 互动数据 type InteractionData struct { - Name string `json:"name,omitempty"` // 标题 - Type InteractionDataType `json:"type,omitempty"` // 数据类型,不同数据类型对应不同的 resolved 数据 - Resolved json.RawMessage `json:"resolved,omitempty"` // 跟不同的互动类型和数据类型有关系的数据 + Name string `json:"name,omitempty"` // 标题 + Type InteractionDataType `json:"type,omitempty"` // 数据类型 + Resolved struct { + ButtonData string `json:"button_data,omitempty"` // 操作按钮的 data 字段值 + ButtonID string `json:"button_id,omitempty"` // 操作按钮的 id 字段值 + UserID string `json:"user_id,omitempty"` // 操作的用户 userid + FeatureID string `json:"feature_id,omitempty"` // 操作按钮的 feature id + MessageID string `json:"message_id,omitempty"` // 操作的消息 id + } `json:"resolved,omitempty"` } // InteractionDataType 互动数据类型 diff --git a/botgo/dto/keyboard/keyboard.go b/botgo/dto/keyboard/keyboard.go index b6c26149..c1087fb5 100644 --- a/botgo/dto/keyboard/keyboard.go +++ b/botgo/dto/keyboard/keyboard.go @@ -55,7 +55,7 @@ type RenderData struct { // Action 按纽点击操作 type Action struct { - Type ActionType `json:"type,omitempty"` // 操作类型 + Type ActionType `json:"type,omitempty"` // 操作类型 设置 0 跳转按钮:http 或 小程序 客户端识别 scheme,设置 1 回调按钮:回调后台接口, data 传给后台,设置 2 指令按钮:自动在输入框插入 @bot data Permission *Permission `json:"permission,omitempty"` // 可操作 ClickLimit uint32 `json:"click_limit,omitempty"` // 可点击的次数, 默认不限 Data string `json:"data,omitempty"` // 操作相关数据 @@ -68,10 +68,10 @@ type Action struct { // Permission 按纽操作权限 type Permission struct { - // Type 操作权限类型 + // Type 操作权限类型 0 指定用户可操作,1 仅管理者可操作,2 所有人可操作,3 指定身份组可操作(仅频道可用) Type PermissionType `json:"type,omitempty"` - // SpecifyRoleIDs 身份组 + // SpecifyRoleIDs 身份组(仅频道可用) SpecifyRoleIDs []string `json:"specify_role_ids,omitempty"` - // SpecifyUserIDs 指定 UserID + // SpecifyUserIDs 指定 UserID 有权限的用户 id 的列表 SpecifyUserIDs []string `json:"specify_user_ids,omitempty"` } diff --git a/config/config.go b/config/config.go index 9f8a7940..78ddf764 100644 --- a/config/config.go +++ b/config/config.go @@ -133,6 +133,9 @@ type Settings struct { TransFormApiIds bool `yaml:"transform_api_ids"` CustomTemplateID string `yaml:"custom_template_id"` KeyBoardID string `yaml:"keyboard_id"` + Uin int64 `yaml:"uin"` + VwhitePrefixMode bool `yaml:"v_white_prefix_mode"` + Enters []string `yaml:"enters"` } // LoadConfig 从文件中加载配置并初始化单例配置 @@ -1676,3 +1679,35 @@ func GetKeyBoardID() string { } return instance.Settings.KeyBoardID } + +// 获取Uin String +func GetUinStr() string { + mu.Lock() + defer mu.Unlock() + if instance != nil { + return fmt.Sprintf("%d", instance.Settings.Uin) + } + return "0" +} + +// 获取 VV GetVwhitePrefixMode 的值 +func GetVwhitePrefixMode() bool { + mu.Lock() + defer mu.Unlock() + + if instance == nil { + mylog.Println("Warning: instance is nil when trying to VwhitePrefixMode value.") + return false + } + return instance.Settings.VwhitePrefixMode +} + +// 获取Enters的值 +func GetEnters() []string { + mu.Lock() + defer mu.Unlock() + if instance != nil { + return instance.Settings.Enters + } + return nil // 返回nil,如果instance为nil +} diff --git a/handlers/message_parser.go b/handlers/message_parser.go index 4298ce71..48e3519a 100644 --- a/handlers/message_parser.go +++ b/handlers/message_parser.go @@ -503,8 +503,8 @@ func RevertTransformedText(data interface{}, msgtype string, api openapi.OpenAPI aliases := config.GetAlias() messageText = processMessageText(messageText, aliases) //mylog.Printf("4[%v]", messageText) - // 检查是否启用白名单模式 - if config.GetWhitePrefixMode() && matchedPrefix != nil { + // 检查是否启用二级白名单模式 + if config.GetVwhitePrefixMode() && matchedPrefix != nil { // 获取白名单反转标志 whiteBypassRevers := config.GetWhiteBypassRevers() diff --git a/handlers/send_group_msg.go b/handlers/send_group_msg.go index 93b869e7..651827d6 100644 --- a/handlers/send_group_msg.go +++ b/handlers/send_group_msg.go @@ -85,6 +85,11 @@ func HandleSendGroupMsg(client callapi.Client, api openapi.OpenAPI, apiv2 openap //由于实现了Params的自定义unmarshell 所以可以类型安全的断言为string messageID = echo.GetLazyMessagesId(message.Params.GroupID.(string)) mylog.Printf("GetLazyMessagesId: %v", messageID) + //如果应用端传递了user_id 就让at不要顺序乱套 + if message.Params.UserID != nil { + messageID = echo.GetLazyMessagesId(message.Params.UserID.(string)) + mylog.Printf("GetLazyMessagesIdv2: %v", messageID) + } if messageID != "" { //尝试发送栈内信息 SSM = true @@ -1082,6 +1087,10 @@ func auto_md(message callapi.ActionMessage, messageText string, richMediaMessage mylog.Printf("获取图片宽高出错") } imgDesc := fmt.Sprintf("图片 #%dpx #%dpx", width, height) + // 将所有的\r\n替换为\r + messageText = strings.ReplaceAll(messageText, "\r\n", "\r") + // 将所有的\n替换为\r + messageText = strings.ReplaceAll(messageText, "\n", "\r") // 检查messageText是否以\r开头 if !strings.HasPrefix(messageText, "\r") { messageText = "\r" + messageText @@ -1108,26 +1117,64 @@ func auto_md(message callapi.ActionMessage, messageText string, richMediaMessage buttonCount := 0 // 当前行的按钮计数 for _, whiteLabel := range whiteList { - // 使用 strconv.Atoi 检查 whiteLabel 是否为纯数字 - if _, err := strconv.Atoi(whiteLabel); err == nil { - // 如果没有错误,表示 whiteLabel 是一个数字,因此忽略这个元素并继续下一个迭代 - continue + // 如果whiteList的成员数大于或等于15,才检查whiteLabel是否为纯数字 + if len(whiteList) >= 15 { + if _, err := strconv.Atoi(whiteLabel); err == nil { + // 如果没有错误,表示 whiteLabel 是一个数字,因此忽略这个元素并继续下一个迭代 + // 避免 因为纯数字按钮太多导致混乱,但是少量的纯数字按钮可以允许 + continue + } + } + // 检查msg_on_touch是否已经以whiteLabel结尾 + //场景 按钮递归时 比如 随机meme 再来一次,同时随机meme是*类型一级指令 + var dataLabel string + if !strings.HasSuffix(msg_on_touch, whiteLabel) { + dataLabel = msg_on_touch + whiteLabel + } else { + dataLabel = msg_on_touch } + var actiontype keyboard.ActionType + var permission *keyboard.Permission + var actiondata string + //检查是否设置了enter数组 + enter := checkDataLabelPrefix(dataLabel) + switch whiteLabel { + case "邀请机器人": + botuin := config.GetUinStr() + botappid := config.GetAppIDStr() + boturl := BuildQQBotShareLink(botuin, botappid) + actiontype = 0 + actiondata = boturl + permission = &keyboard.Permission{ + Type: 2, // 所有人可操作 + } + case "测试按钮回调": + actiontype = 1 + actiondata = "收到就代表是管理员哦" + permission = &keyboard.Permission{ + Type: 1, // 仅管理可操作 + } + default: + actiontype = 2 //帮用户输入指令 + actiondata = dataLabel //从虚拟前缀的二级指令组合md按钮 + permission = &keyboard.Permission{ + Type: 2, // 所有人可操作 + } + } // 创建按钮 button := &keyboard.Button{ RenderData: &keyboard.RenderData{ Label: whiteLabel, VisitedLabel: whiteLabel, - Style: 1, + Style: 1, //蓝色边缘 }, Action: &keyboard.Action{ - Type: 2, // 帮用户输入二级指令 - Permission: &keyboard.Permission{ - Type: 2, //所有人可操作 - }, - Data: msg_on_touch + " " + whiteLabel, + Type: actiontype, + Permission: permission, + Data: actiondata, UnsupportTips: "请升级新版手机QQ", + Enter: enter, }, } @@ -1153,3 +1200,19 @@ func auto_md(message callapi.ActionMessage, messageText string, richMediaMessage } return md, kb, transmd } + +// 构建QQ群机器人分享链接的函数 +func BuildQQBotShareLink(uin string, appid string) string { + return fmt.Sprintf("https://qun.qq.com/qunpro/robot/qunshare?robot_uin=%s&robot_appid=%s", uin, appid) +} + +// 检查dataLabel是否以config中getenters返回的任一字符串开头 +func checkDataLabelPrefix(dataLabel string) bool { + enters := config.GetEnters() + for _, enter := range enters { + if enter != "" && strings.HasPrefix(dataLabel, enter) { + return true + } + } + return false +} diff --git a/main.go b/main.go index e45c8c6a..b2ca9fdd 100644 --- a/main.go +++ b/main.go @@ -523,7 +523,6 @@ func DirectMessageHandler() event.DirectMessageEventHandler { // CreateMessageHandler 处理消息事件 私域的事件 不at信息 func CreateMessageHandler() event.MessageEventHandler { return func(event *dto.WSPayload, data *dto.WSMessageData) error { - log.Println("收到私域信息", data) return p.ProcessGuildNormalMessage(data) } } @@ -531,7 +530,7 @@ func CreateMessageHandler() event.MessageEventHandler { // InteractionHandler 处理内联交互事件 func InteractionHandler() event.InteractionEventHandler { return func(event *dto.WSPayload, data *dto.WSInteractionData) error { - log.Println(data) + mylog.Printf("收到按钮回调:%v", data) return p.ProcessInlineSearch(data) } } diff --git a/template/config_template.go b/template/config_template.go index bec2f464..cda74bc4 100644 --- a/template/config_template.go +++ b/template/config_template.go @@ -6,7 +6,8 @@ settings: #反向ws设置 ws_address: ["ws://:"] # WebSocket服务的地址 支持多个["","",""] ws_token: ["","",""] #连接wss地址时服务器所需的token,按顺序一一对应,如果是ws地址,没有密钥,请留空. - app_id: 12345 # 你的应用ID + app_id: 12345 # 你的应用ID + uin : 0 # 你的机器人QQ号,点击机器人资料卡查看 token: "" # 你的应用令牌 client_secret: "" # 你的客户端密钥 @@ -19,7 +20,7 @@ settings: # - "MemberEventHandler" # 频道成员新增 # - "ChannelEventHandler" # 频道事件 # - "CreateMessageHandler" # 频道不at信息 私域机器人需要开启 公域机器人开启会连接失败 - # - "InteractionHandler" # 添加频道互动回应 + # - "InteractionHandler" # 添加频道互动回应 卡片按钮data回调事件 # - "GroupATMessageEventHandler" # 群at信息 仅频道机器人时候需要注释 # - "C2CMessageEventHandler" # 群私聊 仅频道机器人时候需要注释 # - "ThreadEventHandler" # 发帖事件 (当前版本已禁用) @@ -92,6 +93,7 @@ settings: add_at_group : false #自动在群聊指令前加上at,某些机器人写法特别,必须有at才反应时,请打开,默认请关闭(如果需要at,不需要at指令混杂,请优化代码适配群场景,群场景目前没有at概念) white_prefix_mode : false #公域 过审用 指令白名单模式开关 如果审核严格 请开启并设置白名单指令 以白名单开头的指令会被通过,反之被拦截 + v_white_prefix_mode : true #虚拟二级指令(虚拟前缀)的白名单,默认开启,有二级指令帮助的效果 white_prefixs : [""] #可设置多个 比如设置 机器人 测试 则只有信息以机器人 测试开头会相应 remove_prefix remove_at 需为true时生效 white_bypass : [] #格式[1,2,3],白名单不生效的群或用户(私聊时),用于设置自己的灰度沙箱群/灰度沙箱私聊,避免开发测试时反复开关白名单的不便,请勿用于生产环境. white_enable : [true,true,true,true,true] #指令白名单生效范围,5个分别对应,频道公(ATMessageEventHandler),频道私(CreateMessageHandler),频道私聊,群,群私聊,改成false,这个范围就不生效指令白名单(使用场景:群全量,频道私域的机器人,或有私信资质的机器人) @@ -101,11 +103,12 @@ settings: black_prefix_mode : false #公私域 过审用 指令黑名单模式开关 过滤被审核打回的指令不响应 无需改机器人后端 black_prefixs : [""] #可设置多个 比如设置 查询 则查询开头的信息均被拦截 防止审核失败 alias : ["",""] #两两成对,指令替换,"a","b","c","d"代表将a开头替换为b开头,c开头替换为d开头. + enters : ["",""] #自动md卡片点击直接触发,小众功能,满足以下条件:应用端支持双向echo+设置了visual_prefixs和whiteList visual_prefixs : #虚拟前缀 与white_prefixs配合使用 处理流程自动忽略该前缀 remove_prefix remove_at 需为true时生效 - prefix: "" #虚拟前缀开头 例 你有3个指令 帮助 测试 查询 将 prefix 设置为 工具类 后 则可通过 工具类 帮助 触发机器人 whiteList: [""] #开关状态取决于 white_prefix_mode 为每一个二级指令头设计独立的白名单 - No_White_Response : "" + No_White_Response : "" #带有*代表不忽略掉,但是应用二级白名单的普通指令 如果 whiteList=邀请机器人 就是邀请机器人按钮 - prefix: "" whiteList: [""] No_White_Response : "" diff --git a/wsclient/ws.go b/wsclient/ws.go index b5eff231..5227ffa2 100644 --- a/wsclient/ws.go +++ b/wsclient/ws.go @@ -25,13 +25,14 @@ type WebSocketClient struct { cancel context.CancelFunc mutex sync.Mutex // 用于同步写入和重连操作的互斥锁 isReconnecting bool - sendFailures chan map[string]interface{} + sendFailures []map[string]interface{} // 存储失败的消息 + } // 发送json信息给onebot应用端 -func (c *WebSocketClient) SendMessage(message map[string]interface{}) error { - c.mutex.Lock() // 在写操作之前锁定 - defer c.mutex.Unlock() // 确保在函数返回时解锁 +func (client *WebSocketClient) SendMessage(message map[string]interface{}) error { + client.mutex.Lock() // 在写操作之前锁定 + defer client.mutex.Unlock() // 确保在函数返回时解锁 msgBytes, err := json.Marshal(message) if err != nil { @@ -39,13 +40,11 @@ func (c *WebSocketClient) SendMessage(message map[string]interface{}) error { return err } - err = c.conn.WriteMessage(websocket.TextMessage, msgBytes) + err = client.conn.WriteMessage(websocket.TextMessage, msgBytes) if err != nil { mylog.Println("Error sending message:", err) - // 发送失败,将消息放入channel - go func() { - c.sendFailures <- message - }() + // 发送失败,将消息添加到切片 + client.sendFailures = append(client.sendFailures, message) return err } @@ -53,72 +52,133 @@ func (c *WebSocketClient) SendMessage(message map[string]interface{}) error { } // 处理onebotv11应用端发来的信息 -func (c *WebSocketClient) handleIncomingMessages(ctx context.Context, cancel context.CancelFunc) { +func (client *WebSocketClient) handleIncomingMessages(ctx context.Context, cancel context.CancelFunc) { for { - _, msg, err := c.conn.ReadMessage() + _, msg, err := client.conn.ReadMessage() if err != nil { mylog.Println("WebSocket connection closed:", err) cancel() // 取消心跳 goroutine - if !c.isReconnecting { - go c.Reconnect() + if !client.isReconnecting { + go client.Reconnect() } return // 退出循环,不再尝试读取消息 } - go c.recvMessage(msg) + go client.recvMessage(msg) } } // 断线重连 func (client *WebSocketClient) Reconnect() { - client.mutex.Lock() - if client.isReconnecting { - client.mutex.Unlock() - return // 如果已经有其他携程在重连了,就直接返回 + client.isReconnecting = true + + addresses := config.GetWsAddress() + tokens := config.GetWsToken() + + var token string + for index, address := range addresses { + if address == client.urlStr && index < len(tokens) { + token = tokens[index] + break + } } - // 暂存旧的 sendFailures channel,不要关闭它 - oldSendFailures := client.sendFailures + // 检查URL中是否有access_token参数 + mp := getParamsFromURI(client.urlStr) + if val, ok := mp["access_token"]; ok { + token = val + } - client.isReconnecting = true - client.mutex.Unlock() + headers := http.Header{ + "User-Agent": []string{"CQHttp/4.15.0"}, + "X-Client-Role": []string{"Universal"}, + "X-Self-ID": []string{fmt.Sprintf("%d", client.botID)}, + } + + if token != "" { + headers["Authorization"] = []string{"Token " + token} + } + mylog.Printf("准备使用token[%s]重新连接到[%s]\n", token, client.urlStr) + dialer := websocket.Dialer{ + Proxy: http.ProxyFromEnvironment, + HandshakeTimeout: 45 * time.Second, + } + + var conn *websocket.Conn + var err error + + maxRetryAttempts := config.GetReconnecTimes() + retryCount := 0 + for { + mylog.Println("Dialing URL:", client.urlStr) + conn, _, err = dialer.Dial(client.urlStr, headers) + if err != nil { + retryCount++ + if retryCount > maxRetryAttempts { + mylog.Printf("Exceeded maximum retry attempts for WebSocket[%v]: %v\n", client.urlStr, err) + return + } + mylog.Printf("Failed to connect to WebSocket[%v]: %v, retrying in 5 seconds...\n", client.urlStr, err) + time.Sleep(5 * time.Second) // sleep for 5 seconds before retrying + } else { + mylog.Printf("Successfully connected to %s.\n", client.urlStr) // 输出连接成功提示 + break // successfully connected, break the loop + } + } + // 复用现有的client完成重连 + client.conn = conn + + // 再次发送元事件 + message := map[string]interface{}{ + "meta_event_type": "lifecycle", + "post_type": "meta_event", + "self_id": client.botID, + "sub_type": "connect", + "time": int(time.Now().Unix()), + } + + mylog.Printf("Message: %+v\n", message) + + err = client.SendMessage(message) + if err != nil { + // handle error + mylog.Printf("Error sending message: %v\n", err) + } + + //退出老的sendHeartbeat和handleIncomingMessages + client.cancel() + + // Starting goroutine for heartbeats and another for listening to messages + ctx, cancel := context.WithCancel(context.Background()) + + client.cancel = cancel + heartbeatinterval := config.GetHeartBeatInterval() + go client.sendHeartbeat(ctx, client.botID, heartbeatinterval) + go client.handleIncomingMessages(ctx, cancel) defer func() { - client.mutex.Lock() client.isReconnecting = false - client.mutex.Unlock() }() - reconnecttimes := config.GetReconnecTimes() - newClient, err := NewWebSocketClient(client.urlStr, client.botID, client.api, client.apiv2, reconnecttimes) - if err == nil && newClient != nil { - client.mutex.Lock() // 在替换连接之前锁定 - client.cancel() // 退出旧的连接 - client.conn = newClient.conn - client.api = newClient.api - client.apiv2 = newClient.apiv2 - client.cancel = newClient.cancel // 更新取消函数 - client.mutex.Unlock() - // 重发失败的消息 - newClient.processFailedMessages(oldSendFailures) - mylog.Println("Successfully reconnected to WebSocket.") - return - } + + mylog.Printf("Successfully reconnected to WebSocket.") } // 处理发送失败的消息 -func (client *WebSocketClient) processFailedMessages(failuresChan chan map[string]interface{}) { - for failedMessage := range failuresChan { +func (client *WebSocketClient) processFailedMessages() { + for _, failedMessage := range client.sendFailures { + // 尝试重新发送消息 err := client.SendMessage(failedMessage) if err != nil { - // 处理重发失败的情况,比如重新放入 channel 或记录日志 - mylog.Println("Error re-sending message:", err) + mylog.Printf("Error resending message: %v\n", err) } } + // 清空失败消息列表 + client.sendFailures = []map[string]interface{}{} } // 处理信息,调用腾讯api -func (c *WebSocketClient) recvMessage(msg []byte) { +func (client *WebSocketClient) recvMessage(msg []byte) { var message callapi.ActionMessage //mylog.Println("Received from onebotv11 server raw:", string(msg)) err := json.Unmarshal(msg, &message) @@ -128,7 +188,7 @@ func (c *WebSocketClient) recvMessage(msg []byte) { } mylog.Println("Received from onebotv11 server:", TruncateMessage(message, 800)) // 调用callapi - callapi.CallAPIFromDict(c, c.api, c.apiv2, message) + callapi.CallAPIFromDict(client, client.api, client.apiv2, message) } // 截断信息 @@ -148,7 +208,7 @@ func TruncateMessage(message callapi.ActionMessage, maxLength int) string { } // 发送心跳包 -func (c *WebSocketClient) sendHeartbeat(ctx context.Context, botID uint64, heartbeatinterval int) { +func (client *WebSocketClient) sendHeartbeat(ctx context.Context, botID uint64, heartbeatinterval int) { for { select { case <-ctx.Done(): @@ -179,7 +239,9 @@ func (c *WebSocketClient) sendHeartbeat(ctx context.Context, botID uint64, heart }, "interval": 10000, // 以毫秒为单位 } - c.SendMessage(message) + client.SendMessage(message) + // 重发失败的消息 + client.processFailedMessages() } } } @@ -244,7 +306,7 @@ func NewWebSocketClient(urlStr string, botID uint64, api openapi.OpenAPI, apiv2 apiv2: apiv2, botID: botID, urlStr: urlStr, - sendFailures: make(chan map[string]interface{}, 100), + sendFailures: []map[string]interface{}{}, } // Sending initial message similar to your setupB function From 351c835cdd5843e469a9b418518bc40c333c0f84 Mon Sep 17 00:00:00 2001 From: cosmo Date: Mon, 15 Jan 2024 18:49:01 +0800 Subject: [PATCH 09/13] beta130 --- handlers/message_parser.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/handlers/message_parser.go b/handlers/message_parser.go index 48e3519a..e53351d2 100644 --- a/handlers/message_parser.go +++ b/handlers/message_parser.go @@ -119,7 +119,8 @@ func parseMessageContent(paramsMessage callapi.ParamsContent, message callapi.Ac segmentContent = "[CQ:at,qq=" + qqNumber + "]" case "markdown": mdContent, _ := segmentMap["data"].(map[string]interface{})["data"].(string) - segmentContent = "[CQ:markdown,data=" + mdContent + "]" + encoded := "base64://" + base64.StdEncoding.EncodeToString([]byte(mdContent)) + segmentContent = "[CQ:markdown,data=" + encoded + "]" } messageText += segmentContent @@ -145,7 +146,8 @@ func parseMessageContent(paramsMessage callapi.ParamsContent, message callapi.Ac messageText = "[CQ:at,qq=" + qqNumber + "]" case "markdown": mdContent, _ := message["data"].(map[string]interface{})["data"].(string) - messageText = "[CQ:markdown,data=" + mdContent + "]" + encoded := "base64://" + base64.StdEncoding.EncodeToString([]byte(mdContent)) + messageText = "[CQ:markdown,data=" + encoded + "]" } default: mylog.Println("Unsupported message format: params.message field is not a string, map or slice") From 639ef6b8b6514365a7fe62cfb639b7a540f69d51 Mon Sep 17 00:00:00 2001 From: cosmo Date: Mon, 15 Jan 2024 20:28:55 +0800 Subject: [PATCH 10/13] beta131 --- handlers/message_parser.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/handlers/message_parser.go b/handlers/message_parser.go index e53351d2..8f767b1a 100644 --- a/handlers/message_parser.go +++ b/handlers/message_parser.go @@ -118,8 +118,12 @@ func parseMessageContent(paramsMessage callapi.ParamsContent, message callapi.Ac qqNumber, _ := segmentMap["data"].(map[string]interface{})["qq"].(string) segmentContent = "[CQ:at,qq=" + qqNumber + "]" case "markdown": - mdContent, _ := segmentMap["data"].(map[string]interface{})["data"].(string) - encoded := "base64://" + base64.StdEncoding.EncodeToString([]byte(mdContent)) + mdContentMap, _ := segmentMap["data"].(map[string]interface{})["data"].(map[string]interface{}) + mdContentBytes, err := json.Marshal(mdContentMap) + if err != nil { + fmt.Println("Error marshaling mdContentMap to JSON:", err) + } + encoded := base64.StdEncoding.EncodeToString(mdContentBytes) segmentContent = "[CQ:markdown,data=" + encoded + "]" } @@ -145,8 +149,13 @@ func parseMessageContent(paramsMessage callapi.ParamsContent, message callapi.Ac qqNumber, _ := message["data"].(map[string]interface{})["qq"].(string) messageText = "[CQ:at,qq=" + qqNumber + "]" case "markdown": - mdContent, _ := message["data"].(map[string]interface{})["data"].(string) - encoded := "base64://" + base64.StdEncoding.EncodeToString([]byte(mdContent)) + mdContentMap, _ := message["data"].(map[string]interface{})["data"].(map[string]interface{}) + mdContentBytes, err := json.Marshal(mdContentMap) + if err != nil { + fmt.Println("Error marshaling mdContentMap to JSON:", err) + + } + encoded := "base64://" + base64.StdEncoding.EncodeToString(mdContentBytes) messageText = "[CQ:markdown,data=" + encoded + "]" } default: From 5c4e887bb18a9bc1bdc2276bff15b86e8b3f7652 Mon Sep 17 00:00:00 2001 From: cosmo Date: Tue, 16 Jan 2024 10:10:10 +0800 Subject: [PATCH 11/13] beta132 --- handlers/message_parser.go | 87 ++++++++++++++++++++++++++++++++------ handlers/send_group_msg.go | 15 ++++++- 2 files changed, 88 insertions(+), 14 deletions(-) diff --git a/handlers/message_parser.go b/handlers/message_parser.go index 8f767b1a..deafacf4 100644 --- a/handlers/message_parser.go +++ b/handlers/message_parser.go @@ -118,13 +118,44 @@ func parseMessageContent(paramsMessage callapi.ParamsContent, message callapi.Ac qqNumber, _ := segmentMap["data"].(map[string]interface{})["qq"].(string) segmentContent = "[CQ:at,qq=" + qqNumber + "]" case "markdown": - mdContentMap, _ := segmentMap["data"].(map[string]interface{})["data"].(map[string]interface{}) - mdContentBytes, err := json.Marshal(mdContentMap) - if err != nil { - fmt.Println("Error marshaling mdContentMap to JSON:", err) + mdContent, ok := segmentMap["data"].(map[string]interface{})["data"] + if ok { + if mdContentMap, isMap := mdContent.(map[string]interface{}); isMap { + // mdContent是map[string]interface{},按map处理 + mdContentBytes, err := json.Marshal(mdContentMap) + if err != nil { + mylog.Printf("Error marshaling mdContentMap to JSON:%v", err) + } + encoded := base64.StdEncoding.EncodeToString(mdContentBytes) + segmentContent = "[CQ:markdown,data=" + encoded + "]" + } else if mdContentStr, isString := mdContent.(string); isString { + // mdContent是string + if strings.HasPrefix(mdContentStr, "base64://") { + // 如果以base64://开头,直接使用 + segmentContent = "[CQ:markdown,data=" + mdContentStr + "]" + } else { + // 处理实体化后的JSON文本 + mdContentStr = strings.ReplaceAll(mdContentStr, "&", "&") + mdContentStr = strings.ReplaceAll(mdContentStr, "[", "[") + mdContentStr = strings.ReplaceAll(mdContentStr, "]", "]") + mdContentStr = strings.ReplaceAll(mdContentStr, ",", ",") + + // 将处理过的字符串视为JSON对象,进行序列化和编码 + var jsonMap map[string]interface{} + if err := json.Unmarshal([]byte(mdContentStr), &jsonMap); err != nil { + mylog.Printf("Error unmarshaling string to JSON:%v", err) + } + mdContentBytes, err := json.Marshal(jsonMap) + if err != nil { + mylog.Printf("Error marshaling jsonMap to JSON:%v", err) + } + encoded := base64.StdEncoding.EncodeToString(mdContentBytes) + segmentContent = "[CQ:markdown,data=" + encoded + "]" + } + } + } else { + mylog.Printf("Error marshaling markdown segment to interface,contain type but data is nil.") } - encoded := base64.StdEncoding.EncodeToString(mdContentBytes) - segmentContent = "[CQ:markdown,data=" + encoded + "]" } messageText += segmentContent @@ -149,14 +180,44 @@ func parseMessageContent(paramsMessage callapi.ParamsContent, message callapi.Ac qqNumber, _ := message["data"].(map[string]interface{})["qq"].(string) messageText = "[CQ:at,qq=" + qqNumber + "]" case "markdown": - mdContentMap, _ := message["data"].(map[string]interface{})["data"].(map[string]interface{}) - mdContentBytes, err := json.Marshal(mdContentMap) - if err != nil { - fmt.Println("Error marshaling mdContentMap to JSON:", err) - + mdContent, ok := message["data"].(map[string]interface{})["data"] + if ok { + if mdContentMap, isMap := mdContent.(map[string]interface{}); isMap { + // mdContent是map[string]interface{},按map处理 + mdContentBytes, err := json.Marshal(mdContentMap) + if err != nil { + mylog.Printf("Error marshaling mdContentMap to JSON:%v", err) + } + encoded := base64.StdEncoding.EncodeToString(mdContentBytes) + messageText = "[CQ:markdown,data=" + encoded + "]" + } else if mdContentStr, isString := mdContent.(string); isString { + // mdContent是string + if strings.HasPrefix(mdContentStr, "base64://") { + // 如果以base64://开头,直接使用 + messageText = "[CQ:markdown,data=" + mdContentStr + "]" + } else { + // 处理实体化后的JSON文本 + mdContentStr = strings.ReplaceAll(mdContentStr, "&", "&") + mdContentStr = strings.ReplaceAll(mdContentStr, "[", "[") + mdContentStr = strings.ReplaceAll(mdContentStr, "]", "]") + mdContentStr = strings.ReplaceAll(mdContentStr, ",", ",") + + // 将处理过的字符串视为JSON对象,进行序列化和编码 + var jsonMap map[string]interface{} + if err := json.Unmarshal([]byte(mdContentStr), &jsonMap); err != nil { + mylog.Printf("Error unmarshaling string to JSON:%v", err) + } + mdContentBytes, err := json.Marshal(jsonMap) + if err != nil { + mylog.Printf("Error marshaling jsonMap to JSON:%v", err) + } + encoded := base64.StdEncoding.EncodeToString(mdContentBytes) + messageText = "[CQ:markdown,data=" + encoded + "]" + } + } + } else { + mylog.Printf("Error marshaling markdown segment to interface,contain type but data is nil.") } - encoded := "base64://" + base64.StdEncoding.EncodeToString(mdContentBytes) - messageText = "[CQ:markdown,data=" + encoded + "]" } default: mylog.Println("Unsupported message format: params.message field is not a string, map or slice") diff --git a/handlers/send_group_msg.go b/handlers/send_group_msg.go index 651827d6..e9f66eab 100644 --- a/handlers/send_group_msg.go +++ b/handlers/send_group_msg.go @@ -1059,15 +1059,22 @@ func auto_md(message callapi.ActionMessage, messageText string, richMediaMessage // 判断是否是 GetVisualkPrefixs 数组开头的文本 visualkPrefixs := config.GetVisualkPrefixs() var matchedPrefix *config.VisualPrefixConfig + var isSpecialType bool // 用于标记是否为特殊类型 // 去掉前缀开头的* + // 处理特殊类型前缀 + specialPrefixes := make(map[int]string) for i, vp := range visualkPrefixs { if strings.HasPrefix(vp.Prefix, "*") { + specialPrefixes[i] = vp.Prefix visualkPrefixs[i].Prefix = strings.TrimPrefix(vp.Prefix, "*") } } - for _, vp := range visualkPrefixs { + for i, vp := range visualkPrefixs { if strings.HasPrefix(msg_on_touch, vp.Prefix) { + if _, ok := specialPrefixes[i]; ok { + isSpecialType = true + } if len(msg_on_touch) >= len(vp.Prefix) { if msg_on_touch != " " { transmd = true @@ -1133,6 +1140,12 @@ func auto_md(message callapi.ActionMessage, messageText string, richMediaMessage } else { dataLabel = msg_on_touch } + //当虚拟二级指令是*开头,真实的二级指令 + //以xxxx指令为例 第一次 xxxx 然后 xx攻略 然后 xxxx攻略商店(会失效) + //作用 第一次 xxxx 第二次 xxxx攻略 第三次 xxxx商店 + if isSpecialType && len(msg_on_touch) > len(matchedPrefix.Prefix) { + dataLabel = matchedPrefix.Prefix + whiteLabel + } var actiontype keyboard.ActionType var permission *keyboard.Permission From 894b540a802e0614f625f1778f765e55f47fa7ae Mon Sep 17 00:00:00 2001 From: cosmo Date: Tue, 16 Jan 2024 14:29:55 +0800 Subject: [PATCH 12/13] beta133 --- Processor/Processor.go | 223 ++++++++++++++++++++++++++++++++++++ config/config.go | 52 +++++++++ handlers/send_group_msg.go | 24 +++- template/config_template.go | 4 + 4 files changed, 302 insertions(+), 1 deletion(-) diff --git a/Processor/Processor.go b/Processor/Processor.go index cc104dca..82b36f5b 100644 --- a/Processor/Processor.go +++ b/Processor/Processor.go @@ -23,9 +23,11 @@ import ( "github.com/hoshinonyaruko/gensokyo/echo" "github.com/hoshinonyaruko/gensokyo/handlers" "github.com/hoshinonyaruko/gensokyo/idmap" + "github.com/hoshinonyaruko/gensokyo/images" "github.com/hoshinonyaruko/gensokyo/mylog" "github.com/hoshinonyaruko/gensokyo/wsclient" "github.com/tencent-connect/botgo/dto" + "github.com/tencent-connect/botgo/dto/keyboard" "github.com/tencent-connect/botgo/openapi" ) @@ -552,6 +554,13 @@ func (p *Processors) HandleFrameworkCommand(messageText string, data interface{} mylog.Printf("您没有权限,使用临时指令:%s 忽略权限检查,或将masterid设置为空数组", tempCmd) SendMessage("您没有权限,请配置config.yml或查看日志,使用临时指令", data, Type, p.Api, p.Apiv2) } + + //link指令 + if Type == "group" && strings.HasPrefix(cleanedMessage, config.GetLinkPrefix()) { + md, kb := generateMdByConfig() + SendMessageMd(md, kb, data, Type, p.Api, p.Apiv2) + } + return nil } @@ -789,6 +798,113 @@ func SendMessage(messageText string, data interface{}, messageType string, api o return nil } +// SendMessageMd 发送Md消息根据不同的类型 +func SendMessageMd(md *dto.Markdown, kb *keyboard.MessageKeyboard, data interface{}, messageType string, api openapi.OpenAPI, apiv2 openapi.OpenAPI) error { + // 强制类型转换,获取Message结构 + var msg *dto.Message + switch v := data.(type) { + case *dto.WSGroupATMessageData: + msg = (*dto.Message)(v) + case *dto.WSATMessageData: + msg = (*dto.Message)(v) + case *dto.WSMessageData: + msg = (*dto.Message)(v) + case *dto.WSDirectMessageData: + msg = (*dto.Message)(v) + case *dto.WSC2CMessageData: + msg = (*dto.Message)(v) + default: + return nil + } + switch messageType { + case "guild": + // 处理公会消息 + msgseq := echo.GetMappingSeq(msg.ID) + echo.AddMappingSeq(msg.ID, msgseq+1) + Message := &dto.MessageToCreate{ + Content: "markdown", + MsgID: msg.ID, + MsgSeq: msgseq, + Markdown: md, + Keyboard: kb, + MsgType: 2, //md信息 + } + Message.Timestamp = time.Now().Unix() // 设置时间戳 + if _, err := api.PostMessage(context.TODO(), msg.ChannelID, Message); err != nil { + mylog.Printf("发送文本信息失败: %v", err) + return err + } + + case "group": + // 处理群组消息 + msgseq := echo.GetMappingSeq(msg.ID) + echo.AddMappingSeq(msg.ID, msgseq+1) + Message := &dto.MessageToCreate{ + Content: "markdown", + MsgID: msg.ID, + MsgSeq: msgseq, + Markdown: md, + Keyboard: kb, + MsgType: 2, //md信息 + } + Message.Timestamp = time.Now().Unix() // 设置时间戳 + _, err := apiv2.PostGroupMessage(context.TODO(), msg.GroupID, Message) + if err != nil { + mylog.Printf("发送文本群组信息失败: %v", err) + return err + } + + case "guild_private": + // 处理私信 + timestamp := time.Now().Unix() + timestampStr := fmt.Sprintf("%d", timestamp) + dm := &dto.DirectMessage{ + GuildID: msg.GuildID, + ChannelID: msg.ChannelID, + CreateTime: timestampStr, + } + msgseq := echo.GetMappingSeq(msg.ID) + echo.AddMappingSeq(msg.ID, msgseq+1) + Message := &dto.MessageToCreate{ + Content: "markdown", + MsgID: msg.ID, + MsgSeq: msgseq, + Markdown: md, + Keyboard: kb, + MsgType: 2, //md信息 + } + Message.Timestamp = time.Now().Unix() // 设置时间戳 + if _, err := apiv2.PostDirectMessage(context.TODO(), dm, Message); err != nil { + mylog.Printf("发送文本信息失败: %v", err) + return err + } + + case "group_private": + // 处理群组私聊消息 + msgseq := echo.GetMappingSeq(msg.ID) + echo.AddMappingSeq(msg.ID, msgseq+1) + Message := &dto.MessageToCreate{ + Content: "markdown", + MsgID: msg.ID, + MsgSeq: msgseq, + Markdown: md, + Keyboard: kb, + MsgType: 2, //md信息 + } + Message.Timestamp = time.Now().Unix() // 设置时间戳 + _, err := apiv2.PostC2CMessage(context.TODO(), msg.Author.ID, Message) + if err != nil { + mylog.Printf("发送文本私聊信息失败: %v", err) + return err + } + + default: + return errors.New("未知的消息类型") + } + + return nil +} + // autobind 函数接受 interface{} 类型的数据 // commit by 紫夜 2023-11-19 func (p *Processors) Autobind(data interface{}) error { @@ -922,3 +1038,110 @@ func GenerateAvatarURL(userID int64) (string, error) { // 构建并返回 URL return fmt.Sprintf("http://q%d.qlogo.cn/g?b=qq&nk=%d&s=640", qNumber, userID), nil } + +// 生成link卡片 +func generateMdByConfig() (md *dto.Markdown, kb *keyboard.MessageKeyboard) { + //相关配置获取 + mdtext := config.GetLinkText() + mdtext = "\r" + mdtext + CustomTemplateID := config.GetCustomTemplateID() + linkBots := config.GetLinkBots() + imgURL := config.GetLinkPic() + + //超过16个时候随机显示 + if len(linkBots) > 16 { + linkBots = getRandomSelection(linkBots, 16) + } + + //组合 mdParams + var mdParams []*dto.MarkdownParams + if imgURL != "" { + height, width, err := images.GetImageDimensions(imgURL) + if err != nil { + mylog.Printf("获取图片宽高出错") + } + imgDesc := fmt.Sprintf("图片 #%dpx #%dpx", width, height) + // 创建 MarkdownParams 的实例 + mdParams = []*dto.MarkdownParams{ + {Key: "img_dec", Values: []string{imgDesc}}, + {Key: "img_url", Values: []string{imgURL}}, + {Key: "text_end", Values: []string{mdtext}}, + } + } else { + mdParams = []*dto.MarkdownParams{ + {Key: "text_end", Values: []string{mdtext}}, + } + } + + // 组合模板 Markdown + md = &dto.Markdown{ + CustomTemplateID: CustomTemplateID, + Params: mdParams, + } + + // 创建自定义键盘 + customKeyboard := &keyboard.CustomKeyboard{} + var currentRow *keyboard.Row + var buttonCount int + + for _, bot := range linkBots { + parts := strings.SplitN(bot, "-", 3) + if len(parts) < 3 { + continue // 跳过无效的格式 + } + name := parts[2] + botuin := parts[1] + botappid := parts[0] + boturl := handlers.BuildQQBotShareLink(botuin, botappid) + + button := &keyboard.Button{ + RenderData: &keyboard.RenderData{ + Label: name, + VisitedLabel: name, + Style: 1, // 蓝色边缘 + }, + Action: &keyboard.Action{ + Type: 0, // 链接类型 + Permission: &keyboard.Permission{Type: 2}, // 所有人可操作 + Data: boturl, + UnsupportTips: "请升级新版手机QQ", + }, + } + + // 如果当前行为空或已满(4个按钮),则创建一个新行 + if currentRow == nil || buttonCount == 4 { + currentRow = &keyboard.Row{} + customKeyboard.Rows = append(customKeyboard.Rows, currentRow) + buttonCount = 0 + } + + // 将按钮添加到当前行 + currentRow.Buttons = append(currentRow.Buttons, button) + buttonCount++ + } + + // 创建 MessageKeyboard 并设置其 Content + kb = &keyboard.MessageKeyboard{ + Content: customKeyboard, + } + + return md, kb +} + +func getRandomSelection(slice []string, max int) []string { + if len(slice) <= max { + return slice + } + + selected := make(map[int]bool) + var result []string + for len(result) < max { + index, _ := rand.Int(rand.Reader, big.NewInt(int64(len(slice)))) + idx := int(index.Int64()) + if !selected[idx] { + selected[idx] = true + result = append(result, slice[idx]) + } + } + return result +} diff --git a/config/config.go b/config/config.go index 78ddf764..dfeb04b6 100644 --- a/config/config.go +++ b/config/config.go @@ -136,6 +136,10 @@ type Settings struct { Uin int64 `yaml:"uin"` VwhitePrefixMode bool `yaml:"v_white_prefix_mode"` Enters []string `yaml:"enters"` + LinkPrefix string `yaml:"link_prefix"` + LinkBots []string `yaml:"link_bots"` + LinkText string `yaml:"link_text"` + LinkPic string `yaml:"link_pic"` } // LoadConfig 从文件中加载配置并初始化单例配置 @@ -1711,3 +1715,51 @@ func GetEnters() []string { } return nil // 返回nil,如果instance为nil } + +// 获取 LinkPrefix +func GetLinkPrefix() string { + mu.Lock() + defer mu.Unlock() + + if instance == nil { + mylog.Println("Warning: instance is nil when trying to get LinkPrefix.") + return "" + } + return instance.Settings.LinkPrefix +} + +// 获取 LinkBots 数组 +func GetLinkBots() []string { + mu.Lock() + defer mu.Unlock() + + if instance == nil { + mylog.Println("Warning: instance is nil when trying to get LinkBots.") + return nil + } + return instance.Settings.LinkBots +} + +// 获取 LinkText +func GetLinkText() string { + mu.Lock() + defer mu.Unlock() + + if instance == nil { + mylog.Println("Warning: instance is nil when trying to get LinkText.") + return "" + } + return instance.Settings.LinkText +} + +// 获取 LinkPic +func GetLinkPic() string { + mu.Lock() + defer mu.Unlock() + + if instance == nil { + mylog.Println("Warning: instance is nil when trying to get LinkPic.") + return "" + } + return instance.Settings.LinkPic +} diff --git a/handlers/send_group_msg.go b/handlers/send_group_msg.go index e9f66eab..a2cb87c5 100644 --- a/handlers/send_group_msg.go +++ b/handlers/send_group_msg.go @@ -1147,13 +1147,35 @@ func auto_md(message callapi.ActionMessage, messageText string, richMediaMessage dataLabel = matchedPrefix.Prefix + whiteLabel } + //在虚拟二级指令白名单,设置*前缀,代表不真实添加,仅再来一次 + if strings.HasPrefix(whiteLabel, "*") { + //移除whiteLabel前端的*,*仅作为判断,不作为显示 + whiteLabel = strings.TrimPrefix(whiteLabel, "*") + dataLabel = matchedPrefix.Prefix + } + + //在虚拟二级指令白名单,设置&前缀,代表仅触发其本身 + //如果&前缀指令包含了空格 则只显示空格右侧的文本 + if strings.HasPrefix(whiteLabel, "&") { + //移除whiteLabel前端的*,*仅作为判断,不作为显示 + whiteLabel = strings.TrimPrefix(whiteLabel, "&") + //这里是实际填充到data的 + dataLabel = whiteLabel + // 找到最后一个空格的位置 显示空格右边的文本 没有找到空格则不变 + lastSpaceIndex := strings.LastIndex(whiteLabel, " ") + if lastSpaceIndex != -1 && lastSpaceIndex < len(whiteLabel)-1 { + // 获取空格右侧的子字符串 + whiteLabel = whiteLabel[lastSpaceIndex+1:] + } + } + var actiontype keyboard.ActionType var permission *keyboard.Permission var actiondata string //检查是否设置了enter数组 enter := checkDataLabelPrefix(dataLabel) switch whiteLabel { - case "邀请机器人": + case "邀请机器人", "邀请我", "添加我": botuin := config.GetUinStr() botappid := config.GetAppIDStr() boturl := BuildQQBotShareLink(botuin, botappid) diff --git a/template/config_template.go b/template/config_template.go index cda74bc4..d41bbde2 100644 --- a/template/config_template.go +++ b/template/config_template.go @@ -144,6 +144,10 @@ settings: bind_prefix : "/bind" #需设置 #增强配置项 master_id 可触发 me_prefix : "/me" #需设置 #增强配置项 master_id 可触发 unlock_prefix : "/unlock" #频道私信卡住了? gsk可以帮到你 在任意子频道发送unlock 你会收到来自机器人的频道私信 + link_prefix : "/link" #友情链接配置 配置custom_template_id后可用(https://www.yuque.com/km57bt/hlhnxg/tzbr84y59dbz6pib) + link_bots : ["",""] #发送友情链接时 下方按钮携带的机器人 格式 "appid-qq-name","appid-qq-name" + link_text : "" #友情链接文本 不可为空! + link_pic : "" #友情链接图片 可为空 需url图片 可带端口 不填可能会有显示错误 #穿透\cos\oss类配置(可选!) frp_port : "0" #不使用请保持为0,frp的端口,frp有内外端口,请在frp软件设置gensokyo的port,并将frp显示的对外端口填入这里 From 766fa060ca302ff47f98937fa2e9a256ab1516a2 Mon Sep 17 00:00:00 2001 From: cosmo Date: Wed, 17 Jan 2024 10:44:02 +0800 Subject: [PATCH 13/13] beta134 --- handlers/message_parser.go | 20 +++++++++++++++++--- handlers/send_group_msg.go | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/handlers/message_parser.go b/handlers/message_parser.go index deafacf4..f927e85e 100644 --- a/handlers/message_parser.go +++ b/handlers/message_parser.go @@ -552,6 +552,7 @@ func RevertTransformedText(data interface{}, msgtype string, api openapi.OpenAPI } } + //从当前信息去掉虚拟前缀(因为是虚拟的),不会实际发给应用端 for i, vp := range visualkPrefixs { if strings.HasPrefix(messageText, vp.Prefix) { if _, ok := specialPrefixes[i]; ok { @@ -560,8 +561,8 @@ func RevertTransformedText(data interface{}, msgtype string, api openapi.OpenAPI } // 检查 messageText 的长度是否大于 prefix 的长度 if len(messageText) > len(vp.Prefix) { - // 移除找到的前缀 且messageText不为空格 - if messageText != " " { + // 移除找到的前缀 且messageText不为空 + if messageText != "" { messageText = strings.TrimPrefix(messageText, vp.Prefix) messageText = strings.TrimSpace(messageText) matchedPrefix = &vp @@ -618,7 +619,20 @@ func RevertTransformedText(data interface{}, msgtype string, api openapi.OpenAPI // 遍历白名单数组,检查是否有匹配项 for _, prefix := range allPrefixes { - if strings.HasPrefix(messageText, prefix) { + trimmedPrefix := prefix + if strings.HasPrefix(prefix, "*") { + // 如果前缀以 * 开头,则移除 * + trimmedPrefix = strings.TrimPrefix(prefix, "*") + } else if strings.HasPrefix(prefix, "&") { + // 如果前缀以 & 开头,则移除 & 并从 trimmedPrefix 前端去除 matchedPrefix.Prefix + trimmedPrefix = strings.TrimPrefix(prefix, "&") + trimmedPrefix = strings.TrimPrefix(trimmedPrefix, matchedPrefix.Prefix) + } + + // 从trimmedPrefix中去除前后空格(可能会有bug) + trimmedPrefix = strings.TrimSpace(trimmedPrefix) + + if strings.HasPrefix(messageText, trimmedPrefix) { matched = true break } diff --git a/handlers/send_group_msg.go b/handlers/send_group_msg.go index a2cb87c5..3a39d88d 100644 --- a/handlers/send_group_msg.go +++ b/handlers/send_group_msg.go @@ -1076,7 +1076,7 @@ func auto_md(message callapi.ActionMessage, messageText string, richMediaMessage isSpecialType = true } if len(msg_on_touch) >= len(vp.Prefix) { - if msg_on_touch != " " { + if msg_on_touch != "" { transmd = true matchedPrefix = &vp break // 匹配到了