package main
import (
"encoding/hex"
"fmt"
"math/rand"
"time"
"google.golang.org/protobuf/proto"
wxproto "github.com/yincongcyincong/weixin-macos/onebot/proto"
)
// BuildReplyMsgProto 构建发送回复消息的protobuf并返回hex编码的字符串
func BuildReplyMsgProto(sender, receiver string, replyInfo *ReplyInfo) (string, error) {
now := time.Now().Unix()
// 构建appmsg XML (需要传入自己的wxid用于fromusername)
appmsgXml := buildReplyAppmsgXml(sender, replyInfo)
// 构建客户端消息ID (使用对方的wxid,即receiver)
clientMsgId := fmt.Sprintf("%s_%d_%d_xwechat_3", receiver, now, rand.Intn(100))
// msgsource
msgsource := "1"
// proto2 需要使用指针
var (
unknown2 = []byte{}
unknown3 = int32(0)
msgType = int32(57)
flag = int32(1)
unknown13 = []byte{}
unknown14 = []byte{}
unknown15 = []byte{}
version = uint32(161)
)
msg := &wxproto.WxSendReplyMsg{
Header: &wxproto.ReplyMsgHeader{
Flag: []byte{0x00},
SessionId: &globalSessionId,
ClientProof: globalClientProof,
DeviceId: &globalDeviceId,
Platform: proto.String("UnifiedPCMac 26 arm64"),
Version: &version,
},
Body: &wxproto.ReplyMsgBody{
Sender: &sender,
Unknown2: unknown2,
Unknown3: &unknown3,
Receiver: &receiver,
MsgType: &msgType,
Content: []byte(appmsgXml),
SendTimestamp: proto.Int64(now),
ClientMsgId: &clientMsgId,
Flag: &flag,
Msgsource: []byte(msgsource),
Unknown13: unknown13,
Unknown14: unknown14,
Unknown15: unknown15,
},
Unknown5: []byte{},
Unknown9: proto.Int32(0),
Unknown10: proto.Uint64(0),
Unknown11: proto.Int32(2),
}
data, err := proto.Marshal(msg)
if err != nil {
return "", fmt.Errorf("marshal reply proto failed: %w", err)
}
//fmt.Println(fmt.Print(HexDump(data, 0x0000000C12334C00)))
return hex.EncodeToString(data), nil
}
// ReplyInfo 回复消息的全部信息
type ReplyInfo struct {
Content string // 回复的文本内容
MsgId string // 被回复消息的svrid
MsgSender string // 被回复消息的发送者wxid
MsgType int // 被回复消息的类型 (1=text, 3=image, 43=video, 49=appmsg)
CreateTime int64 // 被回复消息的时间戳(毫秒)
Msgsource string // 被回复消息的msgsource
DisplayName string // 被回复消息发送者的昵称
MsgContent string // 被回复消息的内容
}
// buildReplyAppmsgXml 构建回复消息的appmsg XML,字段顺序匹配微信真实protobuf
func buildReplyAppmsgXml(selfWxid string, info *ReplyInfo) string {
// 时间戳:毫秒转秒
createTime := info.CreateTime / 1000
xml := ``
xml += `` + escapeXmlStr(info.Content) + ``
xml += ``
xml += ``
xml += `57`
xml += `0`
xml += `0`
xml += ``
xml += ``
xml += ``
xml += ``
xml += `0`
xml += ``
xml += ``
xml += ``
xml += ``
xml += ``
xml += ``
xml += ``
xml += `0`
xml += ``
xml += ``
xml += ``
xml += ``
xml += ``
xml += ``
// refermsg - 字段顺序与微信一致: chatusr → type → createtime → msgsource → displayname → svrid → fromusr → content
xml += ``
xml += `` + escapeXmlStr(info.MsgSender) + ``
xml += `` + fmt.Sprintf("%d", info.MsgType) + ``
xml += `` + fmt.Sprintf("%d", createTime) + ``
xml += `` + escapeXmlStr(info.Msgsource) + ``
xml += ``
xml += `` + escapeXmlStr(info.MsgId) + ``
xml += `` + escapeXmlStr(info.MsgSender) + ``
xml += `` + escapeXmlStr(info.MsgContent) + ``
xml += ``
xml += ``
xml += `` + escapeXmlStr(selfWxid) + ``
return xml
}
// escapeXmlStr 简单的XML转义
func escapeXmlStr(s string) string {
result := ""
for _, c := range s {
switch c {
case '&':
result += "&"
case '<':
result += "<"
case '>':
result += ">"
case '"':
result += """
case '\'':
result += "'"
default:
result += string(c)
}
}
return result
}
// generateClientProof 生成n字节可见字符(模拟微信的client_proof格式)
func generateClientProof(n int) []byte {
const chars = "0123456789abcdefghijklmnopqrstuvwxyz"
b := make([]byte, n)
for i := range b {
b[i] = chars[rand.Intn(len(chars))]
}
return b
}