Procházet zdrojové kódy

添加企业微信会话存档SDK (#419)

* 添加企业微信会话存档SDK

* 更新说明文档

* 更新包名为msgaudit并更新说明文档

* 迁移会话存档SDK到work目录下

* 移动RSA文件到util并添加动态库文件

* 整合企业微信和会话存档配置文件

* 修复golangcli-lint提示中的错误

* 对整个项目进行gofmt

* 更新会话存档说明文档

* 会话存档消息获取是抛出error

* 更新会话存档说明文档

Co-authored-by: Afeyer <afeyer@h5base.cn>
Afeyer před 4 roky
rodič
revize
1005807328

+ 43 - 0
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)
+}

+ 5 - 4
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 // 消息加密私钥,可以在企业微信管理端--管理工具--消息加密公钥查看对用公钥,私钥一般由自己保存
 }

+ 95 - 0
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()
+}
+
+
+
+```

+ 207 - 0
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
+}

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 260 - 0
work/msgaudit/client.go


+ 8 - 0
work/msgaudit/config.go

@@ -0,0 +1,8 @@
+package msgaudit
+
+// Config 会话存档初始化参数
+type Config struct {
+	CorpID        string // 调用企业的企业id,例如:wwd08c8exxxx5ab44d,可以在企业微信管理端--我的企业--企业信息查看
+	CorpSecret    string // 聊天内容存档的Secret,可以在企业微信管理端--管理工具--聊天内容存档查看
+	RasPrivateKey string // 消息加密私钥,可以在企业微信管理端--管理工具--消息加密公钥查看对用公钥,私钥一般由自己保存
+}

+ 76 - 0
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}
+}

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 152 - 0
work/msgaudit/lib/WeWorkFinanceSdk_C.h


binární
work/msgaudit/lib/libWeWorkFinanceSdk_C.so


+ 1 - 0
work/msgaudit/lib/md5.txt

@@ -0,0 +1 @@
+781ec3cbad904b1527023cc9df0f279b

+ 148 - 0
work/msgaudit/lib/tool_testSdk.cpp

@@ -0,0 +1,148 @@
+#include "WeWorkFinanceSdk_C.h"
+#include <dlfcn.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string>
+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;
+}

+ 1 - 0
work/msgaudit/lib/version.txt

@@ -0,0 +1 @@
+200215

+ 8 - 0
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"`
+}

+ 352 - 0
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,通过此字段进行媒体数据下载
+}

+ 6 - 0
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)
+}