From fb716fffb33fd62ef3d0ae12ff014e482536327b Mon Sep 17 00:00:00 2001 From: yincong Date: Thu, 9 Jul 2026 09:42:32 +0800 Subject: [PATCH] add file reply voice --- onebot/file_builder.go | 258 ++++++++++++++ onebot/go.mod | 2 +- onebot/http.go | 50 ++- onebot/main.go | 35 +- onebot/param.go | 20 +- onebot/proto/check_md5.pb.go | 213 ++++++++++++ onebot/proto/file_msg.pb.go | 257 ++++++++++++++ onebot/proto/send_app_msg.pb.go | 495 +++++++++++++++++++++++++++ onebot/proto/upload_app_attach.pb.go | 404 ++++++++++++++++++++++ onebot/proto/voice_msg.pb.go | 388 +++++++++++++++++++++ onebot/script.js | 47 ++- onebot/text_builder.go | 5 + onebot/upload_app_builder.go | 194 +++++++++++ onebot/upload_builder.go | 114 ++++++ onebot/utils.go | 193 +++++++++++ onebot/voice_builder.go | 67 ++++ onebot/worker.go | 127 +++++++ 17 files changed, 2860 insertions(+), 9 deletions(-) create mode 100644 onebot/file_builder.go create mode 100644 onebot/proto/check_md5.pb.go create mode 100644 onebot/proto/file_msg.pb.go create mode 100644 onebot/proto/send_app_msg.pb.go create mode 100644 onebot/proto/upload_app_attach.pb.go create mode 100644 onebot/proto/voice_msg.pb.go create mode 100644 onebot/upload_app_builder.go create mode 100644 onebot/voice_builder.go diff --git a/onebot/file_builder.go b/onebot/file_builder.go new file mode 100644 index 0000000..6e617bc --- /dev/null +++ b/onebot/file_builder.go @@ -0,0 +1,258 @@ +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 +} diff --git a/onebot/go.mod b/onebot/go.mod index 5244df8..891b82d 100644 --- a/onebot/go.mod +++ b/onebot/go.mod @@ -4,6 +4,7 @@ go 1.25.0 require ( github.com/frida/frida-go v1.0.2 + github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d github.com/rs/zerolog v1.35.1 @@ -14,7 +15,6 @@ require ( require ( github.com/dustin/go-humanize v1.0.1 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.22 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect diff --git a/onebot/http.go b/onebot/http.go index 172d765..0788eab 100644 --- a/onebot/http.go +++ b/onebot/http.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "net/http" + "net/url" "runtime/debug" "strings" "time" @@ -49,13 +50,21 @@ func sendHandler(w http.ResponseWriter, r *http.Request) { } } - } else if v.Type == "image" || v.Type == "video" { + } else if v.Type == "image" || v.Type == "video" || v.Type == "record" || v.Type == "voice" || v.Type == "file" { + msgType := v.Type + if msgType == "record" || msgType == "voice" { + msgType = "voice" + } + // file: 走 iPad860 风格 uploadappattach 直传(不走 CDN) + if msgType == "file" { + msgType = "send_file_simple" + } ch := make(chan error, 1) msg := &SendMsg{ UserId: req.UserID, GroupID: req.GroupID, Content: v.Data.File, - Type: v.Type, + Type: msgType, ResultChan: ch, } msgChan <- msg @@ -212,3 +221,40 @@ func jsonUnescapeString(s string) string { return result } +// getFileExt 从文件名中提取扩展名(不含.) +func getFileExt(fileName string) string { + idx := strings.LastIndex(fileName, ".") + if idx == -1 || idx == len(fileName)-1 { + return "" + } + return fileName[idx+1:] +} + +// extractFileName 从路径或URL中提取文件名 +func extractFileName(s string) string { + if s == "" { + return "" + } + // 跳过 base64 数据 + if strings.HasPrefix(s, "base64://") || strings.Contains(s, ";base64,") { + return "" + } + // URL 路径 + if strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") || strings.HasPrefix(s, "file://") { + if u, err := url.Parse(s); err == nil { + p := u.Path + if idx := strings.LastIndex(p, "/"); idx != -1 { + return p[idx+1:] + } + return p + } + } + // 本地文件路径 + if strings.Contains(s, "/") || strings.Contains(s, "\\") { + if idx := strings.LastIndexAny(s, "/\\"); idx != -1 { + return s[idx+1:] + } + } + return s +} + diff --git a/onebot/main.go b/onebot/main.go index 7f9985b..e116291 100644 --- a/onebot/main.go +++ b/onebot/main.go @@ -61,7 +61,7 @@ func initFlag() { flag.StringVar(&config.FridaGadgetAddr, "gadget_addr", "127.0.0.1:27042", "Gadget 地址: 127.0.0.1:27042 仅当 type 为 gadget 时有效") flag.StringVar(&config.OnebotToken, "token", "MuseBot", "OneBot Token: MuseBot") flag.StringVar(&config.ImagePath, "image_path", "", "图片路径: /Users/xxx/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/xxx/temp/xxx/2026-01/Img/") - flag.StringVar(&config.WechatConf, "wechat_conf", "../wechat_version/4_1_9_58_mac.json", "微信配置文件路径: ../wechat_version/4_1_6_12_mac.json") + flag.StringVar(&config.WechatConf, "wechat_conf", "../wechat_version/4_1_10_53_mac.json", "微信配置文件路径: ../wechat_version/4_1_6_12_mac.json") flag.StringVar(&config.ConnType, "conn_type", "http", "连接类型: http | websocket") flag.IntVar(&config.SendInterval, "send_interval", 1000, "发送间隔: ms") flag.IntVar(&config.WechatPid, "wechat_pid", 0, "微信进程 PID,不设置则自动查找") @@ -294,6 +294,39 @@ func loadJs() { m.ResultChan = ch.(chan error) } msgChan <- m + case "upload_voice_finish": + m := &SendMsg{ + Type: "send_voice", + } + targetId := "" + if targetIdInter, ok := pMap["target_id"]; ok { + targetId = targetIdInter.(string) + if strings.Contains(targetId, "wxid_") { + m.UserId = targetId + } else { + m.GroupID = targetId + } + } + if cdnKey, ok := pMap["cdn_key"]; ok { + m.CdnKey = cdnKey.(string) + } + if aesKey, ok := pMap["aes_key"]; ok { + m.AesKey = aesKey.(string) + } + if voiceDuration, ok := pMap["voice_duration"]; ok { + if vd, ok := voiceDuration.(float64); ok { + m.VoiceDuration = int32(vd) + } + } + if silkDataLen, ok := pMap["silk_data_len"]; ok { + if sdl, ok := silkDataLen.(float64); ok { + m.SilkDataLen = int32(sdl) + } + } + if ch, ok := pendingResultMap.LoadAndDelete(targetId); ok { + m.ResultChan = ch.(chan error) + } + msgChan <- m case "download": err = Download(payloadJson) if err != nil { diff --git a/onebot/param.go b/onebot/param.go index 1aa68f8..f7429a0 100644 --- a/onebot/param.go +++ b/onebot/param.go @@ -5,6 +5,7 @@ import ( "fmt" "math/rand" "sync" + "sync/atomic" "github.com/frida/frida-go/frida" ) @@ -27,12 +28,19 @@ var ( config = &Config{} - userID2NicknameMap sync.Map - userID2FileMsgMap sync.Map - videoInfoMap sync.Map // targetId -> *VideoInfo - buf2RespChan = make(chan *Buf2RespData, 10) + userID2NicknameMap sync.Map + userID2FileMsgMap sync.Map + videoInfoMap sync.Map // targetId -> *VideoInfo + buf2RespChan = make(chan *Buf2RespData, 10) + debugRespChan = make(chan []byte, 1) + appAttachRespChan = make(chan []byte, 1) ) +// NextVersion 获取当前taskId作为版本号 +func NextVersion() uint32 { + return uint32(atomic.LoadInt64(&taskId)) +} + type WechatMessage struct { GroupId string `json:"group_id"` SelfID string `json:"self_id"` @@ -73,6 +81,10 @@ type SendMsg struct { Duration int32 VideoSize int32 + VoiceDuration int32 + SilkDataLen int32 + Unknown13 int32 + ReferMsgId string ReferMsgSender string ReferMsgType int diff --git a/onebot/proto/check_md5.pb.go b/onebot/proto/check_md5.pb.go new file mode 100644 index 0000000..f5cab5d --- /dev/null +++ b/onebot/proto/check_md5.pb.go @@ -0,0 +1,213 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v3.5.1 +// source: check_md5.proto + +package wxproto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// /cgi-bin/micromsg-bin/checkmd5 请求 +type CheckMd5Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + BaseRequest *ReplyMsgHeader `protobuf:"bytes,1,opt,name=baseRequest" json:"baseRequest,omitempty"` + FileKey *string `protobuf:"bytes,2,opt,name=fileKey" json:"fileKey,omitempty"` // CDN file key (attachid) + FileMd5 *string `protobuf:"bytes,3,opt,name=fileMd5" json:"fileMd5,omitempty"` // 文件MD5 (32位hex字符串) + Field4 []byte `protobuf:"bytes,4,opt,name=field4" json:"field4,omitempty"` // 空 + Field5 *uint32 `protobuf:"varint,5,opt,name=field5" json:"field5,omitempty"` // 0 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckMd5Request) Reset() { + *x = CheckMd5Request{} + mi := &file_check_md5_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckMd5Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckMd5Request) ProtoMessage() {} + +func (x *CheckMd5Request) ProtoReflect() protoreflect.Message { + mi := &file_check_md5_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckMd5Request.ProtoReflect.Descriptor instead. +func (*CheckMd5Request) Descriptor() ([]byte, []int) { + return file_check_md5_proto_rawDescGZIP(), []int{0} +} + +func (x *CheckMd5Request) GetBaseRequest() *ReplyMsgHeader { + if x != nil { + return x.BaseRequest + } + return nil +} + +func (x *CheckMd5Request) GetFileKey() string { + if x != nil && x.FileKey != nil { + return *x.FileKey + } + return "" +} + +func (x *CheckMd5Request) GetFileMd5() string { + if x != nil && x.FileMd5 != nil { + return *x.FileMd5 + } + return "" +} + +func (x *CheckMd5Request) GetField4() []byte { + if x != nil { + return x.Field4 + } + return nil +} + +func (x *CheckMd5Request) GetField5() uint32 { + if x != nil && x.Field5 != nil { + return *x.Field5 + } + return 0 +} + +// /cgi-bin/micromsg-bin/checkmd5 响应 +type CheckMd5Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + BaseResponse *BaseResponse `protobuf:"bytes,1,opt,name=baseResponse" json:"baseResponse,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckMd5Response) Reset() { + *x = CheckMd5Response{} + mi := &file_check_md5_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckMd5Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckMd5Response) ProtoMessage() {} + +func (x *CheckMd5Response) ProtoReflect() protoreflect.Message { + mi := &file_check_md5_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckMd5Response.ProtoReflect.Descriptor instead. +func (*CheckMd5Response) Descriptor() ([]byte, []int) { + return file_check_md5_proto_rawDescGZIP(), []int{1} +} + +func (x *CheckMd5Response) GetBaseResponse() *BaseResponse { + if x != nil { + return x.BaseResponse + } + return nil +} + +var File_check_md5_proto protoreflect.FileDescriptor + +const file_check_md5_proto_rawDesc = "" + + "\n" + + "\x0fcheck_md5.proto\x12\awxproto\x1a\rapp_msg.proto\x1a\x11common_resp.proto\"\xb0\x01\n" + + "\x0fCheckMd5Request\x129\n" + + "\vbaseRequest\x18\x01 \x01(\v2\x17.wxproto.ReplyMsgHeaderR\vbaseRequest\x12\x18\n" + + "\afileKey\x18\x02 \x01(\tR\afileKey\x12\x18\n" + + "\afileMd5\x18\x03 \x01(\tR\afileMd5\x12\x16\n" + + "\x06field4\x18\x04 \x01(\fR\x06field4\x12\x16\n" + + "\x06field5\x18\x05 \x01(\rR\x06field5\"M\n" + + "\x10CheckMd5Response\x129\n" + + "\fbaseResponse\x18\x01 \x01(\v2\x15.wxproto.BaseResponseR\fbaseResponseB>Z wxproto.ReplyMsgHeader + 3, // 1: wxproto.CheckMd5Response.baseResponse:type_name -> wxproto.BaseResponse + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_check_md5_proto_init() } +func file_check_md5_proto_init() { + if File_check_md5_proto != nil { + return + } + file_app_msg_proto_init() + file_common_resp_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_check_md5_proto_rawDesc), len(file_check_md5_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_check_md5_proto_goTypes, + DependencyIndexes: file_check_md5_proto_depIdxs, + MessageInfos: file_check_md5_proto_msgTypes, + }.Build() + File_check_md5_proto = out.File + file_check_md5_proto_goTypes = nil + file_check_md5_proto_depIdxs = nil +} diff --git a/onebot/proto/file_msg.pb.go b/onebot/proto/file_msg.pb.go new file mode 100644 index 0000000..a64dda8 --- /dev/null +++ b/onebot/proto/file_msg.pb.go @@ -0,0 +1,257 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v3.5.1 +// source: file_msg.proto + +package wxproto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// /cgi-bin/micromsg-bin/sendfileuploadmsg 请求 +type SendFileUploadMsgRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + BaseRequest *ReplyMsgHeader `protobuf:"bytes,1,opt,name=baseRequest" json:"baseRequest,omitempty"` + ToUserName *string `protobuf:"bytes,2,opt,name=toUserName" json:"toUserName,omitempty"` + ClientMsgId *string `protobuf:"bytes,3,opt,name=clientMsgId" json:"clientMsgId,omitempty"` + MsgType *uint32 `protobuf:"varint,4,opt,name=msgType" json:"msgType,omitempty"` + FileInfo *FileUploadFileInfo `protobuf:"bytes,5,opt,name=fileInfo" json:"fileInfo,omitempty"` + Field7 *uint32 `protobuf:"varint,7,opt,name=field7" json:"field7,omitempty"` + Field12 *uint32 `protobuf:"varint,12,opt,name=field12" json:"field12,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendFileUploadMsgRequest) Reset() { + *x = SendFileUploadMsgRequest{} + mi := &file_file_msg_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendFileUploadMsgRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendFileUploadMsgRequest) ProtoMessage() {} + +func (x *SendFileUploadMsgRequest) ProtoReflect() protoreflect.Message { + mi := &file_file_msg_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendFileUploadMsgRequest.ProtoReflect.Descriptor instead. +func (*SendFileUploadMsgRequest) Descriptor() ([]byte, []int) { + return file_file_msg_proto_rawDescGZIP(), []int{0} +} + +func (x *SendFileUploadMsgRequest) GetBaseRequest() *ReplyMsgHeader { + if x != nil { + return x.BaseRequest + } + return nil +} + +func (x *SendFileUploadMsgRequest) GetToUserName() string { + if x != nil && x.ToUserName != nil { + return *x.ToUserName + } + return "" +} + +func (x *SendFileUploadMsgRequest) GetClientMsgId() string { + if x != nil && x.ClientMsgId != nil { + return *x.ClientMsgId + } + return "" +} + +func (x *SendFileUploadMsgRequest) GetMsgType() uint32 { + if x != nil && x.MsgType != nil { + return *x.MsgType + } + return 0 +} + +func (x *SendFileUploadMsgRequest) GetFileInfo() *FileUploadFileInfo { + if x != nil { + return x.FileInfo + } + return nil +} + +func (x *SendFileUploadMsgRequest) GetField7() uint32 { + if x != nil && x.Field7 != nil { + return *x.Field7 + } + return 0 +} + +func (x *SendFileUploadMsgRequest) GetField12() uint32 { + if x != nil && x.Field12 != nil { + return *x.Field12 + } + return 0 +} + +type FileUploadFileInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + FileName *string `protobuf:"bytes,1,opt,name=fileName" json:"fileName,omitempty"` + FileExt *string `protobuf:"bytes,2,opt,name=fileExt" json:"fileExt,omitempty"` + FileMd5 *string `protobuf:"bytes,3,opt,name=fileMd5" json:"fileMd5,omitempty"` + FileSize *uint32 `protobuf:"varint,4,opt,name=fileSize" json:"fileSize,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FileUploadFileInfo) Reset() { + *x = FileUploadFileInfo{} + mi := &file_file_msg_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FileUploadFileInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FileUploadFileInfo) ProtoMessage() {} + +func (x *FileUploadFileInfo) ProtoReflect() protoreflect.Message { + mi := &file_file_msg_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FileUploadFileInfo.ProtoReflect.Descriptor instead. +func (*FileUploadFileInfo) Descriptor() ([]byte, []int) { + return file_file_msg_proto_rawDescGZIP(), []int{1} +} + +func (x *FileUploadFileInfo) GetFileName() string { + if x != nil && x.FileName != nil { + return *x.FileName + } + return "" +} + +func (x *FileUploadFileInfo) GetFileExt() string { + if x != nil && x.FileExt != nil { + return *x.FileExt + } + return "" +} + +func (x *FileUploadFileInfo) GetFileMd5() string { + if x != nil && x.FileMd5 != nil { + return *x.FileMd5 + } + return "" +} + +func (x *FileUploadFileInfo) GetFileSize() uint32 { + if x != nil && x.FileSize != nil { + return *x.FileSize + } + return 0 +} + +var File_file_msg_proto protoreflect.FileDescriptor + +const file_file_msg_proto_rawDesc = "" + + "\n" + + "\x0efile_msg.proto\x12\awxproto\x1a\rapp_msg.proto\"\x9c\x02\n" + + "\x18SendFileUploadMsgRequest\x129\n" + + "\vbaseRequest\x18\x01 \x01(\v2\x17.wxproto.ReplyMsgHeaderR\vbaseRequest\x12\x1e\n" + + "\n" + + "toUserName\x18\x02 \x01(\tR\n" + + "toUserName\x12 \n" + + "\vclientMsgId\x18\x03 \x01(\tR\vclientMsgId\x12\x18\n" + + "\amsgType\x18\x04 \x01(\rR\amsgType\x127\n" + + "\bfileInfo\x18\x05 \x01(\v2\x1b.wxproto.FileUploadFileInfoR\bfileInfo\x12\x16\n" + + "\x06field7\x18\a \x01(\rR\x06field7\x12\x18\n" + + "\afield12\x18\f \x01(\rR\afield12\"\x80\x01\n" + + "\x12FileUploadFileInfo\x12\x1a\n" + + "\bfileName\x18\x01 \x01(\tR\bfileName\x12\x18\n" + + "\afileExt\x18\x02 \x01(\tR\afileExt\x12\x18\n" + + "\afileMd5\x18\x03 \x01(\tR\afileMd5\x12\x1a\n" + + "\bfileSize\x18\x04 \x01(\rR\bfileSizeB>Z wxproto.ReplyMsgHeader + 1, // 1: wxproto.SendFileUploadMsgRequest.fileInfo:type_name -> wxproto.FileUploadFileInfo + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_file_msg_proto_init() } +func file_file_msg_proto_init() { + if File_file_msg_proto != nil { + return + } + file_app_msg_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_file_msg_proto_rawDesc), len(file_file_msg_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_file_msg_proto_goTypes, + DependencyIndexes: file_file_msg_proto_depIdxs, + MessageInfos: file_file_msg_proto_msgTypes, + }.Build() + File_file_msg_proto = out.File + file_file_msg_proto_goTypes = nil + file_file_msg_proto_depIdxs = nil +} diff --git a/onebot/proto/send_app_msg.pb.go b/onebot/proto/send_app_msg.pb.go new file mode 100644 index 0000000..b1c3715 --- /dev/null +++ b/onebot/proto/send_app_msg.pb.go @@ -0,0 +1,495 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v3.5.1 +// source: send_app_msg.proto + +package wxproto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// /cgi-bin/micromsg-bin/sendappmsg 请求 +// 字段号严格对齐 wechat7016 SendAppMsgRequest, +// 但 baseRequest 使用本项目 macOS 风格的 ReplyMsgHeader。 +type SendAppMsgReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + BaseRequest *ReplyMsgHeader `protobuf:"bytes,1,opt,name=baseRequest" json:"baseRequest,omitempty"` + Msg *AppMsgBody `protobuf:"bytes,2,opt,name=msg" json:"msg,omitempty"` + CommentUrl *string `protobuf:"bytes,3,opt,name=commentUrl" json:"commentUrl,omitempty"` + ReqTime *uint32 `protobuf:"varint,4,opt,name=reqTime" json:"reqTime,omitempty"` + Md5 *string `protobuf:"bytes,5,opt,name=md5" json:"md5,omitempty"` + FileType *uint32 `protobuf:"varint,6,opt,name=fileType" json:"fileType,omitempty"` + Signature *string `protobuf:"bytes,7,opt,name=signature" json:"signature,omitempty"` + FromSence *string `protobuf:"bytes,8,opt,name=fromSence" json:"fromSence,omitempty"` + HitMd5 *uint32 `protobuf:"varint,9,opt,name=hitMd5" json:"hitMd5,omitempty"` + Crc32 *uint32 `protobuf:"varint,10,opt,name=crc32" json:"crc32,omitempty"` + MsgForwardType *uint32 `protobuf:"varint,11,opt,name=msgForwardType" json:"msgForwardType,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendAppMsgReq) Reset() { + *x = SendAppMsgReq{} + mi := &file_send_app_msg_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendAppMsgReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendAppMsgReq) ProtoMessage() {} + +func (x *SendAppMsgReq) ProtoReflect() protoreflect.Message { + mi := &file_send_app_msg_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendAppMsgReq.ProtoReflect.Descriptor instead. +func (*SendAppMsgReq) Descriptor() ([]byte, []int) { + return file_send_app_msg_proto_rawDescGZIP(), []int{0} +} + +func (x *SendAppMsgReq) GetBaseRequest() *ReplyMsgHeader { + if x != nil { + return x.BaseRequest + } + return nil +} + +func (x *SendAppMsgReq) GetMsg() *AppMsgBody { + if x != nil { + return x.Msg + } + return nil +} + +func (x *SendAppMsgReq) GetCommentUrl() string { + if x != nil && x.CommentUrl != nil { + return *x.CommentUrl + } + return "" +} + +func (x *SendAppMsgReq) GetReqTime() uint32 { + if x != nil && x.ReqTime != nil { + return *x.ReqTime + } + return 0 +} + +func (x *SendAppMsgReq) GetMd5() string { + if x != nil && x.Md5 != nil { + return *x.Md5 + } + return "" +} + +func (x *SendAppMsgReq) GetFileType() uint32 { + if x != nil && x.FileType != nil { + return *x.FileType + } + return 0 +} + +func (x *SendAppMsgReq) GetSignature() string { + if x != nil && x.Signature != nil { + return *x.Signature + } + return "" +} + +func (x *SendAppMsgReq) GetFromSence() string { + if x != nil && x.FromSence != nil { + return *x.FromSence + } + return "" +} + +func (x *SendAppMsgReq) GetHitMd5() uint32 { + if x != nil && x.HitMd5 != nil { + return *x.HitMd5 + } + return 0 +} + +func (x *SendAppMsgReq) GetCrc32() uint32 { + if x != nil && x.Crc32 != nil { + return *x.Crc32 + } + return 0 +} + +func (x *SendAppMsgReq) GetMsgForwardType() uint32 { + if x != nil && x.MsgForwardType != nil { + return *x.MsgForwardType + } + return 0 +} + +// AppMsg 消息体 (wechat7016 AppMsg) +type AppMsgBody struct { + state protoimpl.MessageState `protogen:"open.v1"` + FromUserName *string `protobuf:"bytes,1,opt,name=fromUserName" json:"fromUserName,omitempty"` + AppId *string `protobuf:"bytes,2,opt,name=appId" json:"appId,omitempty"` + SdkVersion *uint32 `protobuf:"varint,3,opt,name=sdkVersion" json:"sdkVersion,omitempty"` + ToUserName *string `protobuf:"bytes,4,opt,name=toUserName" json:"toUserName,omitempty"` + Type *uint32 `protobuf:"varint,5,opt,name=type" json:"type,omitempty"` // 6 = 文件 + Content *string `protobuf:"bytes,6,opt,name=content" json:"content,omitempty"` // appmsg XML + CreateTime *int64 `protobuf:"varint,7,opt,name=createTime" json:"createTime,omitempty"` + ClientMsgId *string `protobuf:"bytes,8,opt,name=clientMsgId" json:"clientMsgId,omitempty"` + MsgSource *string `protobuf:"bytes,12,opt,name=msgSource" json:"msgSource,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AppMsgBody) Reset() { + *x = AppMsgBody{} + mi := &file_send_app_msg_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AppMsgBody) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AppMsgBody) ProtoMessage() {} + +func (x *AppMsgBody) ProtoReflect() protoreflect.Message { + mi := &file_send_app_msg_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AppMsgBody.ProtoReflect.Descriptor instead. +func (*AppMsgBody) Descriptor() ([]byte, []int) { + return file_send_app_msg_proto_rawDescGZIP(), []int{1} +} + +func (x *AppMsgBody) GetFromUserName() string { + if x != nil && x.FromUserName != nil { + return *x.FromUserName + } + return "" +} + +func (x *AppMsgBody) GetAppId() string { + if x != nil && x.AppId != nil { + return *x.AppId + } + return "" +} + +func (x *AppMsgBody) GetSdkVersion() uint32 { + if x != nil && x.SdkVersion != nil { + return *x.SdkVersion + } + return 0 +} + +func (x *AppMsgBody) GetToUserName() string { + if x != nil && x.ToUserName != nil { + return *x.ToUserName + } + return "" +} + +func (x *AppMsgBody) GetType() uint32 { + if x != nil && x.Type != nil { + return *x.Type + } + return 0 +} + +func (x *AppMsgBody) GetContent() string { + if x != nil && x.Content != nil { + return *x.Content + } + return "" +} + +func (x *AppMsgBody) GetCreateTime() int64 { + if x != nil && x.CreateTime != nil { + return *x.CreateTime + } + return 0 +} + +func (x *AppMsgBody) GetClientMsgId() string { + if x != nil && x.ClientMsgId != nil { + return *x.ClientMsgId + } + return "" +} + +func (x *AppMsgBody) GetMsgSource() string { + if x != nil && x.MsgSource != nil { + return *x.MsgSource + } + return "" +} + +// /cgi-bin/micromsg-bin/sendappmsg 响应 +type SendAppMsgResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + BaseResponse *BaseResponse `protobuf:"bytes,1,opt,name=baseResponse" json:"baseResponse,omitempty"` + AppId *string `protobuf:"bytes,2,opt,name=appId" json:"appId,omitempty"` + FromUserName *string `protobuf:"bytes,3,opt,name=fromUserName" json:"fromUserName,omitempty"` + ToUserName *string `protobuf:"bytes,4,opt,name=toUserName" json:"toUserName,omitempty"` + MsgId *int32 `protobuf:"varint,5,opt,name=msgId" json:"msgId,omitempty"` + ClientMsgId *string `protobuf:"bytes,6,opt,name=clientMsgId" json:"clientMsgId,omitempty"` + CreateTime *int32 `protobuf:"varint,7,opt,name=createTime" json:"createTime,omitempty"` + Type *int32 `protobuf:"varint,8,opt,name=type" json:"type,omitempty"` + NewMsgId *int64 `protobuf:"varint,9,opt,name=newMsgId" json:"newMsgId,omitempty"` + Aeskey *string `protobuf:"bytes,10,opt,name=aeskey" json:"aeskey,omitempty"` + MsgSource *string `protobuf:"bytes,11,opt,name=msgSource" json:"msgSource,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendAppMsgResp) Reset() { + *x = SendAppMsgResp{} + mi := &file_send_app_msg_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendAppMsgResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendAppMsgResp) ProtoMessage() {} + +func (x *SendAppMsgResp) ProtoReflect() protoreflect.Message { + mi := &file_send_app_msg_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendAppMsgResp.ProtoReflect.Descriptor instead. +func (*SendAppMsgResp) Descriptor() ([]byte, []int) { + return file_send_app_msg_proto_rawDescGZIP(), []int{2} +} + +func (x *SendAppMsgResp) GetBaseResponse() *BaseResponse { + if x != nil { + return x.BaseResponse + } + return nil +} + +func (x *SendAppMsgResp) GetAppId() string { + if x != nil && x.AppId != nil { + return *x.AppId + } + return "" +} + +func (x *SendAppMsgResp) GetFromUserName() string { + if x != nil && x.FromUserName != nil { + return *x.FromUserName + } + return "" +} + +func (x *SendAppMsgResp) GetToUserName() string { + if x != nil && x.ToUserName != nil { + return *x.ToUserName + } + return "" +} + +func (x *SendAppMsgResp) GetMsgId() int32 { + if x != nil && x.MsgId != nil { + return *x.MsgId + } + return 0 +} + +func (x *SendAppMsgResp) GetClientMsgId() string { + if x != nil && x.ClientMsgId != nil { + return *x.ClientMsgId + } + return "" +} + +func (x *SendAppMsgResp) GetCreateTime() int32 { + if x != nil && x.CreateTime != nil { + return *x.CreateTime + } + return 0 +} + +func (x *SendAppMsgResp) GetType() int32 { + if x != nil && x.Type != nil { + return *x.Type + } + return 0 +} + +func (x *SendAppMsgResp) GetNewMsgId() int64 { + if x != nil && x.NewMsgId != nil { + return *x.NewMsgId + } + return 0 +} + +func (x *SendAppMsgResp) GetAeskey() string { + if x != nil && x.Aeskey != nil { + return *x.Aeskey + } + return "" +} + +func (x *SendAppMsgResp) GetMsgSource() string { + if x != nil && x.MsgSource != nil { + return *x.MsgSource + } + return "" +} + +var File_send_app_msg_proto protoreflect.FileDescriptor + +const file_send_app_msg_proto_rawDesc = "" + + "\n" + + "\x12send_app_msg.proto\x12\awxproto\x1a\rapp_msg.proto\x1a\x11common_resp.proto\"\xeb\x02\n" + + "\rSendAppMsgReq\x129\n" + + "\vbaseRequest\x18\x01 \x01(\v2\x17.wxproto.ReplyMsgHeaderR\vbaseRequest\x12%\n" + + "\x03msg\x18\x02 \x01(\v2\x13.wxproto.AppMsgBodyR\x03msg\x12\x1e\n" + + "\n" + + "commentUrl\x18\x03 \x01(\tR\n" + + "commentUrl\x12\x18\n" + + "\areqTime\x18\x04 \x01(\rR\areqTime\x12\x10\n" + + "\x03md5\x18\x05 \x01(\tR\x03md5\x12\x1a\n" + + "\bfileType\x18\x06 \x01(\rR\bfileType\x12\x1c\n" + + "\tsignature\x18\a \x01(\tR\tsignature\x12\x1c\n" + + "\tfromSence\x18\b \x01(\tR\tfromSence\x12\x16\n" + + "\x06hitMd5\x18\t \x01(\rR\x06hitMd5\x12\x14\n" + + "\x05crc32\x18\n" + + " \x01(\rR\x05crc32\x12&\n" + + "\x0emsgForwardType\x18\v \x01(\rR\x0emsgForwardType\"\x94\x02\n" + + "\n" + + "AppMsgBody\x12\"\n" + + "\ffromUserName\x18\x01 \x01(\tR\ffromUserName\x12\x14\n" + + "\x05appId\x18\x02 \x01(\tR\x05appId\x12\x1e\n" + + "\n" + + "sdkVersion\x18\x03 \x01(\rR\n" + + "sdkVersion\x12\x1e\n" + + "\n" + + "toUserName\x18\x04 \x01(\tR\n" + + "toUserName\x12\x12\n" + + "\x04type\x18\x05 \x01(\rR\x04type\x12\x18\n" + + "\acontent\x18\x06 \x01(\tR\acontent\x12\x1e\n" + + "\n" + + "createTime\x18\a \x01(\x03R\n" + + "createTime\x12 \n" + + "\vclientMsgId\x18\b \x01(\tR\vclientMsgId\x12\x1c\n" + + "\tmsgSource\x18\f \x01(\tR\tmsgSource\"\xe3\x02\n" + + "\x0eSendAppMsgResp\x129\n" + + "\fbaseResponse\x18\x01 \x01(\v2\x15.wxproto.BaseResponseR\fbaseResponse\x12\x14\n" + + "\x05appId\x18\x02 \x01(\tR\x05appId\x12\"\n" + + "\ffromUserName\x18\x03 \x01(\tR\ffromUserName\x12\x1e\n" + + "\n" + + "toUserName\x18\x04 \x01(\tR\n" + + "toUserName\x12\x14\n" + + "\x05msgId\x18\x05 \x01(\x05R\x05msgId\x12 \n" + + "\vclientMsgId\x18\x06 \x01(\tR\vclientMsgId\x12\x1e\n" + + "\n" + + "createTime\x18\a \x01(\x05R\n" + + "createTime\x12\x12\n" + + "\x04type\x18\b \x01(\x05R\x04type\x12\x1a\n" + + "\bnewMsgId\x18\t \x01(\x03R\bnewMsgId\x12\x16\n" + + "\x06aeskey\x18\n" + + " \x01(\tR\x06aeskey\x12\x1c\n" + + "\tmsgSource\x18\v \x01(\tR\tmsgSourceB>Z wxproto.ReplyMsgHeader + 1, // 1: wxproto.SendAppMsgReq.msg:type_name -> wxproto.AppMsgBody + 4, // 2: wxproto.SendAppMsgResp.baseResponse:type_name -> wxproto.BaseResponse + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_send_app_msg_proto_init() } +func file_send_app_msg_proto_init() { + if File_send_app_msg_proto != nil { + return + } + file_app_msg_proto_init() + file_common_resp_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_send_app_msg_proto_rawDesc), len(file_send_app_msg_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_send_app_msg_proto_goTypes, + DependencyIndexes: file_send_app_msg_proto_depIdxs, + MessageInfos: file_send_app_msg_proto_msgTypes, + }.Build() + File_send_app_msg_proto = out.File + file_send_app_msg_proto_goTypes = nil + file_send_app_msg_proto_depIdxs = nil +} diff --git a/onebot/proto/upload_app_attach.pb.go b/onebot/proto/upload_app_attach.pb.go new file mode 100644 index 0000000..29b2c7d --- /dev/null +++ b/onebot/proto/upload_app_attach.pb.go @@ -0,0 +1,404 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v3.5.1 +// source: upload_app_attach.proto + +package wxproto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// 通用二进制缓冲(iPad860 SKBuiltinBufferT 对齐) +type SKBuiltinBufferT struct { + state protoimpl.MessageState `protogen:"open.v1"` + ILen *uint32 `protobuf:"varint,1,opt,name=iLen" json:"iLen,omitempty"` + Buffer []byte `protobuf:"bytes,2,opt,name=buffer" json:"buffer,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SKBuiltinBufferT) Reset() { + *x = SKBuiltinBufferT{} + mi := &file_upload_app_attach_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SKBuiltinBufferT) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SKBuiltinBufferT) ProtoMessage() {} + +func (x *SKBuiltinBufferT) ProtoReflect() protoreflect.Message { + mi := &file_upload_app_attach_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SKBuiltinBufferT.ProtoReflect.Descriptor instead. +func (*SKBuiltinBufferT) Descriptor() ([]byte, []int) { + return file_upload_app_attach_proto_rawDescGZIP(), []int{0} +} + +func (x *SKBuiltinBufferT) GetILen() uint32 { + if x != nil && x.ILen != nil { + return *x.ILen + } + return 0 +} + +func (x *SKBuiltinBufferT) GetBuffer() []byte { + if x != nil { + return x.Buffer + } + return nil +} + +// /cgi-bin/micromsg-bin/uploadappattach 请求 +// 参考 we-chat-ipad860 wechat.proto UploadAppAttachRequest, +// 但 baseRequest 使用本项目 macOS 风格的 ReplyMsgHeader。 +type UploadAppAttachRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + BaseRequest *ReplyMsgHeader `protobuf:"bytes,1,opt,name=baseRequest" json:"baseRequest,omitempty"` + AppId *string `protobuf:"bytes,2,opt,name=appId" json:"appId,omitempty"` + SdkVersion *uint32 `protobuf:"varint,3,opt,name=sdkVersion" json:"sdkVersion,omitempty"` + ClientAppDataId *string `protobuf:"bytes,4,opt,name=clientAppDataId" json:"clientAppDataId,omitempty"` // {receiver}_{ts}_UploadFile + UserName *string `protobuf:"bytes,5,opt,name=userName" json:"userName,omitempty"` // 接收方 wxid + TotalLen *uint32 `protobuf:"varint,6,opt,name=totalLen" json:"totalLen,omitempty"` // 文件总长度 + StartPos *uint32 `protobuf:"varint,7,opt,name=startPos" json:"startPos,omitempty"` // 本分片偏移 + DataLen *uint32 `protobuf:"varint,8,opt,name=dataLen" json:"dataLen,omitempty"` // 本分片长度 + Data *SKBuiltinBufferT `protobuf:"bytes,9,opt,name=data" json:"data,omitempty"` // 本分片原始字节 + Type *uint32 `protobuf:"varint,10,opt,name=type" json:"type,omitempty"` // 6 = 文件 + Md5 *string `protobuf:"bytes,11,opt,name=md5" json:"md5,omitempty"` // 文件 MD5 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UploadAppAttachRequest) Reset() { + *x = UploadAppAttachRequest{} + mi := &file_upload_app_attach_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UploadAppAttachRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UploadAppAttachRequest) ProtoMessage() {} + +func (x *UploadAppAttachRequest) ProtoReflect() protoreflect.Message { + mi := &file_upload_app_attach_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UploadAppAttachRequest.ProtoReflect.Descriptor instead. +func (*UploadAppAttachRequest) Descriptor() ([]byte, []int) { + return file_upload_app_attach_proto_rawDescGZIP(), []int{1} +} + +func (x *UploadAppAttachRequest) GetBaseRequest() *ReplyMsgHeader { + if x != nil { + return x.BaseRequest + } + return nil +} + +func (x *UploadAppAttachRequest) GetAppId() string { + if x != nil && x.AppId != nil { + return *x.AppId + } + return "" +} + +func (x *UploadAppAttachRequest) GetSdkVersion() uint32 { + if x != nil && x.SdkVersion != nil { + return *x.SdkVersion + } + return 0 +} + +func (x *UploadAppAttachRequest) GetClientAppDataId() string { + if x != nil && x.ClientAppDataId != nil { + return *x.ClientAppDataId + } + return "" +} + +func (x *UploadAppAttachRequest) GetUserName() string { + if x != nil && x.UserName != nil { + return *x.UserName + } + return "" +} + +func (x *UploadAppAttachRequest) GetTotalLen() uint32 { + if x != nil && x.TotalLen != nil { + return *x.TotalLen + } + return 0 +} + +func (x *UploadAppAttachRequest) GetStartPos() uint32 { + if x != nil && x.StartPos != nil { + return *x.StartPos + } + return 0 +} + +func (x *UploadAppAttachRequest) GetDataLen() uint32 { + if x != nil && x.DataLen != nil { + return *x.DataLen + } + return 0 +} + +func (x *UploadAppAttachRequest) GetData() *SKBuiltinBufferT { + if x != nil { + return x.Data + } + return nil +} + +func (x *UploadAppAttachRequest) GetType() uint32 { + if x != nil && x.Type != nil { + return *x.Type + } + return 0 +} + +func (x *UploadAppAttachRequest) GetMd5() string { + if x != nil && x.Md5 != nil { + return *x.Md5 + } + return "" +} + +// /cgi-bin/micromsg-bin/uploadappattach 响应 +type UploadAppAttachResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + BaseResponse *BaseResponse `protobuf:"bytes,1,opt,name=baseResponse" json:"baseResponse,omitempty"` + AppId *string `protobuf:"bytes,2,opt,name=appId" json:"appId,omitempty"` + MediaId *string `protobuf:"bytes,3,opt,name=mediaId" json:"mediaId,omitempty"` // attachid + ClientAppDataId *string `protobuf:"bytes,4,opt,name=clientAppDataId" json:"clientAppDataId,omitempty"` + UserName *string `protobuf:"bytes,5,opt,name=userName" json:"userName,omitempty"` + TotalLen *uint32 `protobuf:"varint,6,opt,name=totalLen" json:"totalLen,omitempty"` + StartPos *uint32 `protobuf:"varint,7,opt,name=startPos" json:"startPos,omitempty"` + DataLen *uint32 `protobuf:"varint,8,opt,name=dataLen" json:"dataLen,omitempty"` + CreateTime *uint64 `protobuf:"varint,9,opt,name=createTime" json:"createTime,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UploadAppAttachResponse) Reset() { + *x = UploadAppAttachResponse{} + mi := &file_upload_app_attach_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UploadAppAttachResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UploadAppAttachResponse) ProtoMessage() {} + +func (x *UploadAppAttachResponse) ProtoReflect() protoreflect.Message { + mi := &file_upload_app_attach_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UploadAppAttachResponse.ProtoReflect.Descriptor instead. +func (*UploadAppAttachResponse) Descriptor() ([]byte, []int) { + return file_upload_app_attach_proto_rawDescGZIP(), []int{2} +} + +func (x *UploadAppAttachResponse) GetBaseResponse() *BaseResponse { + if x != nil { + return x.BaseResponse + } + return nil +} + +func (x *UploadAppAttachResponse) GetAppId() string { + if x != nil && x.AppId != nil { + return *x.AppId + } + return "" +} + +func (x *UploadAppAttachResponse) GetMediaId() string { + if x != nil && x.MediaId != nil { + return *x.MediaId + } + return "" +} + +func (x *UploadAppAttachResponse) GetClientAppDataId() string { + if x != nil && x.ClientAppDataId != nil { + return *x.ClientAppDataId + } + return "" +} + +func (x *UploadAppAttachResponse) GetUserName() string { + if x != nil && x.UserName != nil { + return *x.UserName + } + return "" +} + +func (x *UploadAppAttachResponse) GetTotalLen() uint32 { + if x != nil && x.TotalLen != nil { + return *x.TotalLen + } + return 0 +} + +func (x *UploadAppAttachResponse) GetStartPos() uint32 { + if x != nil && x.StartPos != nil { + return *x.StartPos + } + return 0 +} + +func (x *UploadAppAttachResponse) GetDataLen() uint32 { + if x != nil && x.DataLen != nil { + return *x.DataLen + } + return 0 +} + +func (x *UploadAppAttachResponse) GetCreateTime() uint64 { + if x != nil && x.CreateTime != nil { + return *x.CreateTime + } + return 0 +} + +var File_upload_app_attach_proto protoreflect.FileDescriptor + +const file_upload_app_attach_proto_rawDesc = "" + + "\n" + + "\x17upload_app_attach.proto\x12\awxproto\x1a\rapp_msg.proto\x1a\x11common_resp.proto\">\n" + + "\x10SKBuiltinBufferT\x12\x12\n" + + "\x04iLen\x18\x01 \x01(\rR\x04iLen\x12\x16\n" + + "\x06buffer\x18\x02 \x01(\fR\x06buffer\"\xf6\x02\n" + + "\x16UploadAppAttachRequest\x129\n" + + "\vbaseRequest\x18\x01 \x01(\v2\x17.wxproto.ReplyMsgHeaderR\vbaseRequest\x12\x14\n" + + "\x05appId\x18\x02 \x01(\tR\x05appId\x12\x1e\n" + + "\n" + + "sdkVersion\x18\x03 \x01(\rR\n" + + "sdkVersion\x12(\n" + + "\x0fclientAppDataId\x18\x04 \x01(\tR\x0fclientAppDataId\x12\x1a\n" + + "\buserName\x18\x05 \x01(\tR\buserName\x12\x1a\n" + + "\btotalLen\x18\x06 \x01(\rR\btotalLen\x12\x1a\n" + + "\bstartPos\x18\a \x01(\rR\bstartPos\x12\x18\n" + + "\adataLen\x18\b \x01(\rR\adataLen\x12-\n" + + "\x04data\x18\t \x01(\v2\x19.wxproto.SKBuiltinBufferTR\x04data\x12\x12\n" + + "\x04type\x18\n" + + " \x01(\rR\x04type\x12\x10\n" + + "\x03md5\x18\v \x01(\tR\x03md5\"\xbc\x02\n" + + "\x17UploadAppAttachResponse\x129\n" + + "\fbaseResponse\x18\x01 \x01(\v2\x15.wxproto.BaseResponseR\fbaseResponse\x12\x14\n" + + "\x05appId\x18\x02 \x01(\tR\x05appId\x12\x18\n" + + "\amediaId\x18\x03 \x01(\tR\amediaId\x12(\n" + + "\x0fclientAppDataId\x18\x04 \x01(\tR\x0fclientAppDataId\x12\x1a\n" + + "\buserName\x18\x05 \x01(\tR\buserName\x12\x1a\n" + + "\btotalLen\x18\x06 \x01(\rR\btotalLen\x12\x1a\n" + + "\bstartPos\x18\a \x01(\rR\bstartPos\x12\x18\n" + + "\adataLen\x18\b \x01(\rR\adataLen\x12\x1e\n" + + "\n" + + "createTime\x18\t \x01(\x04R\n" + + "createTimeB>Z wxproto.ReplyMsgHeader + 0, // 1: wxproto.UploadAppAttachRequest.data:type_name -> wxproto.SKBuiltinBufferT + 4, // 2: wxproto.UploadAppAttachResponse.baseResponse:type_name -> wxproto.BaseResponse + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_upload_app_attach_proto_init() } +func file_upload_app_attach_proto_init() { + if File_upload_app_attach_proto != nil { + return + } + file_app_msg_proto_init() + file_common_resp_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_upload_app_attach_proto_rawDesc), len(file_upload_app_attach_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_upload_app_attach_proto_goTypes, + DependencyIndexes: file_upload_app_attach_proto_depIdxs, + MessageInfos: file_upload_app_attach_proto_msgTypes, + }.Build() + File_upload_app_attach_proto = out.File + file_upload_app_attach_proto_goTypes = nil + file_upload_app_attach_proto_depIdxs = nil +} diff --git a/onebot/proto/voice_msg.pb.go b/onebot/proto/voice_msg.pb.go new file mode 100644 index 0000000..7c39961 --- /dev/null +++ b/onebot/proto/voice_msg.pb.go @@ -0,0 +1,388 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v3.5.1 +// source: voice_msg.proto + +package wxproto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// 发送语音消息的protobuf结构 +type WxSendVoiceMsg struct { + state protoimpl.MessageState `protogen:"open.v1"` + FromUser string `protobuf:"bytes,1,opt,name=from_user,json=fromUser,proto3" json:"from_user,omitempty"` + ToUser string `protobuf:"bytes,2,opt,name=to_user,json=toUser,proto3" json:"to_user,omitempty"` + Unknown3 int32 `protobuf:"varint,3,opt,name=unknown3,proto3" json:"unknown3,omitempty"` + Unknown4 int32 `protobuf:"varint,4,opt,name=unknown4,proto3" json:"unknown4,omitempty"` + ClientMsgId string `protobuf:"bytes,5,opt,name=client_msg_id,json=clientMsgId,proto3" json:"client_msg_id,omitempty"` + // field 6 skipped + Duration int32 `protobuf:"varint,7,opt,name=duration,proto3" json:"duration,omitempty"` + // field 8 skipped + Unknown9 int32 `protobuf:"varint,9,opt,name=unknown9,proto3" json:"unknown9,omitempty"` + Header *VoiceMsgHeader `protobuf:"bytes,10,opt,name=header,proto3" json:"header,omitempty"` + Unknown11 int32 `protobuf:"varint,11,opt,name=unknown11,proto3" json:"unknown11,omitempty"` + // field 12 skipped + Unknown13 int32 `protobuf:"varint,13,opt,name=unknown13,proto3" json:"unknown13,omitempty"` + // field 14 skipped + Unknown15 int32 `protobuf:"varint,15,opt,name=unknown15,proto3" json:"unknown15,omitempty"` + Unknown16 int32 `protobuf:"varint,16,opt,name=unknown16,proto3" json:"unknown16,omitempty"` + Unknown17 int64 `protobuf:"varint,17,opt,name=unknown17,proto3" json:"unknown17,omitempty"` + // fields 18-19 skipped + AesKey []byte `protobuf:"bytes,20,opt,name=aes_key,json=aesKey,proto3" json:"aes_key,omitempty"` + CdnKey []byte `protobuf:"bytes,21,opt,name=cdn_key,json=cdnKey,proto3" json:"cdn_key,omitempty"` + // fields 22-23 skipped + Unknown24 []byte `protobuf:"bytes,24,opt,name=unknown24,proto3" json:"unknown24,omitempty"` + Unknown25 int32 `protobuf:"varint,25,opt,name=unknown25,proto3" json:"unknown25,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WxSendVoiceMsg) Reset() { + *x = WxSendVoiceMsg{} + mi := &file_voice_msg_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WxSendVoiceMsg) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WxSendVoiceMsg) ProtoMessage() {} + +func (x *WxSendVoiceMsg) ProtoReflect() protoreflect.Message { + mi := &file_voice_msg_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WxSendVoiceMsg.ProtoReflect.Descriptor instead. +func (*WxSendVoiceMsg) Descriptor() ([]byte, []int) { + return file_voice_msg_proto_rawDescGZIP(), []int{0} +} + +func (x *WxSendVoiceMsg) GetFromUser() string { + if x != nil { + return x.FromUser + } + return "" +} + +func (x *WxSendVoiceMsg) GetToUser() string { + if x != nil { + return x.ToUser + } + return "" +} + +func (x *WxSendVoiceMsg) GetUnknown3() int32 { + if x != nil { + return x.Unknown3 + } + return 0 +} + +func (x *WxSendVoiceMsg) GetUnknown4() int32 { + if x != nil { + return x.Unknown4 + } + return 0 +} + +func (x *WxSendVoiceMsg) GetClientMsgId() string { + if x != nil { + return x.ClientMsgId + } + return "" +} + +func (x *WxSendVoiceMsg) GetDuration() int32 { + if x != nil { + return x.Duration + } + return 0 +} + +func (x *WxSendVoiceMsg) GetUnknown9() int32 { + if x != nil { + return x.Unknown9 + } + return 0 +} + +func (x *WxSendVoiceMsg) GetHeader() *VoiceMsgHeader { + if x != nil { + return x.Header + } + return nil +} + +func (x *WxSendVoiceMsg) GetUnknown11() int32 { + if x != nil { + return x.Unknown11 + } + return 0 +} + +func (x *WxSendVoiceMsg) GetUnknown13() int32 { + if x != nil { + return x.Unknown13 + } + return 0 +} + +func (x *WxSendVoiceMsg) GetUnknown15() int32 { + if x != nil { + return x.Unknown15 + } + return 0 +} + +func (x *WxSendVoiceMsg) GetUnknown16() int32 { + if x != nil { + return x.Unknown16 + } + return 0 +} + +func (x *WxSendVoiceMsg) GetUnknown17() int64 { + if x != nil { + return x.Unknown17 + } + return 0 +} + +func (x *WxSendVoiceMsg) GetAesKey() []byte { + if x != nil { + return x.AesKey + } + return nil +} + +func (x *WxSendVoiceMsg) GetCdnKey() []byte { + if x != nil { + return x.CdnKey + } + return nil +} + +func (x *WxSendVoiceMsg) GetUnknown24() []byte { + if x != nil { + return x.Unknown24 + } + return nil +} + +func (x *WxSendVoiceMsg) GetUnknown25() int32 { + if x != nil { + return x.Unknown25 + } + return 0 +} + +type VoiceMsgHeader struct { + state protoimpl.MessageState `protogen:"open.v1"` + Flag []byte `protobuf:"bytes,1,opt,name=flag,proto3" json:"flag,omitempty"` + SessionId int64 `protobuf:"varint,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + ClientProof []byte `protobuf:"bytes,3,opt,name=client_proof,json=clientProof,proto3" json:"client_proof,omitempty"` + DeviceId int64 `protobuf:"varint,4,opt,name=device_id,json=deviceId,proto3" json:"device_id,omitempty"` + Platform []byte `protobuf:"bytes,5,opt,name=platform,proto3" json:"platform,omitempty"` + Version int32 `protobuf:"varint,6,opt,name=version,proto3" json:"version,omitempty"` + // field 7 skipped + Unknown8 int32 `protobuf:"varint,8,opt,name=unknown8,proto3" json:"unknown8,omitempty"` + // fields 9-14 skipped + Unknown15 int32 `protobuf:"varint,15,opt,name=unknown15,proto3" json:"unknown15,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VoiceMsgHeader) Reset() { + *x = VoiceMsgHeader{} + mi := &file_voice_msg_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VoiceMsgHeader) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VoiceMsgHeader) ProtoMessage() {} + +func (x *VoiceMsgHeader) ProtoReflect() protoreflect.Message { + mi := &file_voice_msg_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VoiceMsgHeader.ProtoReflect.Descriptor instead. +func (*VoiceMsgHeader) Descriptor() ([]byte, []int) { + return file_voice_msg_proto_rawDescGZIP(), []int{1} +} + +func (x *VoiceMsgHeader) GetFlag() []byte { + if x != nil { + return x.Flag + } + return nil +} + +func (x *VoiceMsgHeader) GetSessionId() int64 { + if x != nil { + return x.SessionId + } + return 0 +} + +func (x *VoiceMsgHeader) GetClientProof() []byte { + if x != nil { + return x.ClientProof + } + return nil +} + +func (x *VoiceMsgHeader) GetDeviceId() int64 { + if x != nil { + return x.DeviceId + } + return 0 +} + +func (x *VoiceMsgHeader) GetPlatform() []byte { + if x != nil { + return x.Platform + } + return nil +} + +func (x *VoiceMsgHeader) GetVersion() int32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *VoiceMsgHeader) GetUnknown8() int32 { + if x != nil { + return x.Unknown8 + } + return 0 +} + +func (x *VoiceMsgHeader) GetUnknown15() int32 { + if x != nil { + return x.Unknown15 + } + return 0 +} + +var File_voice_msg_proto protoreflect.FileDescriptor + +const file_voice_msg_proto_rawDesc = "" + + "\n" + + "\x0fvoice_msg.proto\x12\awxproto\"\x8f\x04\n" + + "\x0eWxSendVoiceMsg\x12\x1b\n" + + "\tfrom_user\x18\x01 \x01(\tR\bfromUser\x12\x17\n" + + "\ato_user\x18\x02 \x01(\tR\x06toUser\x12\x1a\n" + + "\bunknown3\x18\x03 \x01(\x05R\bunknown3\x12\x1a\n" + + "\bunknown4\x18\x04 \x01(\x05R\bunknown4\x12\"\n" + + "\rclient_msg_id\x18\x05 \x01(\tR\vclientMsgId\x12\x1a\n" + + "\bduration\x18\a \x01(\x05R\bduration\x12\x1a\n" + + "\bunknown9\x18\t \x01(\x05R\bunknown9\x12/\n" + + "\x06header\x18\n" + + " \x01(\v2\x17.wxproto.VoiceMsgHeaderR\x06header\x12\x1c\n" + + "\tunknown11\x18\v \x01(\x05R\tunknown11\x12\x1c\n" + + "\tunknown13\x18\r \x01(\x05R\tunknown13\x12\x1c\n" + + "\tunknown15\x18\x0f \x01(\x05R\tunknown15\x12\x1c\n" + + "\tunknown16\x18\x10 \x01(\x05R\tunknown16\x12\x1c\n" + + "\tunknown17\x18\x11 \x01(\x03R\tunknown17\x12\x17\n" + + "\aaes_key\x18\x14 \x01(\fR\x06aesKey\x12\x17\n" + + "\acdn_key\x18\x15 \x01(\fR\x06cdnKey\x12\x1c\n" + + "\tunknown24\x18\x18 \x01(\fR\tunknown24\x12\x1c\n" + + "\tunknown25\x18\x19 \x01(\x05R\tunknown25\"\xf3\x01\n" + + "\x0eVoiceMsgHeader\x12\x12\n" + + "\x04flag\x18\x01 \x01(\fR\x04flag\x12\x1d\n" + + "\n" + + "session_id\x18\x02 \x01(\x03R\tsessionId\x12!\n" + + "\fclient_proof\x18\x03 \x01(\fR\vclientProof\x12\x1b\n" + + "\tdevice_id\x18\x04 \x01(\x03R\bdeviceId\x12\x1a\n" + + "\bplatform\x18\x05 \x01(\fR\bplatform\x12\x18\n" + + "\aversion\x18\x06 \x01(\x05R\aversion\x12\x1a\n" + + "\bunknown8\x18\b \x01(\x05R\bunknown8\x12\x1c\n" + + "\tunknown15\x18\x0f \x01(\x05R\tunknown15B>Z wxproto.VoiceMsgHeader + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_voice_msg_proto_init() } +func file_voice_msg_proto_init() { + if File_voice_msg_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_voice_msg_proto_rawDesc), len(file_voice_msg_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_voice_msg_proto_goTypes, + DependencyIndexes: file_voice_msg_proto_depIdxs, + MessageInfos: file_voice_msg_proto_msgTypes, + }.Build() + File_voice_msg_proto = out.File + file_voice_msg_proto_goTypes = nil + file_voice_msg_proto_depIdxs = nil +} diff --git a/onebot/script.js b/onebot/script.js index 51c1af0..05470f1 100644 --- a/onebot/script.js +++ b/onebot/script.js @@ -93,6 +93,7 @@ function initAddresses() { setImmediate(setupSendTextMessageDynamic); setImmediate(setupSendFileMessageDynamic); setImmediate(setupSendFileUploadMessageDynamic); + setImmediate(setupSendAppAttachMessageDynamic); setImmediate(attachBlrX8Hook); setImmediate(AttachSendFunc); setImmediate(attachReq2buf); @@ -332,6 +333,8 @@ var replyProtoHexGlobal = ""; // 文件消息protobuf全局变量 (从Go直接传入hex编码) var fileProtoHexGlobal = ""; var fileUploadProtoHexGlobal = ""; +// uploadappattach protobuf全局变量 (从Go直接传入hex编码) +var appAttachProtoHexGlobal = ""; // 文件消息全局变量 var fileCgiAddr = ptr(0); @@ -345,6 +348,11 @@ var fileUploadCgiAddr = ptr(0); var sendFileUploadMessageAddr = ptr(0); var fileUploadMessageAddr = ptr(0); +// uploadappattach 全局变量 +var appAttachCgiAddr = ptr(0); +var sendAppAttachMessageAddr = ptr(0); +var appAttachMessageAddr = ptr(0); + // 回复消息全局变量 var replyMessageCallbackFunc; var replyCgiAddr = ptr(0); @@ -362,7 +370,7 @@ function setupSendTextMessageDynamic() { textCgiAddr = Memory.alloc(128); sendTextMessageAddr = Memory.alloc(256); textMessageAddr = Memory.alloc(256); - textProtoDataAddr = Memory.alloc(4096); + textProtoDataAddr = Memory.alloc(64 * 1024); // 支持 50KB 分片(uploadappattach)的 protobuf // A. 写入字符串内容 patchString(textCgiAddr, "/cgi-bin/micromsg-bin/newsendmsg"); @@ -452,6 +460,35 @@ function triggerSendFileUploadMessage(taskId, sender, receiver, protoHex, payloa return triggerSendMediaMessage(taskId, sender, receiver, protoHex, payloadHex, "fileupload"); } +// -------------------------uploadappattach分区------------------------- +function setupSendAppAttachMessageDynamic() { + appAttachCgiAddr = Memory.alloc(128); + sendAppAttachMessageAddr = Memory.alloc(256); + appAttachMessageAddr = Memory.alloc(256); + + patchString(appAttachCgiAddr, "/cgi-bin/micromsg-bin/uploadappattach"); + + sendAppAttachMessageAddr.add(0x00).writeU64(0); + sendAppAttachMessageAddr.add(0x08).writeU64(0); + sendAppAttachMessageAddr.add(0x10).writeU64(0); + sendAppAttachMessageAddr.add(0x18).writeU64(1); + sendAppAttachMessageAddr.add(0x20).writeU32(taskIdGlobal); + sendAppAttachMessageAddr.add(0x28).writePointer(appAttachMessageAddr); + + appAttachMessageAddr.add(0x00).writePointer(fakeVtable); + appAttachMessageAddr.add(0x08).writeU32(taskIdGlobal); + appAttachMessageAddr.add(0x0c).writeU32(0x6e); + appAttachMessageAddr.add(0x10).writeU64(0x3); + appAttachMessageAddr.add(0x18).writePointer(appAttachCgiAddr); + appAttachMessageAddr.add(0x20).writeU64(0x25); + appAttachMessageAddr.add(0x28).writeU64(uint64("0x8000000000000030")); + appAttachMessageAddr.add(0x30).writeU64(uint64("0x0000000001010100")); +} + +function triggerUploadAppAttach(taskId, sender, receiver, protoHex, payloadHex) { + return triggerSendMediaMessage(taskId, sender, receiver, protoHex, payloadHex, "appattach"); +} + // -------------------------发送文件消息分区------------------------- @@ -502,6 +539,8 @@ function attachBlrX8Hook() { protoHex = fileProtoHexGlobal; } else if (sendMsgType === "fileupload") { protoHex = fileUploadProtoHexGlobal; + } else if (sendMsgType === "appattach") { + protoHex = appAttachProtoHexGlobal; } else if (sendMsgType === "voice") { protoHex = voiceProtoHexGlobal; } @@ -587,6 +626,10 @@ function attachReq2buf() { insertMsgAddr.writePointer(sendFileUploadMessageAddr); console.log("[+] 发送fileUploadMsg成功! Req2Buf 已将 X24+0x60 指向新地址: " + sendFileUploadMessageAddr + "[+] Req2Buf 写入后内存预览: " + insertMsgAddr); + } else if (sendMsgType === "appattach") { + insertMsgAddr.writePointer(sendAppAttachMessageAddr); + console.log("[+] 发送uploadAppAttach成功! Req2Buf 已将 X24+0x60 指向新地址: " + sendAppAttachMessageAddr + + "[+] Req2Buf 写入后内存预览: " + insertMsgAddr); } } }); @@ -723,6 +766,7 @@ function triggerSendMediaMessage(taskId, sender, receiver, protoHex, payloadHex, "voice": { messageAddr: voiceMessageAddr, sendMessageAddr: sendVoiceMessageAddr, cgiAddr: voiceCgiAddr, protoHexSetter: function(h) { voiceProtoHexGlobal = h; } }, "file": { messageAddr: fileMessageAddr, sendMessageAddr: sendFileMessageAddr, cgiAddr: fileCgiAddr, protoHexSetter: function(h) { fileProtoHexGlobal = h; } }, "fileupload": { messageAddr: fileUploadMessageAddr, sendMessageAddr: sendFileUploadMessageAddr, cgiAddr: fileUploadCgiAddr, protoHexSetter: function(h) { fileUploadProtoHexGlobal = h; } }, + "appattach": { messageAddr: appAttachMessageAddr, sendMessageAddr: sendAppAttachMessageAddr, cgiAddr: appAttachCgiAddr, protoHexSetter: function(h) { appAttachProtoHexGlobal = h; } }, }; var info = msgAddrInfo[msgType]; @@ -998,6 +1042,7 @@ rpc.exports = { triggerSendFileMessage: triggerSendFileMessage, triggerSendFileUploadMessage: triggerSendFileUploadMessage, triggerUploadFile: triggerUploadFile, + triggerUploadAppAttach: triggerUploadAppAttach, }; // -------------------------发送图片消息分区------------------------- diff --git a/onebot/text_builder.go b/onebot/text_builder.go index 5dc1500..5043ecf 100644 --- a/onebot/text_builder.go +++ b/onebot/text_builder.go @@ -141,6 +141,11 @@ func BuildSendPayload(taskId int64, msgType string) string { payloadData[16] = 0x10 payloadData[28] = 0x27 payloadData[92] = 0x6E + case "appattach": + payloadData[0] = 0x6E + payloadData[16] = 0x10 + payloadData[28] = 0x25 + payloadData[92] = 0x6E case "voice": payloadData[0] = 0x6E payloadData[16] = 0x10 diff --git a/onebot/upload_app_builder.go b/onebot/upload_app_builder.go new file mode 100644 index 0000000..16a744f --- /dev/null +++ b/onebot/upload_app_builder.go @@ -0,0 +1,194 @@ +package main + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "os" + "time" + + "google.golang.org/protobuf/proto" + + wxproto "github.com/yincongcyincong/weixin-macos/onebot/proto" +) + +// uploadAppAttachChunkSize 每个分片大小 (参考 we-chat-ipad860 的 50000 字节) +const uploadAppAttachChunkSize = 50000 + +// BuildUploadAppAttachChunks 读取文件并按分片构造 uploadappattach 请求的 protobuf hex 列表。 +// 返回: 每个分片的 protobuf hex、填充好基础信息的 FileInfo、错误。 +// 参考 we-chat-ipad860 models/Tools/UploadApp.go 的 SendUploadAppAttach。 +func BuildUploadAppAttachChunks(receiver, filePath string) ([]string, *FileInfo, error) { + fileData, err := os.ReadFile(filePath) + if err != nil { + return nil, nil, fmt.Errorf("读取文件失败: %w", err) + } + + total := len(fileData) + if total == 0 { + return nil, nil, fmt.Errorf("文件为空: %s", filePath) + } + + sum := md5.Sum(fileData) + fileMd5 := hex.EncodeToString(sum[:]) + + // clientAppDataId: {receiver}_{ts}_UploadFile (对齐 iPad860) + clientAppDataId := fmt.Sprintf("%s_%d_UploadFile", receiver, time.Now().Unix()) + + fileInfo := &FileInfo{ + FileName: extractFileName(filePath), + TotalLen: int64(total), + FileExt: getFileExt(filePath), + Md5: fileMd5, + ClientMsgId: clientAppDataId, + } + + var hexChunks []string + for startPos := 0; startPos < total; startPos += uploadAppAttachChunkSize { + end := startPos + uploadAppAttachChunkSize + if end > total { + end = total + } + chunk := fileData[startPos:end] + + hexStr, err := buildUploadAppAttachChunk(receiver, clientAppDataId, fileMd5, + uint32(total), uint32(startPos), chunk) + if err != nil { + return nil, nil, err + } + hexChunks = append(hexChunks, hexStr) + } + + return hexChunks, fileInfo, nil +} + +// buildUploadAppAttachChunk 构造单个分片的 UploadAppAttachRequest protobuf hex。 +func buildUploadAppAttachChunk(receiver, clientAppDataId, fileMd5 string, + totalLen, startPos uint32, chunk []byte) (string, error) { + + version := NextVersion() + appId := "" + sdkVersion := uint32(0) + dataLen := uint32(len(chunk)) + fileType := uint32(6) // 6 = 文件 + + req := &wxproto.UploadAppAttachRequest{ + BaseRequest: &wxproto.ReplyMsgHeader{ + Flag: []byte{0x00}, + SessionId: &globalSessionId, + ClientProof: globalClientProof, + DeviceId: &globalDeviceId, + Platform: proto.String("UnifiedPCMac 26 arm64"), + Version: &version, + }, + AppId: &appId, + SdkVersion: &sdkVersion, + ClientAppDataId: &clientAppDataId, + UserName: &receiver, + TotalLen: &totalLen, + StartPos: &startPos, + DataLen: &dataLen, + Data: &wxproto.SKBuiltinBufferT{ + ILen: &dataLen, + Buffer: chunk, + }, + Type: &fileType, + Md5: &fileMd5, + } + + data, err := proto.Marshal(req) + if err != nil { + return "", fmt.Errorf("marshal uploadappattach chunk failed: %w", err) + } + + //fmt.Printf("[uploadappattach-proto] startPos=%d dataLen=%d totalLen=%d len=%d\n%s\n", + // startPos, dataLen, totalLen, len(data), HexDump(data, 0)) + + return hex.EncodeToString(data), nil +} + +// ParseUploadAppAttachResponse 解析 uploadappattach 响应,提取 mediaId(attachid)。 +func ParseUploadAppAttachResponse(data []byte) (string, error) { + // fmt.Printf("[uploadappattach-resp] len=%d\n%s\n", len(data), HexDump(data, 0)) + + resp := &wxproto.UploadAppAttachResponse{} + if err := proto.Unmarshal(data, resp); err != nil { + return "", fmt.Errorf("unmarshal uploadappattach 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("uploadappattach failed, ret=%d, errMsg=%s", + *resp.BaseResponse.Ret, errMsg) + } + + return resp.GetMediaId(), nil +} + +// BuildSimpleFileMsgProto 构建发送文件消息的 protobuf(uploadappattach 直传后用)。 +// 严格按 wechat7016 WXSendMsgFile / SendAppMsgRequest 格式: +// - 顶层 SendAppMsgReq{baseRequest, msg=AppMsgBody{...}} +// - AppMsgBody 只填 fromUserName/toUserName/type=6/content(XML)/clientMsgId/createTime +// - appmsg XML 极简:只有 title/type/appattach(totallen/attachid/fileext),不含 cdnattachurl +func BuildSimpleFileMsgProto(sender, receiver string, fileInfo *FileInfo) (string, error) { + now := time.Now().Unix() + + // wechat7016 WXSendMsgFile 的极简 XML(attachid 用完整 mediaId,不加 cdnattachurl) + xml := `` + "\n" + xml += `` + xml += `` + escapeXmlStr(fileInfo.FileName) + `` + xml += `` + xml += `` + xml += `6` + xml += `` + xml += `` + xml += `` + xml += `` + xml += `` + fmt.Sprintf("%d", fileInfo.TotalLen) + `` + xml += `` + escapeXmlStr(fileInfo.AttachId) + `` + xml += `` + escapeXmlStr(fileInfo.FileExt) + `` + xml += `` + xml += `` + xml += `` + + version := NextVersion() + appId := "" + sdkVersion := uint32(0) + msgType := uint32(6) // 6 = 文件 + clientMsgId := fmt.Sprintf("%d", now) + msgSource := "12" + + req := &wxproto.SendAppMsgReq{ + BaseRequest: &wxproto.ReplyMsgHeader{ + Flag: []byte{0x00}, + SessionId: &globalSessionId, + ClientProof: globalClientProof, + DeviceId: &globalDeviceId, + Platform: proto.String("UnifiedPCMac 26 arm64"), + Version: &version, + }, + Msg: &wxproto.AppMsgBody{ + FromUserName: &sender, + AppId: &appId, + SdkVersion: &sdkVersion, + ToUserName: &receiver, + Type: &msgType, + Content: &xml, + CreateTime: proto.Int64(now), + ClientMsgId: &clientMsgId, + MsgSource: &msgSource, + }, + } + + data, err := proto.Marshal(req) + if err != nil { + return "", fmt.Errorf("marshal SendAppMsgReq failed: %w", err) + } + + // fmt.Printf("[simple-file-proto] xml=%s\n[simple-file-proto] hex dump:\n%s\n", xml, HexDump(data, 0)) + + return hex.EncodeToString(data), nil +} diff --git a/onebot/upload_builder.go b/onebot/upload_builder.go index 7aae39d..7f373b1 100644 --- a/onebot/upload_builder.go +++ b/onebot/upload_builder.go @@ -119,3 +119,117 @@ func BuildUploadPayload(uploadType string) string { return hex.EncodeToString(payload) } + +// BuildVoiceUploadPayload 构建语音上传的payload模板,返回hex编码字符串 +// 基于WeChat实际内存dump校正 +// JS侧会在固定偏移写入运行时指针: 0x00, 0x08, 0x48(voiceIdAddr), 0x50(voiceId长度), 0x58(voiceId容量), 0x68(receiver), 0x100(audioData), 0x108(audioLen), 0x110(audioCap) +func BuildVoiceUploadPayload() string { + payload := []byte{ + // 0x00 (被uploadFunc1Addr覆盖) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x08 (被uploadFunc2Addr覆盖) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x10 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x18 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x20 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x28 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x30 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x38 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x40 + 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + // 0x48 (被voiceIdAddr覆盖) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x50 voiceId length (JS实时写入) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x58 voiceId capacity (JS实时写入) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, + // 0x60 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x68 (被receiver覆盖, inline string区域) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x70 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x78 [0x7F]=receiver length + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, + // 0x80 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x88 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x90 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x98 [0x9C]=0x0F voice type + 0x01, 0xAA, 0xAA, 0xAA, 0x0F, 0x00, 0x00, 0x00, + // 0xA0 + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + // 0xA8 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0xB0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0xB8 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0xC0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0xC8 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0xD0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0xD8 音频参数 + 0x10, 0x0E, 0x00, 0x00, 0x08, 0x07, 0x00, 0x00, + // 0xE0 文件大小(-1=未知) + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + // 0xE8 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0xF0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0xF8 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x100 (被voiceAudioDataAddr覆盖 - 音频数据指针) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x108 音频数据长度 (JS写入) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x110 音频数据容量 (JS写入, 带high bit) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, + // 0x118 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x120 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x128 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x130 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x138 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x140 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x148 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x150 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x158 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x160 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x168 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x170 [0x174]=0x01 + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + // 0x178 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x180 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x188 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x190 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x198 + 0x00, 0xAA, 0xAA, 0xAA, 0x01, 0x00, 0x00, 0x00, + } + + return hex.EncodeToString(payload) +} diff --git a/onebot/utils.go b/onebot/utils.go index c30b129..b6e6414 100644 --- a/onebot/utils.go +++ b/onebot/utils.go @@ -61,6 +61,199 @@ func SaveBase64Image(base64Data string) (string, string, error) { return targetPath, md5Str, nil } +// SaveVoiceFile 解码base64音频数据并保存为文件(不追加salt,保持二进制完整性) +// 返回原始字节、文件路径、错误 +func SaveVoiceFile(base64Data string) ([]byte, string, error) { + rawContents := base64Data + if strings.HasPrefix(base64Data, "base64://") { + rawContents = strings.TrimPrefix(base64Data, "base64://") + } else if idx := strings.Index(base64Data, ","); idx != -1 { + rawContents = base64Data[idx+1:] + } + + data, err := base64.StdEncoding.DecodeString(rawContents) + if err != nil { + return nil, "", fmt.Errorf("base64 decode failed: %v", err) + } + + r := rand.New(rand.NewSource(time.Now().UnixNano())) + randomNumber := r.Intn(1000) + timestamp := time.Now().Unix() + ext := DetectFileFormat(data) + fileName := fmt.Sprintf("%d_%d.%s", randomNumber, timestamp, ext) + targetPath := config.ImagePath + fileName + dir := filepath.Dir(targetPath) + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, "", fmt.Errorf("create directory failed: %v", err) + } + + err = os.WriteFile(targetPath, data, 0666) + if err != nil { + return nil, "", fmt.Errorf("write file failed: %v", err) + } + os.Chmod(targetPath, 0666) + + return data, targetPath, nil +} + +// SaveBase64File 解码 base64 数据并以指定扩展名保存文件,返回文件路径和 MD5 +func SaveBase64File(base64Data string, ext string) (string, string, error) { + rawContents := base64Data + if strings.HasPrefix(base64Data, "base64://") { + rawContents = strings.TrimPrefix(base64Data, "base64://") + } else if idx := strings.Index(base64Data, ","); idx != -1 { + rawContents = base64Data[idx+1:] + } + + data, err := base64.StdEncoding.DecodeString(rawContents) + if err != nil { + return "", "", fmt.Errorf("base64 decode failed: %v", err) + } + + // 如果没有传入扩展名,尝试自动检测 + if ext == "" { + ext = DetectFileFormat(data) + if ext == "unknown" { + // fallback: 用 MIME 类型推断 + mimeType := http.DetectContentType(data) + ext = mimeToExt(mimeType) + } + } + + r := rand.New(rand.NewSource(time.Now().UnixNano())) + randomNumber := r.Intn(1000) + timestamp := time.Now().Unix() + fileName := fmt.Sprintf("%d_%d.%s", randomNumber, timestamp, ext) + targetPath := config.ImagePath + fileName + dir := filepath.Dir(targetPath) + if err := os.MkdirAll(dir, 0755); err != nil { + return "", "", fmt.Errorf("create directory failed: %v", err) + } + + err = os.WriteFile(targetPath, data, 0666) + if err != nil { + return "", "", fmt.Errorf("write file failed: %v", err) + } + os.Chmod(targetPath, 0666) + + md5Str, err := GetFileMD5(targetPath) + if err != nil { + return "", "", fmt.Errorf("get file md5 failed: %v", err) + } + + return targetPath, md5Str, nil +} + +// mimeToExt 将 MIME 类型转换为文件扩展名 +func mimeToExt(mimeType string) string { + if idx := strings.Index(mimeType, ";"); idx != -1 { + mimeType = strings.TrimSpace(mimeType[:idx]) + } + switch mimeType { + case "text/plain": + return "txt" + case "text/html": + return "html" + case "text/xml", "application/xml": + return "xml" + case "application/json": + return "json" + case "application/pdf": + return "pdf" + case "application/zip": + return "zip" + case "application/gzip": + return "gz" + case "image/jpeg": + return "jpg" + case "image/png": + return "png" + case "image/gif": + return "gif" + case "image/webp": + return "webp" + case "video/mp4": + return "mp4" + case "audio/mpeg": + return "mp3" + default: + return "bin" + } +} + +// ConvertToSilk 将音频数据(任意格式)通过ffmpeg转为PCM,再编码为SILK格式 +// 微信要求格式: \x02#!SILK_V3 开头 (tencent silk) +// 如果输入已经是该格式,则直接返回 +// 返回: silkData, 时长(毫秒), error +func ConvertToSilk(audioData []byte) ([]byte, int32, error) { + // 已经是tencent SILK格式 (\x02#!SILK_V3),直接返回,时长未知设为0 + if len(audioData) > 10 && audioData[0] == 0x02 && bytes.HasPrefix(audioData[1:], []byte("#!SILK_V3")) { + return audioData, 0, nil + } + + // 先用ffmpeg将输入音频转为PCM (s16le, 16000Hz, mono) + cmd := exec.Command("ffmpeg", + "-i", "pipe:0", + "-f", "s16le", + "-ar", "16000", + "-ac", "1", + "pipe:1", + ) + cmd.Stdin = bytes.NewReader(audioData) + + var out bytes.Buffer + cmd.Stdout = &out + var stderr bytes.Buffer + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + return nil, 0, fmt.Errorf("ffmpeg to pcm error: %v, details: %s", err, stderr.String()) + } + + pcmBytes := out.Bytes() + // 时长(ms) = pcm字节数 * 1000 / (采样率 * 通道数 * 每样本字节数) + durationMs := int32(int64(len(pcmBytes)) * 1000 / (16000 * 2)) + + // 尝试使用外部silk-encoder(和微信兼容性更好) + silkData, err := encodeSilkExternal(pcmBytes) + if err != nil { + // fallback: 使用go-silk库 + silkData, err = silk.EncodePcmBuffToSilk(pcmBytes, 16000, 16000, true) + if err != nil { + return nil, 0, fmt.Errorf("encode silk error: %v", err) + } + } + + return silkData, durationMs, nil +} + +// encodeSilkExternal 使用外部pilk(Python)工具编码pcm->silk(和微信兼容) +func encodeSilkExternal(pcmBytes []byte) ([]byte, error) { + tmpPcm, err := os.CreateTemp("", "voice_*.pcm") + if err != nil { + return nil, err + } + defer os.Remove(tmpPcm.Name()) + + if _, err := tmpPcm.Write(pcmBytes); err != nil { + tmpPcm.Close() + return nil, err + } + tmpPcm.Close() + + tmpSilk := tmpPcm.Name() + ".silk" + defer os.Remove(tmpSilk) + + pyScript := fmt.Sprintf(`import pilk; pilk.encode("%s", "%s", pcm_rate=16000, tencent=True)`, tmpPcm.Name(), tmpSilk) + cmd := exec.Command("python3", "-c", pyScript) + var stderr bytes.Buffer + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("pilk encode failed: %v, %s", err, stderr.String()) + } + return os.ReadFile(tmpSilk) +} + // GetVideoDuration 使用ffprobe获取视频时长(秒) func GetVideoDuration(filePath string) (int32, error) { cmd := exec.Command("ffprobe", diff --git a/onebot/voice_builder.go b/onebot/voice_builder.go new file mode 100644 index 0000000..184d669 --- /dev/null +++ b/onebot/voice_builder.go @@ -0,0 +1,67 @@ +package main + +import ( + "encoding/hex" + "fmt" + "time" + + "github.com/google/uuid" + wxproto "github.com/yincongcyincong/weixin-macos/onebot/proto" + "google.golang.org/protobuf/proto" +) + +// BuildVoiceMsgProto 构建发送语音消息的protobuf并返回hex编码的字符串 +func BuildVoiceMsgProto(sender, targetId, cdnKey, aesKey string, voiceDuration int32, silkDataLen int32, unknown13 int32) (string, error) { + // client_msg_id: UUID格式 + clientMsgId := uuid.New().String() + + msg := &wxproto.WxSendVoiceMsg{ + FromUser: sender, + ToUser: targetId, + Unknown3: 0, + Unknown4: silkDataLen, + ClientMsgId: clientMsgId, + Duration: voiceDuration, + Unknown9: 1, + Header: &wxproto.VoiceMsgHeader{ + Flag: []byte{0x00}, + SessionId: int64(globalSessionId), + ClientProof: globalClientProof, + DeviceId: int64(globalDeviceId), + Platform: []byte("UnifiedPCMac 26 arm64"), + Version: int32(NextVersion()), + Unknown8: 4, + Unknown15: 0, + }, + Unknown11: 0, + Unknown13: 4, + Unknown15: 0, + Unknown16: 0, + Unknown17: time.Now().Unix(), + CdnKey: []byte(cdnKey), + AesKey: []byte(aesKey), + Unknown24: []byte{}, + Unknown25: 0, + } + + data, err := proto.Marshal(msg) + if err != nil { + return "", fmt.Errorf("marshal voice proto failed: %w", err) + } + + // 手动追加proto3不会序列化的0值字段 + // field 3 (unknown3=0) + data = appendZeroVarintField(data, 3) + // field 11 (unknown11=0) + data = appendZeroVarintField(data, 11) + // field 15 (unknown15=0) + data = appendZeroVarintField(data, 15) + // field 16 (unknown16=0) + data = appendZeroVarintField(data, 16) + // field 25 (unknown25=0) + data = appendZeroVarintField(data, 25) + + //fmt.Printf("[voice-proto] final protobuf hex dump:\n%s\n", HexDump(data, 0)) + + return hex.EncodeToString(data), nil +} diff --git a/onebot/worker.go b/onebot/worker.go index 65b6b95..6a2a388 100644 --- a/onebot/worker.go +++ b/onebot/worker.go @@ -195,6 +195,126 @@ func SendWechatMsg(m *SendMsg) { sendErr = errors.New("send reply failed") return } + case "voice": + // 直接base64解码,不追加salt(音频二进制不能被修改) + rawAudio, targetPath, err := SaveVoiceFile(m.Content) + if err != nil { + Error("保存语音文件失败", "err", err) + sendErr = err + return + } + + // 转换为SILK格式 + silkData, voiceDurationMs, err := ConvertToSilk(rawAudio) + if err != nil { + Error("转换SILK格式失败", "err", err) + sendErr = err + return + } + + audioHex := hex.EncodeToString(silkData) + + uploadPayloadHex := BuildVoiceUploadPayload() + result := fridaScript.ExportsCall("triggerUploadVoice", targetId, targetPath, uploadPayloadHex, audioHex, voiceDurationMs) + Info("📩 上传语音任务执行结果", "result", result, "target_id", targetId, "path", targetPath, "silk_len", len(silkData), "duration_ms", voiceDurationMs) + if result != "0" { + Error("上传语音失败", "target_id", targetId, "result", result) + sendErr = errors.New("upload voice failed") + return + } + if m.ResultChan != nil { + pendingResultMap.Store(targetId, m.ResultChan) + m.ResultChan = nil + } + return + case "send_voice": + protoHex, err := BuildVoiceMsgProto(myWechatId, targetId, m.CdnKey, m.AesKey, m.VoiceDuration, m.SilkDataLen, m.Unknown13) + if err != nil { + Error("构建语音protobuf失败", "err", err) + sendErr = err + return + } + payloadHex := BuildSendPayload(currTaskId, "voice") + result := fridaScript.ExportsCall("triggerSendVoiceMessage", currTaskId, myWechatId, targetId, protoHex, payloadHex) + Info("📩 发送语音任务执行结果", "result", result, "task_id", currTaskId, "wechat_id", myWechatId, "target_id", targetId, "unknown13", m.Unknown13) + if result != "1" { + Error("发送语音失败", "task_id", currTaskId, "target_id", targetId, "result", result) + sendErr = errors.New("send voice failed") + return + } + case "send_file_simple": + // iPad860 风格: uploadappattach 分片直传 → sendappmsg,不走 CDN。 + // 文件名/扩展名由内容自动识别 + 时间戳随机生成(SaveBase64File 内部完成)。 + targetPath, _, err := SaveBase64File(m.Content, "") + if err != nil { + Error("保存文件失败", "err", err) + sendErr = err + return + } + + chunks, fileInfo, err := BuildUploadAppAttachChunks(targetId, targetPath) + if err != nil { + Error("构建uploadappattach分片失败", "err", err) + sendErr = err + return + } + Info("📩 开始uploadappattach直传", "target_id", targetId, "chunks", len(chunks), + "file_name", fileInfo.FileName, "file_ext", fileInfo.FileExt, + "total_len", fileInfo.TotalLen, "md5", fileInfo.Md5) + + var attachId string + for i, chunkHex := range chunks { + chunkTaskId := atomic.AddInt64(&taskId, 1) + payloadHex := BuildSendPayload(chunkTaskId, "appattach") + result := fridaScript.ExportsCall("triggerUploadAppAttach", chunkTaskId, myWechatId, targetId, chunkHex, payloadHex) + if result != "1" { + Error("uploadappattach分片发送失败", "chunk", i, "result", result) + sendErr = errors.New("upload app attach chunk failed") + return + } + + select { + case <-ctx.Done(): + Error("等待uploadappattach响应超时", "chunk", i) + sendErr = errors.New("upload app attach timeout") + return + case data := <-appAttachRespChan: + id, perr := ParseUploadAppAttachResponse(data) + if perr != nil { + Error("解析uploadappattach响应失败", "chunk", i, "err", perr) + sendErr = perr + return + } + if id != "" { + attachId = id + } + Info("📩 uploadappattach分片完成", "chunk", i, "attach_id", id) + } + } + + if attachId == "" { + Error("uploadappattach未返回attachId", "target_id", targetId) + sendErr = errors.New("upload app attach no attachId") + return + } + fileInfo.AttachId = attachId + + // sendappmsg (type=6),精简版 appmsg,cdnattachurl 也填 attachId + currTaskId = atomic.AddInt64(&taskId, 1) + protoHex, err := BuildSimpleFileMsgProto(myWechatId, targetId, fileInfo) + if err != nil { + Error("构建文件protobuf失败", "err", err) + sendErr = err + return + } + payloadHex := BuildSendPayload(currTaskId, "file") + result := fridaScript.ExportsCall("triggerSendFileMessage", currTaskId, myWechatId, targetId, protoHex, payloadHex) + Info("📩 发送文件消息(simple)执行结果", "result", result, "task_id", currTaskId, "target_id", targetId) + if result != "1" { + Error("发送文件失败(simple)", "task_id", currTaskId, "target_id", targetId, "result", result) + sendErr = errors.New("send file failed") + return + } } select { @@ -383,6 +503,13 @@ func HandleBuf2Resp(msgType string, data []byte) { return } + switch msgType { + case "appattach": + Info("收到uploadappattach响应", "data_len", len(data)) + appAttachRespChan <- data + return + } + ret, errMsg, err := ParseSendMsgResponse(data) if err != nil { Info("buf2resp响应无法提取错误码,视为成功", "msg_type", msgType, "err", err)