diff --git a/util/rsa.go b/util/rsa.go new file mode 100644 index 0000000..528ed14 --- /dev/null +++ b/util/rsa.go @@ -0,0 +1,43 @@ +package util + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" +) + +// RSADecrypt 数据解密 +func RSADecrypt(privateKey string, ciphertext []byte) ([]byte, error) { + block, _ := pem.Decode([]byte(privateKey)) + if block == nil { + return nil, errors.New("PrivateKey format error") + } + priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + oldErr := err + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("ParsePKCS1PrivateKey error: %s, ParsePKCS8PrivateKey error: %s", oldErr.Error(), err.Error()) + } + switch t := key.(type) { + case *rsa.PrivateKey: + priv = key.(*rsa.PrivateKey) + default: + return nil, fmt.Errorf("ParsePKCS1PrivateKey error: %s, ParsePKCS8PrivateKey error: Not supported privatekey format, should be *rsa.PrivateKey, got %T", oldErr.Error(), t) + } + } + return rsa.DecryptPKCS1v15(rand.Reader, priv, ciphertext) +} + +// RSADecryptBase64 Base64解码后再次进行RSA解密 +func RSADecryptBase64(privateKey string, cryptoText string) ([]byte, error) { + encryptedData, err := base64.StdEncoding.DecodeString(cryptoText) + if err != nil { + return nil, err + } + return RSADecrypt(privateKey, encryptedData) +} diff --git a/work/config/config.go b/work/config/config.go index 2bdc393..f4f35ee 100644 --- a/work/config/config.go +++ b/work/config/config.go @@ -7,8 +7,9 @@ import ( // Config config for 企业微信 type Config struct { - CorpID string `json:"corp_id"` // corp_id - CorpSecret string `json:"corp_secret"` // corp_secret - AgentID string `json:"agent_id"` // agent_id - Cache cache.Cache + CorpID string `json:"corp_id"` // corp_id + CorpSecret string `json:"corp_secret"` // corp_secret,如果需要获取会话存档实例,当前参数请填写聊天内容存档的Secret,可以在企业微信管理端--管理工具--聊天内容存档查看 + AgentID string `json:"agent_id"` // agent_id + Cache cache.Cache + RasPrivateKey string // 消息加密私钥,可以在企业微信管理端--管理工具--消息加密公钥查看对用公钥,私钥一般由自己保存 } diff --git a/work/msgaudit/README.md b/work/msgaudit/README.md new file mode 100644 index 0000000..007cf6d --- /dev/null +++ b/work/msgaudit/README.md @@ -0,0 +1,95 @@ +企业微信会话存档SDK(基于企业微信C版官方SDK封装),暂时只支持在`linux`环境下使用当前SDK。 + +### 官方文档地址 +https://open.work.weixin.qq.com/api/doc/90000/90135/91774 + +### 使用方式 + +1、安装 go module +> go get -u github.com/silenceper/wechat/v2 + +2、从 `github.com/silenceper/wechat/v2/work/msgaudit/lib` 文件夹下复制 `libWeWorkFinanceSdk_C.so` 动态库文件到系统动态链接库默认文件夹下,或者复制到任意文件夹并在当前文件夹下执行 `export LD_LIBRARY_PATH=$(pwd)`命令设置动态链接库检索地址后即可正常使用 + +### Example + +```go +package main + +import ( + "bytes" + "fmt" + "github.com/silenceper/wechat/v2" + "github.com/silenceper/wechat/v2/work/msgaudit" + "github.com/silenceper/wechat/v2/work/config" + "io/ioutil" + "os" + "path" +) + +func main() { + //初始化客户端 + wechatClient := wechat.NewWechat() + + workClient := wechatClient.NewWork(&config.Config{ + CorpID: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + CorpSecret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + RasPrivateKey: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + }) + + client, err := workClient.GetMsgAudit() + if err != nil { + fmt.Printf("SDK 初始化失败:%v \n", err) + return + } + + //同步消息 + chatDataList, err := client.GetChatData(0, 100, "", "", 3) + if err != nil { + fmt.Printf("消息同步失败:%v \n", err) + return + } + + for _, chatData := range chatDataList { + //消息解密 + chatInfo, err := client.DecryptData(chatData.EncryptRandomKey, chatData.EncryptChatMsg) + if err != nil { + fmt.Printf("消息解密失败:%v \n", err) + return + } + + if chatInfo.Type == "image" { + image, _ := chatInfo.GetImageMessage() + sdkfileid := image.Image.SdkFileId + + isFinish := false + buffer := bytes.Buffer{} + for !isFinish { + //获取媒体数据 + mediaData, err := client.GetMediaData("", sdkfileid, "", "", 5) + if err != nil { + fmt.Printf("媒体数据拉取失败:%v \n", err) + return + } + buffer.Write(mediaData.Data) + if mediaData.IsFinish { + isFinish = mediaData.IsFinish + } + } + filePath, _ := os.Getwd() + filePath = path.Join(filePath, "test.png") + err := ioutil.WriteFile(filePath, buffer.Bytes(), 0666) + if err != nil { + fmt.Printf("文件存储失败:%v \n", err) + return + } + break + } + } + + //释放SDK实例 + client.Free() +} + + + +``` diff --git a/work/msgaudit/chat.go b/work/msgaudit/chat.go new file mode 100644 index 0000000..229282f --- /dev/null +++ b/work/msgaudit/chat.go @@ -0,0 +1,207 @@ +package msgaudit + +import "encoding/json" + +// ChatDataResponse 会话存档消息响应数据 +type ChatDataResponse struct { + Error + ChatDataList []ChatData `json:"chatdata,omitempty"` +} + +// IsError 判断是否正确响应 +func (c ChatDataResponse) IsError() bool { + return c.ErrCode != 0 +} + +// ChatData 会话存档原始数据 +type ChatData struct { + Seq uint64 `json:"seq,omitempty"` // 消息的seq值,标识消息的序号。再次拉取需要带上上次回包中最大的seq。Uint64类型,范围0-pow(2,64)-1 + MsgID string `json:"msgid,omitempty"` // 消息id,消息的唯一标识,企业可以使用此字段进行消息去重。 + PublickeyVer uint32 `json:"publickey_ver,omitempty"` // 加密此条消息使用的公钥版本号。 + EncryptRandomKey string `json:"encrypt_random_key,omitempty"` // 使用publickey_ver指定版本的公钥进行非对称加密后base64加密的内容,需要业务方先base64 decode处理后,再使用指定版本的私钥进行解密,得出内容。 + EncryptChatMsg string `json:"encrypt_chat_msg,omitempty"` // 消息密文。需要业务方使用将encrypt_random_key解密得到的内容,与encrypt_chat_msg,传入sdk接口DecryptData,得到消息明文。 +} + +// ChatMessage 会话存档消息 +type ChatMessage struct { + ID string // 消息id,消息的唯一标识,企业可以使用此字段进行消息去重。 + From string // 消息发送方id。同一企业内容为userid,非相同企业为external_userid。消息如果是机器人发出,也为external_userid。 + ToList []string // 消息接收方列表,可能是多个,同一个企业内容为userid,非相同企业为external_userid。 + Action string // 消息动作,目前有send(发送消息)/recall(撤回消息)/switch(切换企业日志)三种类型。 + Type string // 消息类型 + originData []byte // 原始消息对象 +} + +// GetOriginMessage 获取消息原始数据 +func (c ChatMessage) GetOriginMessage() (msg map[string]interface{}, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetTextMessage 获取文本消息 +func (c ChatMessage) GetTextMessage() (msg TextMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetImageMessage 获取图片消息 +func (c ChatMessage) GetImageMessage() (msg ImageMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetRevokeMessage 获取撤回消息 +func (c ChatMessage) GetRevokeMessage() (msg RevokeMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetAgreeMessage 获取同意会话聊天内容 +func (c ChatMessage) GetAgreeMessage() (msg AgreeMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetVoiceMessage 获取语音消息 +func (c ChatMessage) GetVoiceMessage() (msg VoiceMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetVideoMessage 获取视频消息 +func (c ChatMessage) GetVideoMessage() (msg VideoMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetCardMessage 获取名片消息 +func (c ChatMessage) GetCardMessage() (msg CardMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetLocationMessage 获取位置消息 +func (c ChatMessage) GetLocationMessage() (msg LocationMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetEmotionMessage 获取表情消息 +func (c ChatMessage) GetEmotionMessage() (msg EmotionMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetFileMessage 获取文件消息 +func (c ChatMessage) GetFileMessage() (msg FileMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetLinkMessage 获取链接消息 +func (c ChatMessage) GetLinkMessage() (msg LinkMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetWeappMessage 获取小程序消息 +func (c ChatMessage) GetWeappMessage() (msg WeappMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetChatRecordMessage 获取会话记录消息 +func (c ChatMessage) GetChatRecordMessage() (msg ChatRecordMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetTodoMessage 获取待办消息 +func (c ChatMessage) GetTodoMessage() (msg TodoMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetVoteMessage 获取投票消息 +func (c ChatMessage) GetVoteMessage() (msg VoteMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetCollectMessage 获取填表消息 +func (c ChatMessage) GetCollectMessage() (msg CollectMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetRedpacketMessage 获取红包消息 +func (c ChatMessage) GetRedpacketMessage() (msg RedpacketMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetMeetingMessage 获取会议邀请消息 +func (c ChatMessage) GetMeetingMessage() (msg MeetingMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetDocMessage 获取在线文档消息 +func (c ChatMessage) GetDocMessage() (msg DocMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetMarkdownMessage 获取MarkDown格式消息 +func (c ChatMessage) GetMarkdownMessage() (msg MarkdownMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetNewsMessage 获取图文消息 +func (c ChatMessage) GetNewsMessage() (msg NewsMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetCalendarMessage 获取日程消息 +func (c ChatMessage) GetCalendarMessage() (msg CalendarMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetMixedMessage 获取混合消息 +func (c ChatMessage) GetMixedMessage() (msg MixedMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetMeetingVoiceCallMessage 获取音频存档消息 +func (c ChatMessage) GetMeetingVoiceCallMessage() (msg MeetingVoiceCallMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetVoipDocShareMessage 获取音频共享消息 +func (c ChatMessage) GetVoipDocShareMessage() (msg VoipDocShareMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetExternalRedPacketMessage 获取互通红包消息 +func (c ChatMessage) GetExternalRedPacketMessage() (msg ExternalRedPacketMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetSphFeedMessage 获取视频号消息 +func (c ChatMessage) GetSphFeedMessage() (msg SphFeedMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} + +// GetSwitchMessage 获取切换企业日志 +func (c ChatMessage) GetSwitchMessage() (msg SwitchMessage, err error) { + err = json.Unmarshal(c.originData, &msg) + return msg, err +} diff --git a/work/msgaudit/client.go b/work/msgaudit/client.go new file mode 100644 index 0000000..1199b93 --- /dev/null +++ b/work/msgaudit/client.go @@ -0,0 +1,260 @@ +package msgaudit + +// #cgo LDFLAGS: -L${SRCDIR}/lib -lWeWorkFinanceSdk_C +// #cgo CFLAGS: -I ./lib/ +// #include +// #include "WeWorkFinanceSdk_C.h" +import "C" +import ( + "encoding/json" + "github.com/silenceper/wechat/v2/util" + "github.com/silenceper/wechat/v2/work/config" + "unsafe" +) + +// Client 会话存档 +type Client struct { + ptr *C.WeWorkFinanceSdk_t + privateKey string +} + +// NewClient 初始会话会话存档实例 +/** +* 初始化函数 +* Return值=0表示该API调用成功 +* +* @param [in] sdk NewSdk返回的sdk指针 +* @param [in] corpid 调用企业的企业id,例如:wwd08c8exxxx5ab44d,可以在企业微信管理端--我的企业--企业信息查看 +* @param [in] secret 聊天内容存档的Secret,可以在企业微信管理端--管理工具--聊天内容存档查看 +* @param [in] privateKey 消息加密私钥,可以在企业微信管理端--管理工具--消息加密公钥查看对用公钥,私钥一般由自己保存 +* +* +* @return 返回是否初始化成功 +* 0 - 成功 +* !=0 - 失败 + */ +func NewClient(cfg *config.Config) (*Client, error) { + ptr := C.NewSdk() + corpIDC := C.CString(cfg.CorpID) + corpSecretC := C.CString(cfg.CorpSecret) + defer func() { + C.free(unsafe.Pointer(corpIDC)) + C.free(unsafe.Pointer(corpSecretC)) + }() + retC := C.Init(ptr, corpIDC, corpSecretC) + ret := int(retC) + if ret != 0 { + return nil, NewSDKErr(ret) + } + return &Client{ + ptr: ptr, + privateKey: cfg.RasPrivateKey, + }, nil +} + +// Free 释放SDK实例是可调用该方法释放内存 +func (s *Client) Free() { + if s.ptr == nil { + return + } + C.DestroySdk(s.ptr) + s.ptr = nil +} + +// GetChatData 拉取聊天记录函数 +/** +* 拉取聊天记录函数 +* +* +* @param [in] seq 从指定的seq开始拉取消息,注意的是返回的消息从seq+1开始返回,seq为之前接口返回的最大seq值。首次使用请使用seq:0 +* @param [in] limit 一次拉取的消息条数,最大值1000条,超过1000条会返回错误 +* @param [in] proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081 +* @param [in] passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123 +* @param [in] timeout 超时时间,单位秒 +* @return chatDatas 返回本次拉取消息的数据,slice结构体.内容包括errcode/errmsg,以及每条消息内容。示例如下: + +{"errcode":0,"errmsg":"ok","chatdata":[{"seq":196,"msgid":"CAQQ2fbb4QUY0On2rYSAgAMgip/yzgs=","publickey_ver":3,"encrypt_random_key":"ftJ+uz3n/z1DsxlkwxNgE+mL38H42/KCvN8T60gbbtPD+Rta1hKTuQPzUzO6Hzne97MgKs7FfdDxDck/v8cDT6gUVjA2tZ/M7euSD0L66opJ/IUeBtpAtvgVSD5qhlaQjvfKJc/zPMGNK2xCLFYqwmQBZXbNT7uA69Fflm512nZKW/piK2RKdYJhRyvQnA1ISxK097sp9WlEgDg250fM5tgwMjujdzr7ehK6gtVBUFldNSJS7ndtIf6aSBfaLktZgwHZ57ONewWq8GJe7WwQf1hwcDbCh7YMG8nsweEwhDfUz+u8rz9an+0lgrYMZFRHnmzjgmLwrR7B/32Qxqd79A==","encrypt_chat_msg":"898WSfGMnIeytTsea7Rc0WsOocs0bIAerF6de0v2cFwqo9uOxrW9wYe5rCjCHHH5bDrNvLxBE/xOoFfcwOTYX0HQxTJaH0ES9OHDZ61p8gcbfGdJKnq2UU4tAEgGb8H+Q9n8syRXIjaI3KuVCqGIi4QGHFmxWenPFfjF/vRuPd0EpzUNwmqfUxLBWLpGhv+dLnqiEOBW41Zdc0OO0St6E+JeIeHlRZAR+E13Isv9eS09xNbF0qQXWIyNUi+ucLr5VuZnPGXBrSfvwX8f0QebTwpy1tT2zvQiMM2MBugKH6NuMzzuvEsXeD+6+3VRqL"}]} +*/ +func (s *Client) GetChatData(seq uint64, limit uint64, proxy string, passwd string, timeout int) ([]ChatData, error) { + proxyC := C.CString(proxy) + passwdC := C.CString(passwd) + chatSlice := C.NewSlice() + defer func() { + C.free(unsafe.Pointer(proxyC)) + C.free(unsafe.Pointer(passwdC)) + C.FreeSlice(chatSlice) + }() + + if s.ptr == nil { + return nil, NewSDKErr(10002) + } + + retC := C.GetChatData(s.ptr, C.ulonglong(seq), C.uint(limit), proxyC, passwdC, C.int(timeout), chatSlice) + ret := int(retC) + if ret != 0 { + return nil, NewSDKErr(ret) + } + buf := s.GetContentFromSlice(chatSlice) + + var data ChatDataResponse + err := json.Unmarshal(buf, &data) + if err != nil { + return nil, err + } + if data.ErrCode != 0 { + return nil, data.Error + } + return data.ChatDataList, nil +} + +// GetRawChatData 拉取聊天记录函数 +/** +* 拉取聊天记录函数 +* +* +* @param [in] seq 从指定的seq开始拉取消息,注意的是返回的消息从seq+1开始返回,seq为之前接口返回的最大seq值。首次使用请使用seq:0 +* @param [in] limit 一次拉取的消息条数,最大值1000条,超过1000条会返回错误 +* @param [in] proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081 +* @param [in] passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123 +* @param [in] timeout 超时时间,单位秒 +* @return chatDatas 返回本次拉取消息的数据,slice结构体.内容包括errcode/errmsg,以及每条消息内容。示例如下: + +{"errcode":0,"errmsg":"ok","chatdata":[{"seq":196,"msgid":"CAQQ2fbb4QUY0On2rYSAgAMgip/yzgs=","publickey_ver":3,"encrypt_random_key":"ftJ+uz3n/z1DsxlkwxNgE+mL38H42/KCvN8T60gbbtPD+Rta1hKTuQPzUzO6Hzne97MgKs7FfdDxDck/v8cDT6gUVjA2tZ/M7euSD0L66opJ/IUeBtpAtvgVSD5qhlaQjvfKJc/zPMGNK2xCLFYqwmQBZXbNT7uA69Fflm512nZKW/piK2RKdYJhRyvQnA1ISxK097sp9WlEgDg250fM5tgwMjujdzr7ehK6gtVBUFldNSJS7ndtIf6aSBfaLktZgwHZ57ONewWq8GJe7WwQf1hwcDbCh7YMG8nsweEwhDfUz+u8rz9an+0lgrYMZFRHnmzjgmLwrR7B/32Qxqd79A==","encrypt_chat_msg":"898WSfGMnIeytTsea7Rc0WsOocs0bIAerF6de0v2cFwqo9uOxrW9wYe5rCjCHHH5bDrNvLxBE/xOoFfcwOTYX0HQxTJaH0ES9OHDZ61p8gcbfGdJKnq2UU4tAEgGb8H+Q9n8syRXIjaI3KuVCqGIi4QGHFmxWenPFfjF/vRuPd0EpzUNwmqfUxLBWLpGhv+dLnqiEOBW41Zdc0OO0St6E+JeIeHlRZAR+E13Isv9eS09xNbF0qQXWIyNUi+ucLr5VuZnPGXBrSfvwX8f0QebTwpy1tT2zvQiMM2MBugKH6NuMzzuvEsXeD+6+3VRqL"}]} +*/ +func (s *Client) GetRawChatData(seq uint64, limit uint64, proxy string, passwd string, timeout int) (ChatDataResponse, error) { + proxyC := C.CString(proxy) + passwdC := C.CString(passwd) + chatSlice := C.NewSlice() + defer func() { + C.free(unsafe.Pointer(proxyC)) + C.free(unsafe.Pointer(passwdC)) + C.FreeSlice(chatSlice) + }() + + if s.ptr == nil { + return ChatDataResponse{}, NewSDKErr(10002) + } + + retC := C.GetChatData(s.ptr, C.ulonglong(seq), C.uint(limit), proxyC, passwdC, C.int(timeout), chatSlice) + ret := int(retC) + if ret != 0 { + return ChatDataResponse{}, NewSDKErr(ret) + } + buf := s.GetContentFromSlice(chatSlice) + + var data ChatDataResponse + err := json.Unmarshal(buf, &data) + if err != nil { + return ChatDataResponse{}, err + } + return data, nil +} + +// DecryptData 解析密文.企业微信自有解密内容 +/** +* @brief 解析密文.企业微信自有解密内容 +* @param [in] encrypt_key, getchatdata返回的encrypt_random_key,使用企业自持对应版本秘钥RSA解密后的内容 +* @param [in] encrypt_msg, getchatdata返回的encrypt_chat_msg +* @param [out] msg, 解密的消息明文 +* @return 返回是否调用成功 +* 0 - 成功 +* !=0 - 失败 + */ +func (s *Client) DecryptData(encryptRandomKey string, encryptMsg string) (msg ChatMessage, err error) { + encryptKey, err := util.RSADecryptBase64(s.privateKey, encryptRandomKey) + if err != nil { + return msg, err + } + encryptKeyC := C.CString(string(encryptKey)) + encryptMsgC := C.CString(encryptMsg) + msgSlice := C.NewSlice() + defer func() { + C.free(unsafe.Pointer(encryptKeyC)) + C.free(unsafe.Pointer(encryptMsgC)) + C.FreeSlice(msgSlice) + }() + + retC := C.DecryptData(encryptKeyC, encryptMsgC, msgSlice) + ret := int(retC) + if ret != 0 { + return msg, NewSDKErr(ret) + } + buf := s.GetContentFromSlice(msgSlice) + + // handle illegal escape character in text + for i := 0; i < len(buf); { + if buf[i] < 0x20 { + buf = append(buf[:i], buf[i+1:]...) + continue + } + i++ + } + + var baseMessage BaseMessage + err = json.Unmarshal(buf, &baseMessage) + if err != nil { + return msg, err + } + + msg.ID = baseMessage.MsgID + msg.From = baseMessage.From + msg.ToList = baseMessage.ToList + msg.Action = baseMessage.Action + msg.Type = baseMessage.MsgType + msg.originData = buf + return msg, err +} + +// GetMediaData 拉取媒体消息函数 +/** + * 拉取媒体消息函数 + * Return值=0表示该API调用成功 + * + * + * @param [in] sdk NewSdk返回的sdk指针 + * @param [in] sdkFileid 从GetChatData返回的聊天消息中,媒体消息包括的sdkfileid + * @param [in] proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081 + * @param [in] passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123 + * @param [in] indexbuf 媒体消息分片拉取,需要填入每次拉取的索引信息。首次不需要填写,默认拉取512k,后续每次调用只需要将上次调用返回的outindexbuf填入即可。 + * @param [in] timeout 超时时间,单位秒 + * @param [out] media_data 返回本次拉取的媒体数据.MediaData结构体.内容包括data(数据内容)/outindexbuf(下次索引)/is_finish(拉取完成标记) + + * + * @return 返回是否调用成功 + * 0 - 成功 + * !=0 - 失败 + */ +func (s *Client) GetMediaData(indexBuf string, sdkFileID string, proxy string, passwd string, timeout int) (*MediaData, error) { + indexBufC := C.CString(indexBuf) + sdkFileIDC := C.CString(sdkFileID) + proxyC := C.CString(proxy) + passwdC := C.CString(passwd) + mediaDataC := C.NewMediaData() + defer func() { + C.free(unsafe.Pointer(indexBufC)) + C.free(unsafe.Pointer(sdkFileIDC)) + C.free(unsafe.Pointer(proxyC)) + C.free(unsafe.Pointer(passwdC)) + C.FreeMediaData(mediaDataC) + }() + + if s.ptr == nil { + return nil, NewSDKErr(10002) + } + + retC := C.GetMediaData(s.ptr, indexBufC, sdkFileIDC, proxyC, passwdC, C.int(timeout), mediaDataC) + ret := int(retC) + if ret != 0 { + return nil, NewSDKErr(ret) + } + return &MediaData{ + OutIndexBuf: C.GoString(C.GetOutIndexBuf(mediaDataC)), + Data: C.GoBytes(unsafe.Pointer(C.GetData(mediaDataC)), C.GetDataLen(mediaDataC)), + IsFinish: int(C.IsMediaDataFinish(mediaDataC)) == 1, + }, nil +} + +// GetContentFromSlice 从切片内获取内容 +func (s *Client) GetContentFromSlice(slice *C.struct_Slice_t) []byte { + return C.GoBytes(unsafe.Pointer(C.GetContentFromSlice(slice)), C.GetSliceLen(slice)) +} diff --git a/work/msgaudit/config.go b/work/msgaudit/config.go new file mode 100644 index 0000000..6130b28 --- /dev/null +++ b/work/msgaudit/config.go @@ -0,0 +1,8 @@ +package msgaudit + +// Config 会话存档初始化参数 +type Config struct { + CorpID string // 调用企业的企业id,例如:wwd08c8exxxx5ab44d,可以在企业微信管理端--我的企业--企业信息查看 + CorpSecret string // 聊天内容存档的Secret,可以在企业微信管理端--管理工具--聊天内容存档查看 + RasPrivateKey string // 消息加密私钥,可以在企业微信管理端--管理工具--消息加密公钥查看对用公钥,私钥一般由自己保存 +} diff --git a/work/msgaudit/error.go b/work/msgaudit/error.go new file mode 100644 index 0000000..34406f2 --- /dev/null +++ b/work/msgaudit/error.go @@ -0,0 +1,76 @@ +package msgaudit + +import ( + "fmt" +) + +//返回码 错误说明 +//10000 参数错误,请求参数错误 +//10001 网络错误,网络请求错误 +//10002 数据解析失败 +//10003 系统失败 +//10004 密钥错误导致加密失败 +//10005 fileid错误 +//10006 解密失败 +//10007 找不到消息加密版本的私钥,需要重新传入私钥对 +//10008 解析encrypt_key出错 +//10009 ip非法 +//10010 数据过期 +//10011 证书错误 +const ( + SDKErrMsg = "sdk failed" + SDKParamsErrMsg = "参数错误,请求参数错误" + SDKNetworkErrMsg = "网络错误,网络请求错误" + SDKParseErrMsg = "数据解析失败" + SDKSystemErrMsg = "系统失败" + SDKSecretErrMsg = "密钥错误导致加密失败" + SDKFileIDErrMsg = "fileid错误" + SDKDecryptErrMsg = "解密失败" + SDKSecretMissErrMsg = "找不到消息加密版本的私钥,需要重新传入私钥对" + SDKEncryptKeyErrMsg = "解析encrypt_key出错" + SDKIPNotWhiteListErrMsg = "ip非法" + SDKDataExpiredErrMsg = "数据过期" + SDKTokenExpiredErrMsg = "证书过期" +) + +// Error 错误 +type Error struct { + ErrCode int `json:"errcode,omitempty"` + ErrMsg string `json:"errmsg,omitempty"` +} + +func (e Error) Error() string { + return fmt.Sprintf("%d:%s", e.ErrCode, e.ErrMsg) +} + +// NewSDKErr 初始化新的SDK错误 +func NewSDKErr(code int) Error { + msg := "" + switch code { + case 10000: + msg = SDKParamsErrMsg + case 10001: + msg = SDKNetworkErrMsg + case 10002: + msg = SDKParseErrMsg + case 10003: + msg = SDKSystemErrMsg + case 10004: + msg = SDKSecretErrMsg + case 10005: + msg = SDKFileIDErrMsg + case 10006: + msg = SDKDecryptErrMsg + case 10007: + msg = SDKSecretMissErrMsg + case 10008: + msg = SDKEncryptKeyErrMsg + case 10009: + msg = SDKIPNotWhiteListErrMsg + case 10010: + msg = SDKDataExpiredErrMsg + case 10011: + msg = SDKTokenExpiredErrMsg + } + return Error{ErrCode: code, ErrMsg: msg} +} diff --git a/work/msgaudit/lib/WeWorkFinanceSdk_C.h b/work/msgaudit/lib/WeWorkFinanceSdk_C.h new file mode 100644 index 0000000..27c4bc7 --- /dev/null +++ b/work/msgaudit/lib/WeWorkFinanceSdk_C.h @@ -0,0 +1,152 @@ +// All Rights Reserved. +// *File WeWorkFinanceSdk_C.h +// @Briefȡҵ¼ýϢsdkͷļ + +#pragma once +// ˵ +//10000 +//10001 +//10002 ݽʧ +//10003 ϵͳʧ +//10004 Կ¼ʧ +//10005 fileid +//10006 ʧ +//10007 ҲϢܰ汾˽ԿҪ´˽Կ +//10008 encrypt_key +//10009 ipǷ +//10010 ݹ +//10011 ֤ + +typedef struct WeWorkFinanceSdk_t WeWorkFinanceSdk_t; + +// +typedef struct Slice_t { + char* buf; + int len; +} Slice_t; + +typedef struct MediaData { + char* outindexbuf; + int out_len; + char* data; + int data_len; + int is_finish; +} MediaData_t; + + +#ifdef __cplusplus +extern "C" { +#endif + + WeWorkFinanceSdk_t* NewSdk(); + + + /** + * ʼ + * Returnֵ=0ʾAPIóɹ + * + * @param [in] sdk NewSdkصsdkָ + * @param [in] corpid ҵҵid磺wwd08c8exxxx5ab44dҵ΢Ź--ҵҵ--ҵϢ鿴 + * @param [in] secret ݴ浵Secretҵ΢Ź----ݴ浵鿴 + * + * + * @return Ƿʼɹ + * 0 - ɹ + * !=0 - ʧ + */ + int Init(WeWorkFinanceSdk_t* sdk, const char* corpid, const char* secret); + + /** + * ȡ¼ + * Returnֵ=0ʾAPIóɹ + * + * + * @param [in] sdk NewSdkصsdkָ + * @param [in] seq ָseqʼȡϢעǷصϢseq+1ʼأseqΪ֮ǰӿڷصseqֵ״ʹʹseq:0 + * @param [in] limit һȡϢֵ10001000᷵ش + * @param [in] proxy ʹôҪӡ磺socks5://10.0.0.1:8081 http://10.0.0.1:8081 + * @param [in] passwd ˺룬Ҫ˺롣 user_name:passwd_123 + * @param [in] timeout ʱʱ䣬λ + * @param [out] chatDatas رȡϢݣsliceṹ.ݰerrcode/errmsgԼÿϢݡʾ£ + + {"errcode":0,"errmsg":"ok","chatdata":[{"seq":196,"msgid":"CAQQ2fbb4QUY0On2rYSAgAMgip/yzgs=","publickey_ver":3,"encrypt_random_key":"ftJ+uz3n/z1DsxlkwxNgE+mL38H42/KCvN8T60gbbtPD+Rta1hKTuQPzUzO6Hzne97MgKs7FfdDxDck/v8cDT6gUVjA2tZ/M7euSD0L66opJ/IUeBtpAtvgVSD5qhlaQjvfKJc/zPMGNK2xCLFYqwmQBZXbNT7uA69Fflm512nZKW/piK2RKdYJhRyvQnA1ISxK097sp9WlEgDg250fM5tgwMjujdzr7ehK6gtVBUFldNSJS7ndtIf6aSBfaLktZgwHZ57ONewWq8GJe7WwQf1hwcDbCh7YMG8nsweEwhDfUz+u8rz9an+0lgrYMZFRHnmzjgmLwrR7B/32Qxqd79A==","encrypt_chat_msg":"898WSfGMnIeytTsea7Rc0WsOocs0bIAerF6de0v2cFwqo9uOxrW9wYe5rCjCHHH5bDrNvLxBE/xOoFfcwOTYX0HQxTJaH0ES9OHDZ61p8gcbfGdJKnq2UU4tAEgGb8H+Q9n8syRXIjaI3KuVCqGIi4QGHFmxWenPFfjF/vRuPd0EpzUNwmqfUxLBWLpGhv+dLnqiEOBW41Zdc0OO0St6E+JeIeHlRZAR+E13Isv9eS09xNbF0qQXWIyNUi+ucLr5VuZnPGXBrSfvwX8f0QebTwpy1tT2zvQiMM2MBugKH6NuMzzuvEsXeD+6+3VRqL"}]} + + * + * @return Ƿóɹ + * 0 - ɹ + * !=0 - ʧ + */ + int GetChatData(WeWorkFinanceSdk_t* sdk, unsigned long long seq, unsigned int limit, const char *proxy,const char* passwd, int timeout,Slice_t* chatDatas); + + /** + * @brief .ҵ΢н + * @param [in] encrypt_key, getchatdataصencrypt_random_key,ʹҵԳֶӦ汾ԿRSAܺ + * @param [in] encrypt_msg, getchatdataصencrypt_chat_msg + * @param [out] msg, ܵϢ + * @return Ƿóɹ + * 0 - ɹ + * !=0 - ʧ + */ + int DecryptData(const char* encrypt_key, const char* encrypt_msg, Slice_t* msg); + + /** + * ȡýϢ + * Returnֵ=0ʾAPIóɹ + * + * + * @param [in] sdk NewSdkصsdkָ + * @param [in] sdkFileid GetChatDataصϢУýϢsdkfileid + * @param [in] proxy ʹôҪӡ磺socks5://10.0.0.1:8081 http://10.0.0.1:8081 + * @param [in] passwd ˺룬Ҫ˺롣 user_name:passwd_123 + * @param [in] indexbuf ýϢƬȡҪÿȡϢ״βҪдĬȡ512kÿεֻҪϴε÷صoutindexbuf뼴ɡ + * @param [in] timeout ʱʱ䣬λ + * @param [out] media_data رȡý.MediaDataṹ.ݰdata()/outindexbuf(´)/is_finish(ȡɱ) + + * + * @return Ƿóɹ + * 0 - ɹ + * !=0 - ʧ + */ + int GetMediaData(WeWorkFinanceSdk_t* sdk, const char* indexbuf, + const char* sdkFileid,const char *proxy,const char* passwd, int timeout, MediaData_t* media_data); + + /** + * @brief ͷsdkNewSdkɶʹ + * @return + */ + void DestroySdk(WeWorkFinanceSdk_t* sdk); + + + //--------------ӿΪpythonȵcӿڣʹ-------------- + Slice_t* NewSlice(); + + /** + * @brief ͷsliceNewSliceɶʹ + * @return + */ + void FreeSlice(Slice_t* slice); + + /** + * @brief Ϊṩȡӿ + * @return bufָ + * !=NULL - ɹ + * NULL - ʧ + */ + char* GetContentFromSlice(Slice_t* slice); + int GetSliceLen(Slice_t* slice); + + // ý¼ع + + MediaData_t* NewMediaData(); + + void FreeMediaData(MediaData_t* media_data); + + char* GetOutIndexBuf(MediaData_t* media_data); + char* GetData(MediaData_t* media_data); + int GetIndexLen(MediaData_t* media_data); + int GetDataLen(MediaData_t* media_data); + int IsMediaDataFinish(MediaData_t* media_data); + +#ifdef __cplusplus +} +#endif diff --git a/work/msgaudit/lib/libWeWorkFinanceSdk_C.so b/work/msgaudit/lib/libWeWorkFinanceSdk_C.so new file mode 100644 index 0000000..6bc84a4 Binary files /dev/null and b/work/msgaudit/lib/libWeWorkFinanceSdk_C.so differ diff --git a/work/msgaudit/lib/md5.txt b/work/msgaudit/lib/md5.txt new file mode 100644 index 0000000..313cc61 --- /dev/null +++ b/work/msgaudit/lib/md5.txt @@ -0,0 +1 @@ +781ec3cbad904b1527023cc9df0f279b diff --git a/work/msgaudit/lib/tool_testSdk.cpp b/work/msgaudit/lib/tool_testSdk.cpp new file mode 100644 index 0000000..e5e6ddf --- /dev/null +++ b/work/msgaudit/lib/tool_testSdk.cpp @@ -0,0 +1,148 @@ +#include "WeWorkFinanceSdk_C.h" +#include +#include +#include +#include +#include +using std::string; + +typedef WeWorkFinanceSdk_t* newsdk_t(); +typedef int Init_t(WeWorkFinanceSdk_t*, const char*, const char*); +typedef void DestroySdk_t(WeWorkFinanceSdk_t*); + +typedef int GetChatData_t(WeWorkFinanceSdk_t*, unsigned long long, unsigned int, const char*, const char*, int, Slice_t*); +typedef Slice_t* NewSlice_t(); +typedef void FreeSlice_t(Slice_t*); + +typedef int GetMediaData_t(WeWorkFinanceSdk_t*, const char*, const char*, const char*, const char*, int, MediaData_t*); +typedef int DecryptData_t(const char*, const char*, Slice_t*); +typedef MediaData_t* NewMediaData_t(); +typedef void FreeMediaData_t(MediaData_t*); + +int main(int argc, char* argv[]) +{ + int ret = 0; + //seq 表示该企业存档消息序号,该序号单调递增,拉取序号建议设置为上次拉取返回结果中最大序号。首次拉取时seq传0,sdk会返回有效期内最早的消息。 + //limit 表示本次拉取的最大消息条数,取值范围为1~1000 + //proxy与passwd为代理参数,如果运行sdk的环境不能直接访问外网,需要配置代理参数。sdk访问的域名是"https://qyapi.weixin.qq.com"。 + //建议先通过curl访问"https://qyapi.weixin.qq.com",验证代理配置正确后,再传入sdk。 + //timeout 为拉取会话存档的超时时间,单位为秒,建议超时时间设置为5s。 + //sdkfileid 媒体文件id,从解密后的会话存档中得到 + //savefile 媒体文件保存路径 + //encrypt_key 拉取会话存档返回的encrypt_random_key,使用配置在企业微信管理台的rsa公钥对应的私钥解密后得到encrypt_key。 + //encrypt_chat_msg 拉取会话存档返回的encrypt_chat_msg + if (argc < 2) { + printf("./sdktools 1(chatmsg) 2(mediadata) 3(decryptdata)\n"); + printf("./sdktools 1 seq limit proxy passwd timeout\n"); + printf("./sdktools 2 fileid proxy passwd timeout savefile\n"); + printf("./sdktools 3 encrypt_key encrypt_chat_msg\n"); + return -1; + } + + void* so_handle = dlopen("./libWeWorkFinanceSdk_C.so", RTLD_LAZY); + if (!so_handle) { + printf("load sdk so fail:%s\n", dlerror()); + return -1; + } + newsdk_t* newsdk_fn = (newsdk_t*)dlsym(so_handle, "NewSdk"); + WeWorkFinanceSdk_t* sdk = newsdk_fn(); + + //使用sdk前需要初始化,初始化成功后的sdk可以一直使用。 + //如需并发调用sdk,建议每个线程持有一个sdk实例。 + //初始化时请填入自己企业的corpid与secrectkey。 + Init_t* init_fn = (Init_t*)dlsym(so_handle, "Init"); + DestroySdk_t* destroysdk_fn = (DestroySdk_t*)dlsym(so_handle, "DestroySdk"); + ret = init_fn(sdk, "wwd08c8e7c775ab44d", "zJ6k0naVVQ--gt9PUSSEvs03zW_nlDVmjLCTOTAfrew"); + if (ret != 0) { + //sdk需要主动释放 + destroysdk_fn(sdk); + printf("init sdk err ret:%d\n", ret); + return -1; + } + + int type = strtoul(argv[1], NULL, 10); + if (type == 1) { + //拉取会话存档 + uint64_t iSeq = strtoul(argv[2], NULL, 10); + uint64_t iLimit = strtoul(argv[3], NULL, 10); + uint64_t timeout = strtoul(argv[6], NULL, 10); + + NewSlice_t* newslice_fn = (NewSlice_t*)dlsym(so_handle, "NewSlice"); + FreeSlice_t* freeslice_fn = (FreeSlice_t*)dlsym(so_handle, "FreeSlice"); + + //每次使用GetChatData拉取存档前需要调用NewSlice获取一个chatDatas,在使用完chatDatas中数据后,还需要调用FreeSlice释放。 + Slice_t* chatDatas = newslice_fn(); + GetChatData_t* getchatdata_fn = (GetChatData_t*)dlsym(so_handle, "GetChatData"); + ret = getchatdata_fn(sdk, iSeq, iLimit, argv[4], argv[5], timeout, chatDatas); + if (ret != 0) { + freeslice_fn(chatDatas); + printf("GetChatData err ret:%d\n", ret); + return -1; + } + printf("GetChatData len:%d data:%s\n", chatDatas->len, chatDatas->buf); + freeslice_fn(chatDatas); + } + else if (type == 2) { + //拉取媒体文件 + std::string index; + uint64_t timeout = strtoul(argv[5], NULL, 10); + int isfinish = 0; + + GetMediaData_t* getmediadata_fn = (GetMediaData_t*)dlsym(so_handle, "GetMediaData"); + NewMediaData_t* newmediadata_fn = (NewMediaData_t*)dlsym(so_handle, "NewMediaData"); + FreeMediaData_t* freemediadata_fn = (FreeMediaData_t*)dlsym(so_handle, "FreeMediaData"); + + //媒体文件每次拉取的最大size为512k,因此超过512k的文件需要分片拉取。若该文件未拉取完整,mediaData中的is_finish会返回0,同时mediaData中的outindexbuf会返回下次拉取需要传入GetMediaData的indexbuf。 + //indexbuf一般格式如右侧所示,”Range:bytes=524288-1048575“,表示这次拉取的是从524288到1048575的分片。单个文件首次拉取填写的indexbuf为空字符串,拉取后续分片时直接填入上次返回的indexbuf即可。 + while (isfinish == 0) { + //每次使用GetMediaData拉取存档前需要调用NewMediaData获取一个mediaData,在使用完mediaData中数据后,还需要调用FreeMediaData释放。 + printf("index:%s\n", index.c_str()); + MediaData_t* mediaData = newmediadata_fn(); + ret = getmediadata_fn(sdk, index.c_str(), argv[2], argv[3], argv[4], timeout, mediaData); + if (ret != 0) { + //单个分片拉取失败建议重试拉取该分片,避免从头开始拉取。 + freemediadata_fn(mediaData); + printf("GetMediaData err ret:%d\n", ret); + return -1; + } + printf("content size:%d isfin:%d outindex:%s\n", mediaData->data_len, mediaData->is_finish, mediaData->outindexbuf); + + //大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。 + char file[200]; + snprintf(file, sizeof(file), "%s", argv[6]); + FILE* fp = fopen(file, "ab+"); + printf("filename:%s \n", file); + if (NULL == fp) { + freemediadata_fn(mediaData); + printf("open file err\n"); + return -1; + } + + fwrite(mediaData->data, mediaData->data_len, 1, fp); + fclose(fp); + + //获取下次拉取需要使用的indexbuf + index.assign(string(mediaData->outindexbuf)); + isfinish = mediaData->is_finish; + freemediadata_fn(mediaData); + } + } + else if (type == 3) { + //解密会话存档内容 + //sdk不会要求用户传入rsa私钥,保证用户会话存档数据只有自己能够解密。 + //此处需要用户先用rsa私钥解密encrypt_random_key后,作为encrypt_key参数传入sdk来解密encrypt_chat_msg获取会话存档明文。 + //每次使用DecryptData解密会话存档前需要调用NewSlice获取一个Msgs,在使用完Msgs中数据后,还需要调用FreeSlice释放。 + NewSlice_t* newslice_fn = (NewSlice_t*)dlsym(so_handle, "NewSlice"); + FreeSlice_t* freeslice_fn = (FreeSlice_t*)dlsym(so_handle, "FreeSlice"); + + Slice_t* Msgs = newslice_fn(); + // decryptdata api + DecryptData_t* decryptdata_fn = (DecryptData_t*)dlsym(so_handle, "DecryptData"); + ret = decryptdata_fn(argv[2], argv[3], Msgs); + printf("chatdata :%s ret :%d\n", Msgs->buf, ret); + + freeslice_fn(Msgs); + } + + return ret; +} diff --git a/work/msgaudit/lib/version.txt b/work/msgaudit/lib/version.txt new file mode 100644 index 0000000..1438950 --- /dev/null +++ b/work/msgaudit/lib/version.txt @@ -0,0 +1 @@ +200215 diff --git a/work/msgaudit/media.go b/work/msgaudit/media.go new file mode 100644 index 0000000..4375f8b --- /dev/null +++ b/work/msgaudit/media.go @@ -0,0 +1,8 @@ +package msgaudit + +// MediaData 媒体文件数据 +type MediaData struct { + OutIndexBuf string `json:"outindexbuf,omitempty"` + IsFinish bool `json:"is_finish,omitempty"` + Data []byte `json:"data,omitempty"` +} diff --git a/work/msgaudit/message.go b/work/msgaudit/message.go new file mode 100644 index 0000000..91202de --- /dev/null +++ b/work/msgaudit/message.go @@ -0,0 +1,352 @@ +package msgaudit + +// BaseMessage 基础消息 +type BaseMessage struct { + MsgID string `json:"msgid,omitempty"` // 消息id,消息的唯一标识,企业可以使用此字段进行消息去重。 + Action string `json:"action,omitempty"` // 消息动作,目前有send(发送消息)/recall(撤回消息)/switch(切换企业日志)三种类型。 + From string `json:"from,omitempty"` // 消息发送方id。同一企业内容为userid,非相同企业为external_userid。消息如果是机器人发出,也为external_userid。 + ToList []string `json:"tolist,omitempty"` // 消息接收方列表,可能是多个,同一个企业内容为userid,非相同企业为external_userid。 + RoomID string `json:"roomid,omitempty"` // 群聊消息的群id。如果是单聊则为空。 + MsgTime int64 `json:"msgtime,omitempty"` // 消息发送时间戳,utc时间,ms单位。 + MsgType string `json:"msgtype,omitempty"` // 文本消息为:text。 +} + +// TextMessage 文本消息 +type TextMessage struct { + BaseMessage + Text struct { + Content string `json:"content,omitempty"` // 消息内容。 + } `json:"text,omitempty"` +} + +// ImageMessage 图片消息 +type ImageMessage struct { + BaseMessage + Image struct { + SdkFileID string `json:"sdkfileid,omitempty"` // 媒体资源的id信息。 + Md5Sum string `json:"md5sum,omitempty"` // 图片资源的md5值,供进行校验。 + FileSize uint32 `json:"filesize,omitempty"` // 图片资源的文件大小。 + } `json:"image,omitempty"` +} + +// RevokeMessage 撤回消息 +type RevokeMessage struct { + BaseMessage + Revoke struct { + PreMsgID string `json:"pre_msgid,omitempty"` // 标识撤回的原消息的msgid + } `json:"revoke,omitempty"` +} + +// AgreeMessage 同意会话聊天内容 +type AgreeMessage struct { + BaseMessage + Agree struct { + UserID string `json:"userid,omitempty"` // 同意/不同意协议者的userid,外部企业默认为external_userid。 + AgreeTime int64 `json:"agree_time,omitempty"` // 同意/不同意协议的时间,utc时间,ms单位。 + } `json:"agree,omitempty"` +} + +// VoiceMessage 语音消息 +type VoiceMessage struct { + BaseMessage + Voice struct { + SdkFileID string `json:"sdkfileid,omitempty"` // 媒体资源的id信息。 + VoiceSize uint32 `json:"voice_size,omitempty"` // 语音消息大小。 + PlayLength uint32 `json:"play_length,omitempty"` // 播放长度。 + Md5Sum string `json:"md5sum,omitempty"` // 图片资源的md5值,供进行校验。 + } `json:"voice,omitempty"` +} + +// VideoMessage 视频消息 +type VideoMessage struct { + BaseMessage + Video struct { + SdkFileID string `json:"sdkfileid,omitempty"` // 媒体资源的id信息。 + FileSize uint32 `json:"filesize,omitempty"` // 图片资源的文件大小。 + PlayLength uint32 `json:"play_length,omitempty"` // 播放长度。 + Md5Sum string `json:"md5sum,omitempty"` // 图片资源的md5值,供进行校验。 + } `json:"video,omitempty"` +} + +// CardMessage 名片消息 +type CardMessage struct { + BaseMessage + Card struct { + CorpName string `json:"corpname,omitempty"` // 名片所有者所在的公司名称。 + UserID string `json:"userid,omitempty"` // 名片所有者的id,同一公司是userid,不同公司是external_userid + } `json:"card,omitempty"` +} + +// LocationMessage 位置消息 +type LocationMessage struct { + BaseMessage + Location struct { + Lng float64 `json:"longitude,omitempty"` // 经度,单位double + Lat float64 `json:"latitude,omitempty"` // 纬度,单位double + Address string `json:"address,omitempty"` // 地址信息 + Title string `json:"title,omitempty"` // 位置信息的title。 + Zoom uint32 `json:"zoom,omitempty"` // 缩放比例。 + } `json:"location,omitempty"` +} + +// EmotionMessage 表情消息 +type EmotionMessage struct { + BaseMessage + Emotion struct { + Type uint32 `json:"type,omitempty"` // 表情类型,png或者gif.1表示gif 2表示png。 + Width uint32 `json:"width,omitempty"` // 表情图片宽度。 + Height uint32 `json:"height,omitempty"` // 表情图片高度。 + ImageSize uint32 `json:"imagesize,omitempty"` // 资源的文件大小。 + SdkFileID string `json:"sdkfileid,omitempty"` // 媒体资源的id信息。 + Md5Sum string `json:"md5sum,omitempty"` // 图片资源的md5值,供进行校验。 + } `json:"emotion,omitempty"` +} + +// FileMessage 文件消息 +type FileMessage struct { + BaseMessage + File struct { + FileName string `json:"filename,omitempty"` // 文件名称。 + FileExt string `json:"fileext,omitempty"` // 文件类型后缀。 + SdkFileID string `json:"sdkfileid,omitempty"` // 媒体资源的id信息。 + FileSize uint32 `json:"filesize,omitempty"` // 文件大小。 + Md5Sum string `json:"md5sum,omitempty"` // 资源的md5值,供进行校验。 + } `json:"file,omitempty"` +} + +// LinkMessage 链接消息 +type LinkMessage struct { + BaseMessage + Link struct { + Title string `json:"title,omitempty"` // 消息标题。 + Desc string `json:"description,omitempty"` // 消息描述。 + LinkURL string `json:"link_url,omitempty"` // 链接url地址 + ImageURL string `json:"image_url,omitempty"` // 链接图片url。 + } `json:"link,omitempty"` +} + +// WeappMessage 小程序消息 +type WeappMessage struct { + BaseMessage + WeApp struct { + Title string `json:"title,omitempty"` // 消息标题。 + Desc string `json:"description,omitempty"` // 消息描述。 + Username string `json:"username,omitempty"` // 用户名称。 + DisplayName string `json:"displayname,omitempty"` // 小程序名称 + } `json:"weapp,omitempty"` +} + +// ChatRecordMessage 会话记录消息 +type ChatRecordMessage struct { + BaseMessage + ChatRecord struct { + Title string `json:"title,omitempty"` // 聊天记录标题 + Item []ChatRecord `json:"item,omitempty"` // 消息记录内的消息内容,批量数据 + } `json:"chatrecord,omitempty"` +} + +// TodoMessage 待办消息 +type TodoMessage struct { + BaseMessage + Todo struct { + Title string `json:"title,omitempty"` // 代办的来源文本 + Content string `json:"content,omitempty"` // 代办的具体内容 + } `json:"todo,omitempty"` +} + +// VoteMessage 投票消息 +type VoteMessage struct { + BaseMessage + VoteTitle string `json:"votetitle,omitempty"` // 投票主题。 + VoteItem []string `json:"voteitem,omitempty"` // 投票选项,可能多个内容。 + VoteType uint32 `json:"votetype,omitempty"` // 投票类型.101发起投票、102参与投票。 + VoteID string `json:"voteid,omitempty"` // 投票id,方便将参与投票消息与发起投票消息进行前后对照。 +} + +// CollectMessage 填表消息 +type CollectMessage struct { + BaseMessage + Collect struct { + RoomName string `json:"room_name,omitempty"` // 填表消息所在的群名称。 + Creator string `json:"creator,omitempty"` // 创建者在群中的名字 + CreateTime string `json:"create_time,omitempty"` // 创建的时间 + Details []CollectDetails `json:"details,omitempty"` // 表内容 + } `json:"collect,omitempty"` +} + +// RedpacketMessage 红包消息 +type RedpacketMessage struct { + BaseMessage + RedPacket struct { + Type uint32 `json:"type,omitempty"` // 红包消息类型。1 普通红包、2 拼手气群红包、3 激励群红包。 + Wish string `json:"wish,omitempty"` // 红包祝福语 + TotalCnt uint32 `json:"totalcnt,omitempty"` // 红包总个数 + TotalAmount uint32 `json:"totalamount,omitempty"` // 红包总金额。单位为分。 + } `json:"redpacket,omitempty"` +} + +// MeetingMessage 会议邀请消息 +type MeetingMessage struct { + BaseMessage + Meeting struct { + Topic string `json:"topic,omitempty"` // 会议主题 + StartTime int64 `json:"starttime,omitempty"` // 会议开始时间。Utc时间 + EndTime int64 `json:"endtime,omitempty"` // 会议结束时间。Utc时间 + Address string `json:"address,omitempty"` // 会议地址 + Remarks string `json:"remarks,omitempty"` // 会议备注 + MeetingType uint32 `json:"meetingtype,omitempty"` // 会议消息类型。101发起会议邀请消息、102处理会议邀请消息 + MeetingID uint64 `json:"meetingid,omitempty"` // 会议id。方便将发起、处理消息进行对照 + Status uint32 `json:"status,omitempty"` // 会议邀请处理状态。1 参加会议、2 拒绝会议、3 待定、4 未被邀请、5 会议已取消、6 会议已过期、7 不在房间内。 + } `json:"meeting,omitempty"` +} + +// DocMessage 在线文档消息 +type DocMessage struct { + BaseMessage + Doc struct { + Title string `json:"title,omitempty"` // 在线文档名称 + LinkURL string `json:"link_url,omitempty"` // 在线文档链接 + DocCreator string `json:"doc_creator,omitempty"` // 在线文档创建者。本企业成员创建为userid;外部企业成员创建为external_userid + } `json:"doc,omitempty"` +} + +// MarkdownMessage MarkDown消息 +type MarkdownMessage struct { + BaseMessage + Info struct { + Content string `json:"content,omitempty"` // markdown消息内容,目前为机器人发出的消息 + } `json:"info,omitempty"` +} + +// NewsMessage 图文消息 +type NewsMessage struct { + BaseMessage + Info struct { + Item []News `json:"item,omitempty"` // 图文消息数组 + } `json:"info,omitempty"` // 图文消息的内容 +} + +// CalendarMessage 日程消息 +type CalendarMessage struct { + BaseMessage + Calendar struct { + Title string `json:"title,omitempty"` // 日程主题 + CreatorName string `json:"creatorname,omitempty"` // 日程组织者 + AttendeeName []string `json:"attendeename,omitempty"` // 日程参与人。数组,内容为String类型 + StartTime int64 `json:"starttime,omitempty"` // 日程开始时间。Utc时间,单位秒 + EndTime int64 `json:"endtime,omitempty"` // 日程结束时间。Utc时间,单位秒 + Place string `json:"place,omitempty"` // 日程地点 + Remarks string `json:"remarks,omitempty"` // 日程备注 + } `json:"calendar,omitempty"` +} + +// MixedMessage 混合消息 +type MixedMessage struct { + BaseMessage + Mixed struct { + Item []MixedMsg `json:"item,omitempty"` + } `json:"mixed,omitempty"` // 消息内容。可包含图片、文字、表情等多种消息。Object类型 +} + +// MeetingVoiceCallMessage 音频存档消息 +type MeetingVoiceCallMessage struct { + BaseMessage + VoiceID string `json:"voiceid,omitempty"` // 音频id + MeetingVoiceCall *MeetingVoiceCall `json:"meeting_voice_call,omitempty"` // 音频消息内容。包括结束时间、fileid,可能包括多个demofiledata、sharescreendata消息,demofiledata表示文档共享信息,sharescreendata表示屏幕共享信息。Object类型 +} + +// VoipDocShareMessage 音频共享消息 +type VoipDocShareMessage struct { + BaseMessage + VoipID string `json:"voipid,omitempty"` // 音频id + VoipDocShare *VoipDocShare `json:"voip_doc_share,omitempty"` // 共享文档消息内容。包括filename、md5sum、filesize、sdkfileid字段。Object类型 +} + +// ExternalRedPacketMessage 互通小红包消息 +type ExternalRedPacketMessage struct { + BaseMessage + RedPacket struct { + Type int32 `json:"type,omitempty"` // 红包消息类型。1 普通红包、2 拼手气群红包。Uint32类型 + Wish int32 `json:"wish,omitempty"` // 红包祝福语。String类型 + TotalCnt int32 `json:"totalcnt,omitempty"` // 红包总个数。Uint32类型 + TotalAmount int32 `json:"totalamount,omitempty"` // 红包消息类型。1 普通红包、2 拼手气群红包。Uint32类型 + } `json:"redpacket,omitempty"` +} + +// SphFeedMessage 视频号消息 +type SphFeedMessage struct { + BaseMessage + SphFeed struct { + FeedType string `json:"feed_type,omitempty"` // 视频号消息类型 + SphName string `json:"sph_name,omitempty"` // 视频号账号名称 + FeedDesc uint64 `json:"feed_desc,omitempty"` // 视频号账号名称 + } +} + +// SwitchMessage 企业切换日志 +type SwitchMessage struct { + MsgID string `json:"msgid,omitempty"` // 消息id,消息的唯一标识,企业可以使用此字段进行消息去重 + Action string `json:"action,omitempty"` // 消息动作,切换企业为switch + Time int64 `json:"time,omitempty"` // 消息发送时间戳,utc时间,ms单位。 + User string `json:"user,omitempty"` // 具体为切换企业的成员的userid。 +} + +// ChatRecord 会话记录消息 +type ChatRecord struct { + Type string `json:"type,omitempty"` // 每条聊天记录的具体消息类型:ChatRecordText/ ChatRecordFile/ ChatRecordImage/ ChatRecordVideo/ ChatRecordLink/ ChatRecordLocation/ ChatRecordMixed …. + Content string `json:"content,omitempty"` // 消息内容。Json串,内容为对应类型的json + MsgTime int64 `json:"msgtime,omitempty"` // 消息时间,utc时间,ms单位。 + FromChatroom bool `json:"from_chatroom,omitempty"` // 是否来自群会话。 +} + +// CollectDetails 填表消息 +type CollectDetails struct { + ID uint64 `json:"id,omitempty"` // 表项id + Ques string `json:"ques,omitempty"` // 表项名称 + Type string `json:"type,omitempty"` // 表项类型,有Text(文本),Number(数字),Date(日期),Time(时间) +} + +// News 图文消息 +type News struct { + Title string `json:"title,omitempty"` // 图文消息标题 + Desc string `json:"description,omitempty"` // 图文消息描述 + URL string `json:"url,omitempty"` // 图文消息点击跳转地址 + PicURL string `json:"picurl,omitempty"` // 图文消息配图的url +} + +// MixedMsg 混合消息 +type MixedMsg struct { + Type string `json:"type,omitempty"` + Content string `json:"content,omitempty"` +} + +// MeetingVoiceCall 音频存档消息 +type MeetingVoiceCall struct { + EndTime int64 `json:"endtime,omitempty"` // 音频结束时间 + SdkFileID string `json:"sdkfileid,omitempty"` // 音频媒体下载的id + DemoFileData []DemoFileData `json:"demofiledata,omitempty"` // 文档分享对象,Object类型 + ShareScreenData []ShareScreenData `json:"sharescreendata,omitempty"` // 屏幕共享对象,Object类型 +} + +// DemoFileData 文档共享消息 +type DemoFileData struct { + FileName string `json:"filename,omitempty"` // 文档共享名称 + DemoOperator string `json:"demooperator,omitempty"` // 文档共享操作用户的id + StartTime int64 `json:"starttime,omitempty"` // 文档共享开始时间 + EndTime int64 `json:"endtime,omitempty"` // 文档共享结束时间 +} + +// ShareScreenData 屏幕共享信息 +type ShareScreenData struct { + Share string `json:"share,omitempty"` // 屏幕共享用户的id + StartTime int64 `json:"starttime,omitempty"` // 屏幕共享开始时间 + EndTime int64 `json:"endtime,omitempty"` // 屏幕共享结束时间 +} + +// VoipDocShare 音频共享文档消息 +type VoipDocShare struct { + FileName string `json:"filename,omitempty"` // 文档共享文件名称 + Md5Sum string `json:"md5sum,omitempty"` // 共享文件的md5值 + FileSize uint64 `json:"filesize,omitempty"` // 共享文件的大小 + SdkFileID string `json:"sdkfileid,omitempty"` // 共享文件的sdkfile,通过此字段进行媒体数据下载 +} diff --git a/work/work.go b/work/work.go index 58f577a..5461370 100644 --- a/work/work.go +++ b/work/work.go @@ -4,6 +4,7 @@ import ( "github.com/silenceper/wechat/v2/credential" "github.com/silenceper/wechat/v2/work/config" "github.com/silenceper/wechat/v2/work/context" + "github.com/silenceper/wechat/v2/work/msgaudit" "github.com/silenceper/wechat/v2/work/oauth" ) @@ -31,3 +32,8 @@ func (wk *Work) GetContext() *context.Context { func (wk *Work) GetOauth() *oauth.Oauth { return oauth.NewOauth(wk.ctx) } + +// GetMsgAudit get msgAudit +func (wk *Work) GetMsgAudit() (*msgaudit.Client, error) { + return msgaudit.NewClient(wk.ctx.Config) +}