mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
339 lines
9.7 KiB
Go
339 lines
9.7 KiB
Go
package wecom
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/memohai/memoh/internal/channel"
|
|
)
|
|
|
|
func (a *WeComAdapter) handleFrame(ctx context.Context, cfg channel.ChannelConfig, frame WSFrame, handler channel.InboundHandler) error {
|
|
switch frame.Cmd {
|
|
case WSCmdMsgCallback:
|
|
if handler == nil {
|
|
return nil
|
|
}
|
|
var body MessageCallbackBody
|
|
if err := frame.DecodeBody(&body); err != nil {
|
|
return err
|
|
}
|
|
msg, ok := buildInboundMessage(body, frame.Headers.ReqID)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
a.rememberCallback(body.MsgID, frame.Headers.ReqID, body.ResponseURL, body.ChatID, body.From.UserID)
|
|
return handler(ctx, cfg, msg)
|
|
case WSCmdEventCallback:
|
|
if handler == nil {
|
|
return nil
|
|
}
|
|
var body EventCallbackBody
|
|
if err := frame.DecodeBody(&body); err != nil {
|
|
return err
|
|
}
|
|
msg, ok := buildInboundEventMessage(body, frame.Headers.ReqID)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
a.rememberCallback(body.MsgID, frame.Headers.ReqID, body.ResponseURL, body.ChatID, body.From.UserID)
|
|
return handler(ctx, cfg, msg)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func buildInboundMessage(body MessageCallbackBody, reqID string) (channel.InboundMessage, bool) {
|
|
text, attachments := extractBodyContent(body)
|
|
if strings.TrimSpace(text) == "" && len(attachments) == 0 {
|
|
return channel.InboundMessage{}, false
|
|
}
|
|
target := resolveDeliveryTarget(body.ChatID, body.From.UserID)
|
|
if target == "" {
|
|
return channel.InboundMessage{}, false
|
|
}
|
|
convType := normalizeConversationType(body.ChatType)
|
|
convID := strings.TrimSpace(body.ChatID)
|
|
if convID == "" {
|
|
convID = strings.TrimSpace(body.From.UserID)
|
|
}
|
|
msg := channel.InboundMessage{
|
|
Channel: Type,
|
|
Message: channel.Message{
|
|
ID: strings.TrimSpace(body.MsgID),
|
|
Format: channel.MessageFormatPlain,
|
|
Text: strings.TrimSpace(text),
|
|
Attachments: attachments,
|
|
Metadata: map[string]any{
|
|
"response_url": strings.TrimSpace(body.ResponseURL),
|
|
},
|
|
},
|
|
ReplyTarget: target,
|
|
Sender: channel.Identity{
|
|
SubjectID: strings.TrimSpace(body.From.UserID),
|
|
Attributes: map[string]string{
|
|
"user_id": strings.TrimSpace(body.From.UserID),
|
|
"chat_id": strings.TrimSpace(body.ChatID),
|
|
},
|
|
},
|
|
Conversation: channel.Conversation{
|
|
ID: convID,
|
|
Type: convType,
|
|
},
|
|
ReceivedAt: parseCreateTime(body.CreateTime),
|
|
Source: "wecom",
|
|
Metadata: map[string]any{
|
|
"req_id": strings.TrimSpace(reqID),
|
|
"chat_id": strings.TrimSpace(body.ChatID),
|
|
"chat_type": strings.TrimSpace(body.ChatType),
|
|
"response_url": strings.TrimSpace(body.ResponseURL),
|
|
},
|
|
}
|
|
return msg, true
|
|
}
|
|
|
|
func buildInboundEventMessage(body EventCallbackBody, reqID string) (channel.InboundMessage, bool) {
|
|
eventType := normalizeEventType(body.Event.EventType, body.Event.EventType2)
|
|
if eventType == "" {
|
|
return channel.InboundMessage{}, false
|
|
}
|
|
target := resolveDeliveryTarget(body.ChatID, body.From.UserID)
|
|
if target == "" {
|
|
return channel.InboundMessage{}, false
|
|
}
|
|
convType := normalizeConversationType(body.ChatType)
|
|
convID := strings.TrimSpace(body.ChatID)
|
|
if convID == "" {
|
|
convID = strings.TrimSpace(body.From.UserID)
|
|
}
|
|
return channel.InboundMessage{
|
|
Channel: Type,
|
|
Message: channel.Message{
|
|
ID: strings.TrimSpace(body.MsgID),
|
|
Format: channel.MessageFormatPlain,
|
|
Text: eventType,
|
|
Metadata: map[string]any{
|
|
"event_type": eventType,
|
|
"task_id": strings.TrimSpace(body.Event.TaskID),
|
|
"event_key": strings.TrimSpace(body.Event.EventKey),
|
|
"event_code": strings.TrimSpace(body.Event.Code),
|
|
"event_reason": strings.TrimSpace(body.Event.Reason),
|
|
"task_status": strings.TrimSpace(body.Task.TaskStatus),
|
|
"response_url": strings.TrimSpace(body.ResponseURL),
|
|
},
|
|
},
|
|
ReplyTarget: target,
|
|
Sender: channel.Identity{
|
|
SubjectID: strings.TrimSpace(body.From.UserID),
|
|
Attributes: map[string]string{
|
|
"user_id": strings.TrimSpace(body.From.UserID),
|
|
"chat_id": strings.TrimSpace(body.ChatID),
|
|
},
|
|
},
|
|
Conversation: channel.Conversation{
|
|
ID: convID,
|
|
Type: convType,
|
|
},
|
|
ReceivedAt: parseCreateTime(body.CreateTime),
|
|
Source: "wecom",
|
|
Metadata: map[string]any{
|
|
"is_event": true,
|
|
"event_type": eventType,
|
|
"event_key": strings.TrimSpace(body.Event.EventKey),
|
|
"event_code": strings.TrimSpace(body.Event.Code),
|
|
"event_reason": strings.TrimSpace(body.Event.Reason),
|
|
"req_id": strings.TrimSpace(reqID),
|
|
"response_url": strings.TrimSpace(body.ResponseURL),
|
|
},
|
|
}, true
|
|
}
|
|
|
|
func extractBodyContent(body MessageCallbackBody) (string, []channel.Attachment) {
|
|
switch strings.ToLower(strings.TrimSpace(body.MsgType)) {
|
|
case "text":
|
|
if body.Text == nil {
|
|
return "", nil
|
|
}
|
|
return strings.TrimSpace(body.Text.Content), nil
|
|
case "markdown":
|
|
if body.Markdown == nil {
|
|
return "", nil
|
|
}
|
|
return strings.TrimSpace(body.Markdown.Content), nil
|
|
case "voice":
|
|
if body.Voice == nil {
|
|
return "", nil
|
|
}
|
|
return strings.TrimSpace(body.Voice.Content), nil
|
|
case "video":
|
|
if body.Video == nil {
|
|
return "", nil
|
|
}
|
|
att := channel.Attachment{
|
|
Type: channel.AttachmentVideo,
|
|
URL: strings.TrimSpace(body.Video.URL),
|
|
SourcePlatform: Type.String(),
|
|
Metadata: map[string]any{
|
|
"aeskey": strings.TrimSpace(body.Video.AESKey),
|
|
},
|
|
}
|
|
return "", []channel.Attachment{channel.NormalizeInboundChannelAttachment(att)}
|
|
case "image":
|
|
if body.Image == nil {
|
|
return "", nil
|
|
}
|
|
att := channel.Attachment{
|
|
Type: channel.AttachmentImage,
|
|
URL: strings.TrimSpace(body.Image.URL),
|
|
SourcePlatform: Type.String(),
|
|
Metadata: map[string]any{
|
|
"aeskey": strings.TrimSpace(body.Image.AESKey),
|
|
},
|
|
}
|
|
return "", []channel.Attachment{channel.NormalizeInboundChannelAttachment(att)}
|
|
case "file":
|
|
if body.File == nil {
|
|
return "", nil
|
|
}
|
|
att := channel.Attachment{
|
|
Type: channel.AttachmentFile,
|
|
URL: strings.TrimSpace(body.File.URL),
|
|
SourcePlatform: Type.String(),
|
|
Name: strings.TrimSpace(body.File.FileName),
|
|
Metadata: map[string]any{
|
|
"aeskey": strings.TrimSpace(body.File.AESKey),
|
|
},
|
|
}
|
|
return "", []channel.Attachment{channel.NormalizeInboundChannelAttachment(att)}
|
|
case "mixed":
|
|
var textParts []string
|
|
attachments := make([]channel.Attachment, 0)
|
|
for _, item := range body.Mixed {
|
|
switch strings.ToLower(strings.TrimSpace(item.MsgType)) {
|
|
case "text":
|
|
if item.Text != nil && strings.TrimSpace(item.Text.Content) != "" {
|
|
textParts = append(textParts, strings.TrimSpace(item.Text.Content))
|
|
}
|
|
case "markdown":
|
|
if item.Markdown != nil && strings.TrimSpace(item.Markdown.Content) != "" {
|
|
textParts = append(textParts, strings.TrimSpace(item.Markdown.Content))
|
|
}
|
|
case "image":
|
|
if item.Image == nil {
|
|
continue
|
|
}
|
|
att := channel.Attachment{
|
|
Type: channel.AttachmentImage,
|
|
URL: strings.TrimSpace(item.Image.URL),
|
|
SourcePlatform: Type.String(),
|
|
Metadata: map[string]any{
|
|
"aeskey": strings.TrimSpace(item.Image.AESKey),
|
|
},
|
|
}
|
|
attachments = append(attachments, channel.NormalizeInboundChannelAttachment(att))
|
|
case "file":
|
|
if item.File == nil {
|
|
continue
|
|
}
|
|
att := channel.Attachment{
|
|
Type: channel.AttachmentFile,
|
|
URL: strings.TrimSpace(item.File.URL),
|
|
SourcePlatform: Type.String(),
|
|
Name: strings.TrimSpace(item.File.FileName),
|
|
Metadata: map[string]any{
|
|
"aeskey": strings.TrimSpace(item.File.AESKey),
|
|
},
|
|
}
|
|
attachments = append(attachments, channel.NormalizeInboundChannelAttachment(att))
|
|
case "video":
|
|
if item.Video == nil {
|
|
continue
|
|
}
|
|
att := channel.Attachment{
|
|
Type: channel.AttachmentVideo,
|
|
URL: strings.TrimSpace(item.Video.URL),
|
|
SourcePlatform: Type.String(),
|
|
Metadata: map[string]any{
|
|
"aeskey": strings.TrimSpace(item.Video.AESKey),
|
|
},
|
|
}
|
|
attachments = append(attachments, channel.NormalizeInboundChannelAttachment(att))
|
|
case "voice":
|
|
if item.Voice != nil && strings.TrimSpace(item.Voice.Content) != "" {
|
|
textParts = append(textParts, strings.TrimSpace(item.Voice.Content))
|
|
}
|
|
}
|
|
}
|
|
return strings.Join(textParts, "\n"), attachments
|
|
default:
|
|
return "", nil
|
|
}
|
|
}
|
|
|
|
func resolveDeliveryTarget(chatID, userID string) string {
|
|
if v := strings.TrimSpace(chatID); v != "" {
|
|
return "chat_id:" + v
|
|
}
|
|
if v := strings.TrimSpace(userID); v != "" {
|
|
return "user_id:" + v
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func normalizeConversationType(chatType string) string {
|
|
if strings.EqualFold(strings.TrimSpace(chatType), "group") {
|
|
return channel.ConversationTypeGroup
|
|
}
|
|
return channel.ConversationTypePrivate
|
|
}
|
|
|
|
func parseCreateTime(ts int64) time.Time {
|
|
if t := unixMilliseconds(ts); !t.IsZero() {
|
|
return t
|
|
}
|
|
return time.Now().UTC()
|
|
}
|
|
|
|
func (a *WeComAdapter) rememberCallback(msgID, reqID, responseURL, chatID, userID string) {
|
|
if a == nil || a.cache == nil {
|
|
return
|
|
}
|
|
msgID = strings.TrimSpace(msgID)
|
|
if msgID == "" {
|
|
return
|
|
}
|
|
if strings.TrimSpace(reqID) == "" {
|
|
return
|
|
}
|
|
a.cache.Put(msgID, callbackContext{
|
|
ReqID: strings.TrimSpace(reqID),
|
|
ResponseURL: strings.TrimSpace(responseURL),
|
|
ChatID: strings.TrimSpace(chatID),
|
|
UserID: strings.TrimSpace(userID),
|
|
CreatedAt: time.Now().UTC(),
|
|
})
|
|
}
|
|
|
|
func normalizeEventType(values ...string) string {
|
|
candidates := make([]string, 0, len(values))
|
|
for _, v := range values {
|
|
v = strings.TrimSpace(strings.ToLower(v))
|
|
if v == "" {
|
|
continue
|
|
}
|
|
v = strings.ReplaceAll(v, "-", "_")
|
|
v = strings.ReplaceAll(v, " ", "_")
|
|
candidates = append(candidates, v)
|
|
}
|
|
for _, v := range candidates {
|
|
switch v {
|
|
case "enter_chat", "template_card_event", "feedback_event":
|
|
return v
|
|
}
|
|
}
|
|
if len(candidates) == 0 {
|
|
return ""
|
|
}
|
|
return candidates[0]
|
|
}
|