- Introduced Weixin integration support with new configuration options for WeChat iLink Bot. - Updated existing adapters (DingTalk, Discord, Feishu, Telegram) to include sender_id and app_id in message metadata for improved context handling. - Enhanced dispatcher logic to accommodate the new Weixin adapter and ensure proper initialization and shutdown processes. - Improved message handling across integrations to support typing indicators, providing a more interactive user experience.
179 lines
5.3 KiB
Go
179 lines
5.3 KiB
Go
package weixin
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"mime/multipart"
|
|
"net/textproto"
|
|
"strings"
|
|
|
|
"github.com/yaoapp/yao/attachment"
|
|
weixinapi "github.com/yaoapp/yao/integrations/weixin"
|
|
)
|
|
|
|
type resolvedMedia struct {
|
|
Wrapper string
|
|
MimeType string
|
|
FileName string
|
|
}
|
|
|
|
func convertMessage(ctx context.Context, bot *weixinapi.Bot, items []weixinapi.MsgItem, groups []string) (string, []resolvedMedia) {
|
|
var textBuf strings.Builder
|
|
var media []resolvedMedia
|
|
|
|
for _, item := range items {
|
|
switch item.Type {
|
|
case weixinapi.ItemTypeText:
|
|
if item.TextItem != nil && item.TextItem.Text != "" {
|
|
text := item.TextItem.Text
|
|
if item.RefMsg != nil {
|
|
text = formatRefMessage(item.RefMsg, text)
|
|
}
|
|
textBuf.WriteString(text)
|
|
}
|
|
case weixinapi.ItemTypeVoice:
|
|
if item.VoiceItem != nil {
|
|
if item.VoiceItem.Text != "" {
|
|
textBuf.WriteString(item.VoiceItem.Text)
|
|
} else if item.VoiceItem.Media != nil && item.VoiceItem.Media.EncryptQueryParam != "" {
|
|
m := resolveVoice(ctx, bot, item.VoiceItem, groups)
|
|
if m != nil {
|
|
media = append(media, *m)
|
|
}
|
|
}
|
|
}
|
|
case weixinapi.ItemTypeImage:
|
|
if item.ImageItem != nil {
|
|
m := resolveImage(ctx, bot, item.ImageItem, groups)
|
|
if m != nil {
|
|
media = append(media, *m)
|
|
}
|
|
}
|
|
case weixinapi.ItemTypeFile:
|
|
if item.FileItem != nil && item.FileItem.Media != nil && item.FileItem.Media.EncryptQueryParam != "" {
|
|
m := resolveFile(ctx, bot, item.FileItem, groups)
|
|
if m != nil {
|
|
media = append(media, *m)
|
|
}
|
|
}
|
|
case weixinapi.ItemTypeVideo:
|
|
if item.VideoItem != nil && item.VideoItem.Media != nil && item.VideoItem.Media.EncryptQueryParam != "" {
|
|
m := resolveVideo(ctx, bot, item.VideoItem, groups)
|
|
if m != nil {
|
|
media = append(media, *m)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return textBuf.String(), media
|
|
}
|
|
|
|
func formatRefMessage(ref *weixinapi.RefMessage, text string) string {
|
|
if ref == nil || ref.MessageItem == nil {
|
|
return text
|
|
}
|
|
var refBody string
|
|
if ref.MessageItem.TextItem != nil {
|
|
refBody = ref.MessageItem.TextItem.Text
|
|
}
|
|
title := ref.Title
|
|
if title == "" && refBody == "" {
|
|
return text
|
|
}
|
|
return fmt.Sprintf("[引用: %s | %s]\n%s", title, refBody, text)
|
|
}
|
|
|
|
func resolveImage(ctx context.Context, bot *weixinapi.Bot, img *weixinapi.ImageItem, groups []string) *resolvedMedia {
|
|
if img.AesKey != "" && img.Media != nil && img.Media.EncryptQueryParam != "" {
|
|
rawKey, err := hex.DecodeString(img.AesKey)
|
|
if err == nil {
|
|
data, err := weixinapi.DecryptFromRaw(bot.CDNBaseURL(), img.Media.EncryptQueryParam, rawKey)
|
|
if err == nil {
|
|
return storeMedia(ctx, data, "image/jpeg", "image.jpg", groups)
|
|
}
|
|
}
|
|
}
|
|
if img.Media != nil && img.Media.EncryptQueryParam != "" && img.Media.AesKey != "" {
|
|
data, err := weixinapi.DownloadAndDecrypt(bot.CDNBaseURL(), img.Media.EncryptQueryParam, img.Media.AesKey)
|
|
if err == nil {
|
|
return storeMedia(ctx, data, "image/jpeg", "image.jpg", groups)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func resolveVoice(ctx context.Context, bot *weixinapi.Bot, voice *weixinapi.VoiceItem, groups []string) *resolvedMedia {
|
|
data, err := weixinapi.DownloadAndDecrypt(bot.CDNBaseURL(), voice.Media.EncryptQueryParam, voice.Media.AesKey)
|
|
if err != nil {
|
|
log.Error("weixin: voice decrypt failed: %v", err)
|
|
return nil
|
|
}
|
|
mime := "audio/mpeg"
|
|
ext := "mp3"
|
|
if voice.EncodeType == 6 {
|
|
mime = "audio/silk"
|
|
ext = "silk"
|
|
}
|
|
return storeMedia(ctx, data, mime, "voice."+ext, groups)
|
|
}
|
|
|
|
func resolveFile(ctx context.Context, bot *weixinapi.Bot, file *weixinapi.FileItem, groups []string) *resolvedMedia {
|
|
data, err := weixinapi.DownloadAndDecrypt(bot.CDNBaseURL(), file.Media.EncryptQueryParam, file.Media.AesKey)
|
|
if err != nil {
|
|
log.Error("weixin: file decrypt failed: %v", err)
|
|
return nil
|
|
}
|
|
filename := file.FileName
|
|
if filename == "" {
|
|
filename = "file.bin"
|
|
}
|
|
mime := weixinapi.MimeFromFilename(filename)
|
|
return storeMedia(ctx, data, mime, filename, groups)
|
|
}
|
|
|
|
func resolveVideo(ctx context.Context, bot *weixinapi.Bot, video *weixinapi.VideoItem, groups []string) *resolvedMedia {
|
|
data, err := weixinapi.DownloadAndDecrypt(bot.CDNBaseURL(), video.Media.EncryptQueryParam, video.Media.AesKey)
|
|
if err != nil {
|
|
log.Error("weixin: video decrypt failed: %v", err)
|
|
return nil
|
|
}
|
|
return storeMedia(ctx, data, "video/mp4", "video.mp4", groups)
|
|
}
|
|
|
|
func storeMedia(ctx context.Context, data []byte, mimeType, filename string, groups []string) *resolvedMedia {
|
|
manager, exists := attachment.Managers["__yao.attachment"]
|
|
if !exists {
|
|
log.Error("weixin: __yao.attachment manager not found")
|
|
return nil
|
|
}
|
|
|
|
fh := makeFileHeader(filename, mimeType, int64(len(data)))
|
|
reader := bytes.NewReader(data)
|
|
file, err := manager.Upload(ctx, fh, reader, attachment.UploadOption{Groups: groups})
|
|
if err != nil {
|
|
log.Error("weixin: attachment upload failed: %v", err)
|
|
return nil
|
|
}
|
|
|
|
return &resolvedMedia{
|
|
Wrapper: fmt.Sprintf("__yao.attachment://%s", file.ID),
|
|
MimeType: mimeType,
|
|
FileName: filename,
|
|
}
|
|
}
|
|
|
|
func makeFileHeader(filename, contentType string, size int64) *attachment.FileHeader {
|
|
hdr := make(textproto.MIMEHeader)
|
|
hdr.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, filename))
|
|
hdr.Set("Content-Type", contentType)
|
|
return &attachment.FileHeader{
|
|
FileHeader: &multipart.FileHeader{
|
|
Filename: filename,
|
|
Header: hdr,
|
|
Size: size,
|
|
},
|
|
}
|
|
}
|