package main
import (
"encoding/hex"
"fmt"
"math/rand"
"time"
"google.golang.org/protobuf/proto"
wxproto "github.com/yincongcyincong/weixin-macos/onebot/proto"
)
// BuildFileUploadMsgProto 构建 /cgi-bin/micromsg-bin/sendfileuploadmsg 请求的protobuf
func BuildFileUploadMsgProto(targetId string, fileInfo *FileInfo) (string, error) {
// 复用上传时生成的 clientMsgId(去掉 _1 后缀),保证与 upload body 里的 filekey 一致
clientMsgId := fileInfo.ClientMsgId
if clientMsgId == "" {
now := time.Now().Unix()
clientMsgId = fmt.Sprintf("%s_%d_%d", targetId, now, rand.Intn(1000))
fileInfo.ClientMsgId = clientMsgId
}
version := NextVersion()
msgType := uint32(1)
field7 := uint32(0)
field12 := uint32(0)
fileSize := uint32(fileInfo.TotalLen)
msg := &wxproto.SendFileUploadMsgRequest{
BaseRequest: &wxproto.ReplyMsgHeader{
Flag: []byte{0x00},
SessionId: &globalSessionId,
ClientProof: globalClientProof,
DeviceId: &globalDeviceId,
Platform: proto.String("UnifiedPCMac 26 arm64"),
Version: &version,
},
ToUserName: &targetId,
ClientMsgId: &clientMsgId,
MsgType: &msgType,
FileInfo: &wxproto.FileUploadFileInfo{
FileName: &fileInfo.FileName,
FileExt: &fileInfo.FileExt,
FileMd5: &fileInfo.Md5,
FileSize: &fileSize,
},
Field7: &field7,
Field12: &field12,
}
data, err := proto.Marshal(msg)
if err != nil {
return "", fmt.Errorf("marshal file upload msg proto failed: %w", err)
}
fmt.Printf("[file-upload-proto] final protobuf hex dump:\n%s\n", HexDump(data, 0))
return hex.EncodeToString(data), nil
}
// ParseFileUploadMsgResponse 解析 sendfileuploadmsg 响应,提取 fileUploadToken 和 msgSvrId
func ParseFileUploadMsgResponse(data []byte) (string, string, error) {
resp := &wxproto.SendFileUploadMsgResponse{}
err := proto.Unmarshal(data, resp)
if err != nil {
return "", "", fmt.Errorf("unmarshal file upload response failed: %w", err)
}
if resp.BaseResponse != nil && resp.BaseResponse.Ret != nil && *resp.BaseResponse.Ret != 0 {
errMsg := ""
if resp.BaseResponse.ErrMsg != nil {
errMsg = resp.BaseResponse.ErrMsg.GetMsg()
}
return "", "", fmt.Errorf("sendfileuploadmsg failed, ret=%d, errMsg=%s", *resp.BaseResponse.Ret, errMsg)
}
token := resp.GetFileUploadToken()
msgSvrId := fmt.Sprintf("%d", resp.GetMsgSvrId())
return token, msgSvrId, nil
}
// FileInfo 文件发送消息需要的信息
type FileInfo struct {
FileName string // 文件名 (title)
TotalLen int64 // 文件大小
AttachId string // 附件ID (来自upload完成回调)
FileExt string // 文件扩展名
CdnAttachURL string // CDN附件URL
AesKey string // AES密钥
Md5 string // 文件MD5
OverwriteMsgId string // overwrite_newmsgid
FileUploadToken string // fileuploadtoken
ClientMsgId string // 由 BuildFileUploadMsgProto 生成,BuildFileMsgProto 复用
}
// BuildFileMsgProto 构建发送文件消息的protobuf并返回hex编码的字符串
// 复用reply_msg.proto (WxSendReplyMsg),msg_type=6
func BuildFileMsgProto(sender, receiver string, fileInfo *FileInfo) (string, error) {
now := time.Now().Unix()
// 构建文件appmsg XML
appmsgXml := buildFileAppmsgXml(sender, fileInfo)
// 构建客户端消息ID: 复用 BuildFileUploadMsgProto 的 clientMsgId + 后缀
clientMsgId := fileInfo.ClientMsgId + "_xwechat_1"
// msgsource
msgsource := "12"
// proto2 需要使用指针
var (
unknown2 = []byte("wx6618f1cfc6c132f8") // appid
unknown3 = int32(0)
msgType = int32(6) // 文件消息类型
flag = int32(1)
unknown13 = []byte{}
unknown14 = []byte{}
unknown15 = []byte{}
version = NextVersion()
)
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(fileInfo.Md5),
Unknown9: proto.Int32(1),
Unknown10: proto.Uint64(uint64(rand.Uint32())),
Unknown11: proto.Int32(2),
}
data, err := proto.Marshal(msg)
if err != nil {
return "", fmt.Errorf("marshal file proto failed: %w", err)
}
fmt.Printf("[file-proto] final protobuf hex dump:\n%s\n", HexDump(data, 0))
return hex.EncodeToString(data), nil
}
// buildFileAppmsgXml 构建文件消息的appmsg XML
func buildFileAppmsgXml(selfWxid string, info *FileInfo) string {
xml := ``
xml += `` + escapeXmlStr(info.FileName) + ``
xml += ``
xml += ``
xml += `6`
xml += `0`
xml += `0`
xml += ``
xml += ``
xml += ``
xml += ``
xml += `0`
xml += ``
xml += ``
xml += ``
xml += ``
xml += ``
xml += ``
xml += ``
xml += ``
xml += `` + fmt.Sprintf("%d", info.TotalLen) + ``
xml += `` + escapeXmlStr(info.AttachId) + ``
xml += ``
xml += `` + escapeXmlStr(info.FileExt) + ``
xml += `` + escapeXmlStr(info.CdnAttachURL) + ``
xml += `` + escapeXmlStr(info.AesKey) + ``
xml += `0`
xml += `` + escapeXmlStr(info.OverwriteMsgId) + ``
xml += `` + escapeXmlStr(info.FileUploadToken) + ``
xml += ``
xml += ``
xml += ``
xml += ``
xml += ``
xml += `` + escapeXmlStr(info.Md5) + ``
xml += ``
xml += ``
xml += `` + escapeXmlStr(selfWxid) + ``
return xml
}
// BuildCheckMd5Proto 构建 /cgi-bin/micromsg-bin/checkmd5 请求的protobuf
func BuildCheckMd5Proto(fileInfo *FileInfo) (string, error) {
version := NextVersion()
field5 := uint32(0)
msg := &wxproto.CheckMd5Request{
BaseRequest: &wxproto.ReplyMsgHeader{
Flag: []byte{0x00},
SessionId: &globalSessionId,
ClientProof: globalClientProof,
DeviceId: &globalDeviceId,
Platform: proto.String("UnifiedPCMac 26 arm64"),
Version: &version,
},
FileKey: &fileInfo.CdnAttachURL,
FileMd5: &fileInfo.Md5,
Field4: []byte{},
Field5: &field5,
}
data, err := proto.Marshal(msg)
if err != nil {
return "", fmt.Errorf("marshal checkmd5 proto failed: %w", err)
}
//fmt.Printf("[checkmd5-proto] final protobuf hex dump:\n%s\n", HexDump(data, 0))
return hex.EncodeToString(data), nil
}
// ParseCheckMd5Response 解析 checkmd5 响应
// ret=0 表示文件已存在(秒传),ret=102 表示文件不存在需要正常发送,两者都视为成功
func ParseCheckMd5Response(data []byte) error {
resp := &wxproto.CheckMd5Response{}
err := proto.Unmarshal(data, resp)
if err != nil {
return fmt.Errorf("unmarshal checkmd5 response failed: %w", err)
}
if resp.BaseResponse != nil && resp.BaseResponse.Ret != nil {
ret := *resp.BaseResponse.Ret
// 0=文件已存在(秒传), 102=文件不存在(正常发送), 都可以继续
if ret != 0 {
errMsg := ""
if resp.BaseResponse.ErrMsg != nil {
errMsg = resp.BaseResponse.ErrMsg.GetMsg()
}
return fmt.Errorf("checkmd5 failed, ret=%d, errMsg=%s", ret, errMsg)
}
Info("checkmd5 ret", "ret", ret)
}
return nil
}