diff --git a/botgo/dto/message_create.go b/botgo/dto/message_create.go index 0107c976..554fabea 100644 --- a/botgo/dto/message_create.go +++ b/botgo/dto/message_create.go @@ -117,3 +117,70 @@ type SettingGuide struct { // 频道ID, 当通过私信发送设置引导消息时,需要指定guild_id GuildID string `json:"guild_id"` } + +// 仅供测试 + +type MessageSSE struct { + MsgType int `json:"msg_type,omitempty"` + Markdown *MarkdownSSE `json:"markdown,omitempty"` + MsgID string `json:"msg_id,omitempty"` + MsgSeq int `json:"msg_seq,omitempty"` + Stream *StreamSSE `json:"stream,omitempty"` + PromptKeyboard *KeyboardSSE `json:"prompt_keyboard,omitempty"` + ActionButton *ActionButtonSSE `json:"action_button,omitempty"` +} + +// GetEventID 事件ID +func (msg MessageSSE) GetEventID() string { + return "" +} + +// GetSendType 消息类型 +func (msg MessageSSE) GetSendType() SendType { + return 1 +} + +type MarkdownSSE struct { + Content string `json:"content"` +} + +type StreamSSE struct { + State int `json:"state"` + Index int `json:"index"` + ID string `json:"id,omitempty"` +} + +type KeyboardSSE struct { + KeyboardContentSSE `json:"keyboard"` +} + +type KeyboardContentSSE struct { + Content ContentSSE `json:"content"` +} + +type ContentSSE struct { + Rows []RowSSE `json:"rows"` +} + +type RowSSE struct { + Buttons []ButtonSSE `json:"buttons"` +} + +type ButtonSSE struct { + RenderData RenderDataSSE `json:"render_data"` + Action ActionSSE `json:"action"` +} + +type RenderDataSSE struct { + Label string `json:"label"` + Style int `json:"style"` +} + +type ActionSSE struct { + Type int `json:"type"` +} + +type ActionButtonSSE struct { + TemplateID int `json:"template_id"` + CallbackData string `json:"callback_data"` +} diff --git a/botgo/openapi/iface.go b/botgo/openapi/iface.go index 58bd24ec..7e843809 100644 --- a/botgo/openapi/iface.go +++ b/botgo/openapi/iface.go @@ -83,6 +83,8 @@ type MessageAPI interface { PostGroupMessage(ctx context.Context, groupID string, msg dto.APIMessage) (*dto.GroupMessageResponse, error) // PostC2CMessage 发送C2C消息 PostC2CMessage(ctx context.Context, userID string, msg dto.APIMessage) (*dto.C2CMessageResponse, error) + // PostC2CMessage 发送C2CSSE消息 + PostC2CMessageSSE(ctx context.Context, userID string, msg dto.APIMessage) (*dto.C2CMessageResponse, error) } // GuildAPI guild 相关接口 diff --git a/botgo/openapi/v1/message.go b/botgo/openapi/v1/message.go index 4285a823..6f77ab1a 100644 --- a/botgo/openapi/v1/message.go +++ b/botgo/openapi/v1/message.go @@ -300,3 +300,24 @@ func (o *openAPI) PostC2CMessage(ctx context.Context, userID string, msg dto.API return result, nil } + +// PostC2CMessage 回复C2CSSE消息 +func (o *openAPI) PostC2CMessageSSE(ctx context.Context, userID string, msg dto.APIMessage) (*dto.C2CMessageResponse, error) { + var resp *resty.Response + var err error + + resp, err = o.request(ctx). + SetResult(dto.Message{}). // 设置为消息类型 + SetPathParam("user_id", userID). + SetBody(msg). + Post(o.getURL("/v2/users/{user_id}/messages")) + + if err != nil { + return nil, err + } + + result := &dto.C2CMessageResponse{} + result.Message = resp.Result().(*dto.Message) + + return result, nil +} diff --git a/botgo/openapi/v2/message.go b/botgo/openapi/v2/message.go index d3e23707..7a29802c 100644 --- a/botgo/openapi/v2/message.go +++ b/botgo/openapi/v2/message.go @@ -315,3 +315,24 @@ func (o *openAPIv2) PostC2CMessage(ctx context.Context, userID string, msg dto.A return result, nil } + +// PostC2CMessage 回复C2CSSE消息 +func (o *openAPIv2) PostC2CMessageSSE(ctx context.Context, userID string, msg dto.APIMessage) (*dto.C2CMessageResponse, error) { + var resp *resty.Response + var err error + + resp, err = o.request(ctx). + SetResult(dto.Message{}). // 设置为消息类型 + SetPathParam("user_id", userID). + SetBody(msg). + Post(o.getURL("/v2/users/{user_id}/messages")) + + if err != nil { + return nil, err + } + + result := &dto.C2CMessageResponse{} + result.Message = resp.Result().(*dto.Message) + + return result, nil +} diff --git a/handlers/send_private_msg_sse.go b/handlers/send_private_msg_sse.go new file mode 100644 index 00000000..602ba965 --- /dev/null +++ b/handlers/send_private_msg_sse.go @@ -0,0 +1,229 @@ +package handlers + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/hoshinonyaruko/gensokyo/callapi" + "github.com/hoshinonyaruko/gensokyo/config" + "github.com/hoshinonyaruko/gensokyo/echo" + "github.com/hoshinonyaruko/gensokyo/idmap" + "github.com/hoshinonyaruko/gensokyo/mylog" + "github.com/tencent-connect/botgo/dto" + "github.com/tencent-connect/botgo/openapi" +) + +var msgIDToIndex = make(map[string]int) +var msgIDToRelatedID = make(map[string]string) + +func init() { + callapi.RegisterHandler("send_private_msg_sse", HandleSendPrivateMsgSSE) +} + +type InterfaceBody struct { + Content string `json:"content"` + State int `json:"state"` + PromptKeyboard []string `json:"prompt_keyboard,omitempty"` + ActionButton int `json:"action_button,omitempty"` + CallbackData string `json:"callback_data,omitempty"` +} + +func incrementIndex(msgID string) int { + if _, exists := msgIDToIndex[msgID]; !exists { + msgIDToIndex[msgID] = 0 // 初始化为0 + return 0 + } + msgIDToIndex[msgID]++ // 递增Index + return msgIDToIndex[msgID] +} + +// GetRelatedID 根据MessageID获取相关的ID +func GetRelatedID(MessageID string) string { + if relatedID, exists := msgIDToRelatedID[MessageID]; exists { + return relatedID + } + // 如果没有找到转换关系,返回空字符串 + return "" +} + +// UpdateRelatedID 更新MessageID到respID的映射关系 +func UpdateRelatedID(MessageID, ID string) { + msgIDToRelatedID[MessageID] = ID +} + +func HandleSendPrivateMsgSSE(client callapi.Client, api openapi.OpenAPI, apiv2 openapi.OpenAPI, message callapi.ActionMessage) (string, error) { + // 使用 message.Echo 作为key来获取消息类型 + var retmsg string + + // 检查UserID是否为0 + checkZeroUserID := func(id interface{}) bool { + switch v := id.(type) { + case int: + return v != 0 + case int64: + return v != 0 + case string: + return v != "0" // 同样检查字符串形式的0 + default: + return true // 如果不是int、int64或string,假定它不为0 + } + } + + // New checks for UserID and GroupID being nil or 0 + if message.Params.UserID == nil || !checkZeroUserID(message.Params.UserID) { + mylog.Printf("send_group_msg_sse接收到错误action: %v", message) + return "", nil + } + + var err error + + var resp *dto.C2CMessageResponse + + //私聊信息 + var UserID string + if config.GetIdmapPro() { + //还原真实的userid + //mylog.Printf("group_private:%v", message.Params.UserID.(string)) + _, UserID, err = idmap.RetrieveRowByIDv2Pro("690426430", message.Params.UserID.(string)) + if err != nil { + mylog.Printf("Error reading config: %v", err) + return "", nil + } + mylog.Printf("测试,通过Proid获取的UserID:%v", UserID) + } else { + //还原真实的userid + UserID, err = idmap.RetrieveRowByIDv2(message.Params.UserID.(string)) + if err != nil { + mylog.Printf("Error reading config: %v", err) + return "", nil + } + } + + // 首先,将message.Params.Message序列化成JSON字符串 + messageJSON, err := json.Marshal(message.Params.Message) + if err != nil { + fmt.Printf("Error marshalling message: %v\n", err) + return "", nil + } + + // 然后,将这个JSON字符串反序列化到InterfaceBody类型的对象中 + var messageBody InterfaceBody + err = json.Unmarshal(messageJSON, &messageBody) + if err != nil { + fmt.Printf("Error unmarshalling to InterfaceBody: %v\n", err) + return "", nil + } + + // 输出反序列化后的对象,确认是否成功转换 + fmt.Printf("Recovered InterfaceBody: %+v\n", messageBody) + // 使用 echo 获取消息ID + var messageID string + if config.GetLazyMessageId() { + //由于实现了Params的自定义unmarshell 所以可以类型安全的断言为string + messageID = echo.GetLazyMessagesId(UserID) + mylog.Printf("GetLazyMessagesId: %v", messageID) + } + if messageID == "" { + if echoStr, ok := message.Echo.(string); ok { + messageID = echo.GetMsgIDByKey(echoStr) + mylog.Println("echo取私聊发信息对应的message_id:", messageID) + } + } + // 如果messageID仍然为空,尝试使用config.GetAppID和UserID的组合来获取messageID + // 如果messageID为空,通过函数获取 + if messageID == "" { + messageID = GetMessageIDByUseridOrGroupid(config.GetAppIDStr(), UserID) + mylog.Println("通过GetMessageIDByUserid函数获取的message_id:", messageID) + } + if messageID == "2000" { + messageID = "" + mylog.Println("通过lazymsgid发送群私聊主动信息,每月可发送1次") + } + + // 获取并打印相关ID + relatedID := GetRelatedID(messageID) + fmt.Println("相关ID:", relatedID) + dtoSSE := generateMessageSSE(messageBody, messageID, relatedID) + + mylog.Printf("私聊发信息sse:%v", dtoSSE) + + resp, err = apiv2.PostC2CMessageSSE(context.TODO(), UserID, dtoSSE) + if err != nil { + mylog.Printf("发送文本私聊信息失败: %v", err) + //如果失败 防止进入递归 + return "", nil + } + + // 更新或刷新映射关系 + UpdateRelatedID(messageID, resp.Message.ID) + + //发送成功回执 + retmsg, _ = SendC2CResponse(client, err, &message, resp) + + return retmsg, nil +} + +func generateMessageSSE(body InterfaceBody, msgID, ID string) *dto.MessageSSE { + index := incrementIndex(msgID) // 获取并递增Index + + // 将InterfaceBody的PromptKeyboard转换为MessageSSE的结构 + var rows []dto.RowSSE + for _, label := range body.PromptKeyboard { + row := dto.RowSSE{ + Buttons: []dto.ButtonSSE{ + { + RenderData: dto.RenderDataSSE{Label: label, Style: 2}, + Action: dto.ActionSSE{Type: 2}, + }, + }, + } + rows = append(rows, row) + } + + var msgsse dto.MessageSSE + + if body.Content != "" { + // 确保Markdown已经初始化 + msgsse.Markdown = &dto.MarkdownSSE{} + msgsse.Markdown.Content = body.Content + } + + if len(rows) > 0 { + // 确保PromptKeyboard及其嵌套结构已经初始化 + msgsse.PromptKeyboard = &dto.KeyboardSSE{ + KeyboardContentSSE: dto.KeyboardContentSSE{ + Content: dto.ContentSSE{ + Rows: []dto.RowSSE{}, // 初始化空切片,避免nil切片赋值 + }, + }, + } + msgsse.PromptKeyboard.KeyboardContentSSE.Content.Rows = rows + } + + // 剩余字段赋值 + msgsse.MsgType = 2 + msgsse.MsgSeq = index + 3 + msgsse.Stream = &dto.StreamSSE{ + State: body.State, + Index: index, + } + + if ID != "" { + msgsse.Stream.ID = ID + } + if msgID != "" { + msgsse.MsgID = msgID + } + + // 初始化ActionButtonSSE,如果CallbackData有值 + if body.CallbackData != "" { + msgsse.ActionButton = &dto.ActionButtonSSE{ + TemplateID: body.ActionButton, + CallbackData: body.CallbackData, + } + } + + return &msgsse + +} diff --git a/httpapi/httpapi.go b/httpapi/httpapi.go index 5f95d4f2..e3dad08c 100644 --- a/httpapi/httpapi.go +++ b/httpapi/httpapi.go @@ -38,6 +38,10 @@ func CombinedMiddleware(api openapi.OpenAPI, apiV2 openapi.OpenAPI) gin.HandlerF handleSendPrivateMessage(c, api, apiV2) return } + if c.Request.URL.Path == "/send_private_msg_sse" { + handleSendPrivateMessageSSE(c, api, apiV2) + return + } if c.Request.URL.Path == "/send_guild_channel_msg" { handleSendGuildChannelMessage(c, api, apiV2) return @@ -210,6 +214,55 @@ func handleSendPrivateMessage(c *gin.Context, api openapi.OpenAPI, apiV2 openapi c.String(http.StatusOK, retmsg) } +// handleSendPrivateMessageSSE 处理发送私聊SSE消息的请求 +func handleSendPrivateMessageSSE(c *gin.Context, api openapi.OpenAPI, apiV2 openapi.OpenAPI) { + var retmsg string + var req struct { + GroupID int64 `json:"group_id" form:"group_id"` + UserID int64 `json:"user_id" form:"user_id"` + Message interface{} `json:"message" form:"message"` + AutoEscape bool `json:"auto_escape" form:"auto_escape"` + } + + // 根据请求方法解析参数 + if c.Request.Method == http.MethodGet { + // 从URL查询参数解析 + if err := c.ShouldBindQuery(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + } else { + // 从JSON或表单数据解析 + if err := c.ShouldBind(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + } + + // 使用解析后的参数处理请求 + // 例如:api.SendGroupMessage(req.GroupID, req.Message, req.AutoEscape) + client := &HttpAPIClient{} + // 创建 ActionMessage 实例 + message := callapi.ActionMessage{ + Action: "send_private_msg_sse", + Params: callapi.ParamsContent{ + GroupID: strconv.FormatInt(req.GroupID, 10), // 注意这里需要转换类型,因为 GroupID 是 int64 + UserID: strconv.FormatInt(req.UserID, 10), + Message: req.Message, + }, + } + // 调用处理函数 + retmsg, err := handlers.HandleSendPrivateMsgSSE(client, api, apiV2, message) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // 返回处理结果 + c.Header("Content-Type", "application/json") + c.String(http.StatusOK, retmsg) +} + // handleSendGuildChannelMessage 处理发送消频道息的请求 func handleSendGuildChannelMessage(c *gin.Context, api openapi.OpenAPI, apiV2 openapi.OpenAPI) { var retmsg string