package main
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"time"
wxproto "github.com/yincongcyincong/weixin-macos/onebot/proto"
"google.golang.org/protobuf/encoding/protowire"
"google.golang.org/protobuf/proto"
)
func HandleProtobufMsgAndSend(payload map[string]interface{}) {
jsonList, err := HandleProtobufMsg(payload)
if err != nil {
if isUnsupportedProtobufMessage(err) {
Warn("跳过不匹配的protobuf消息", "err", err)
return
}
Error("protobuf消息处理失败", "err", err)
return
}
// 一个数据帧可能打包了多条消息,逐条下发
for _, jsonData := range jsonList {
if jsonData == nil {
continue
}
if config.ConnType == "http" {
SendHttpReq(jsonData)
} else {
SendWebSocketMsg(jsonData)
}
}
}
func isUnsupportedProtobufMessage(err error) bool {
if err == nil {
return false
}
message := err.Error()
return strings.Contains(message, "cannot parse invalid wire-format data") ||
strings.Contains(message, "cannot extract message data") ||
strings.Contains(message, "missing required fields") ||
strings.Contains(message, "no messages found")
}
// consumeBytesFields 从一段 protobuf 原始字节里,取出指定字段号、且 wiretype 为
// bytes(2) 的所有值。用于遍历"重复出现的 singular 字段"(proto.Unmarshal 只会保留
// 最后一个,这里手动全部取出)。
func consumeBytesFields(raw []byte, field protowire.Number) [][]byte {
var out [][]byte
for len(raw) > 0 {
num, typ, n := protowire.ConsumeTag(raw)
if n < 0 {
break
}
raw = raw[n:]
if typ == protowire.BytesType {
v, m := protowire.ConsumeBytes(raw)
if m < 0 {
break
}
if num == field {
out = append(out, v)
}
raw = raw[m:]
continue
}
m := protowire.ConsumeFieldValue(num, typ, raw)
if m < 0 {
break
}
raw = raw[m:]
}
return out
}
// parseAllRecvData 从 WxRecvMsg 原始字节里提取所有消息数据。
// 结构: WxRecvMsg.wrapper(field2) -> wrapper.body(field2, 可重复) -> body.content.data
// 一个 wrapper 里可能打包多条 body,需要全部取出。
func parseAllRecvData(rawBytes []byte) []*wxproto.WxRecvMsgData {
var result []*wxproto.WxRecvMsgData
for _, wrapperRaw := range consumeBytesFields(rawBytes, 2) {
for _, bodyRaw := range consumeBytesFields(wrapperRaw, 2) {
body := &wxproto.WxRecvMsgBody{}
if err := proto.Unmarshal(bodyRaw, body); err != nil {
continue
}
if body.Content != nil && body.Content.Data != nil {
result = append(result, body.Content.Data)
}
}
}
return result
}
func HandleProtobufMsg(payload map[string]interface{}) ([][]byte, error) {
dataInter, ok := payload["data"]
if !ok {
return nil, fmt.Errorf("protobuf_msg: missing data field")
}
dataArr, ok := dataInter.([]interface{})
if !ok {
return nil, fmt.Errorf("protobuf_msg: data is not array")
}
rawBytes := make([]byte, len(dataArr))
for i, v := range dataArr {
num, ok := v.(float64)
if !ok {
return nil, fmt.Errorf("protobuf_msg: data[%d] is not number", i)
}
rawBytes[i] = byte(int(num))
}
//fmt.Println("[receive protobuf data]", HexDump(rawBytes, 0))
dataList := parseAllRecvData(rawBytes)
if len(dataList) == 0 {
return nil, fmt.Errorf("protobuf_msg: cannot extract message data")
}
var jsonList [][]byte
for _, data := range dataList {
jsonData, err := buildWechatMessageJSON(data)
if err != nil {
// 单条消息(如系统通知/字段不全)不阻断其他消息
Warn("跳过单条protobuf消息", "err", err)
continue
}
if jsonData != nil {
jsonList = append(jsonList, jsonData)
}
}
if len(jsonList) == 0 {
return nil, fmt.Errorf("protobuf_msg: no messages found")
}
return jsonList, nil
}
// buildWechatMessageJSON 把单条 WxRecvMsgData 转成 WechatMessage JSON。
func buildWechatMessageJSON(data *wxproto.WxRecvMsgData) ([]byte, error) {
sender := ""
receiver := ""
content := ""
if data.Sender != nil {
sender = data.Sender.Value
}
if data.Receiver != nil {
receiver = data.Receiver.Value
}
if data.Content != nil {
content = data.Content.Value
}
xmlStr := string(data.Xml)
userContent := string(data.UserContent)
msgId := fmt.Sprintf("%d", data.MsgId)
if sender == "" || receiver == "" || content == "" || msgId == "" || msgId == "0" {
return nil, fmt.Errorf("protobuf_msg: missing required fields sender=%s receiver=%s content_len=%d msgId=%s",
sender, receiver, len(content), msgId)
}
selfId := receiver
if strings.Contains(receiver, "@chatroom") {
selfId = sender
}
msgType := "private"
groupId := ""
senderUser := sender
senderNickname := ""
messages := getMessagesFromProto(content, sender, data.MediaContent)
if len(messages) == 0 {
return nil, fmt.Errorf("protobuf_msg: no messages found")
}
if strings.Contains(sender, "@chatroom") {
msgType = "group"
groupId = sender
splitIndex := strings.Index(content, ":")
sendUserStart := strings.Index(content, "wxid_")
if sendUserStart >= 0 && splitIndex > sendUserStart {
senderUser = strings.TrimSpace(content[sendUserStart:splitIndex])
}
atUserMatch := regexp.MustCompile(`([\s\S]*?)`).FindStringSubmatch(xmlStr)
if len(atUserMatch) > 1 {
atUsers := strings.Split(atUserMatch[1], ",")
for _, atUser := range atUsers {
atUser = strings.TrimSpace(atUser)
if atUser != "" {
messages = append(messages, &Message{Type: "at", Data: &SendRequestData{QQ: atUser}})
}
}
}
// 处理用户的名称
splitIdx := strings.Index(userContent, ":")
if splitIdx == -1 {
if idx := strings.Index(userContent, "在群聊中@了你"); idx != -1 {
senderNickname = strings.TrimSpace(userContent[:idx])
} else if idx := strings.Index(userContent, "在群聊中发了一段语"); idx != -1 {
senderNickname = strings.TrimSpace(userContent[:idx])
}
} else {
senderNickname = strings.TrimSpace(userContent[:splitIdx])
}
if senderNickname == "" {
senderNickname = senderUser
}
} else {
splitIdx := strings.Index(userContent, ":")
if splitIdx != -1 {
senderNickname = strings.TrimSpace(userContent[:splitIdx])
}
if senderNickname == "" {
senderNickname = senderUser
}
}
if groupId != "" {
userID2NicknameMap.Store(groupId+"_"+senderUser, senderNickname)
}
wechatMsg := &WechatMessage{
GroupId: groupId,
SelfID: selfId,
UserID: senderUser,
Sender: &Sender{UserID: senderUser, Nickname: senderNickname},
Time: time.Now().UnixMilli(),
PostType: "message",
MessageId: msgId,
Message: messages,
MsgResource: xmlStr,
RawMessage: content,
ShowContent: userContent,
MessageType: msgType,
}
return json.Marshal(wechatMsg)
}
func getMessagesFromProto(content, sender string, mediaContent []byte) []*Message {
var messages []*Message
if strings.Contains(sender, "@chatroom") {
splitIndex := strings.Index(content, ":")
pureContent := ""
if splitIndex >= 0 {
pureContent = strings.TrimSpace(content[splitIndex+1:])
} else {
pureContent = content
}
parts := strings.Split(pureContent, "\u2005")
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
messages = append(messages, classifyMessage(part, nil))
}
} else {
messages = append(messages, classifyMessage(content, mediaContent))
}
return messages
}
func classifyMessage(content string, mediaContent []byte) *Message {
content = strings.ReplaceAll(content, "\t", "")
content = strings.ReplaceAll(content, "\n", "")
switch {
case strings.HasPrefix(content, "
(.*?)`)
match := re.FindStringSubmatch(content)
if len(match) > 1 {
switch match[1] {
case "5":
return &Message{Type: "share", Data: &SendRequestData{Text: content}}
case "6":
return &Message{Type: "file", Data: &SendRequestData{Text: content}}
}
}
return &Message{Type: "text", Data: &SendRequestData{Text: content}}
case strings.HasPrefix(content, "