一、项目结果图
二、通过官方提供的C++版的ddl来实现,两个关键的类
1.Finance 类,调用官方提供的C++版的ddl 类
using System;
using System.Runtime.InteropServices;
namespace WeWork.Service
{
public static class Finance
{
/// <summary>
/// 企业微信会话存档DDL
/// </summary>
private const string DllName = "Lib\\WeWorkFinanceSdk.dll";
/// <summary>
/// 获取sdk对象,首次使用初始化
/// </summary>
/// <returns></returns>
[DllImport(DllName)]
public extern static IntPtr NewSdk();
/// <summary>
/// 初始化函数
/// Return值=0表示该API调用成功
/// </summary>
/// <param name="sdk">NewSdk返回的sdk指针</param>
/// <param name="corpid">调用企业的企业id,例如:wwd08c8exxxx5ab44d,可以在企业微信管理端--我的企业--企业信息查看</param>
/// <param name="secret">聊天内容存档的Secret,可以在企业微信管理端--管理工具--聊天内容存档查看</param>
/// <returns>返回是否初始化成功 0 - 成功 !=0 - 失败</returns>
[DllImport(DllName)]
public extern static int Init(IntPtr sdk, String corpid, String secret);
/// <summary>
/// 拉取聊天记录函数
/// Return值=0表示该API调用成功
/// </summary>
/// <param name="sdk">NewSdk返回的sdk指针</param>
/// <param name="seq">从指定的seq开始拉取消息,注意的是返回的消息从seq+1开始返回,seq为之前接口返回的最大seq值。首次使用请使用seq:0</param>
/// <param name="limit">一次拉取的消息条数,最大值1000条,超过1000条会返回错误</param>
/// <param name="proxy">使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081</param>
/// <param name="passwd">代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123</param>
/// <param name="timeout">超时时间,单位秒</param>
/// <param name="chatData">返回本次拉取消息的数据,slice结构体.内容包括errcode/errmsg,以及每条消息内容。</param>
/// <returns>返回是否初始化成功 0 - 成功 !=0 - 失败</returns>
[DllImport(DllName)]
public extern static int GetChatData(IntPtr sdk, long seq, long limit, String proxy, String passwd, long timeout, IntPtr chatData);
/// <summary>
/// 拉取媒体消息函数
/// Return值=0表示该API调用成功
/// </summary>
/// <param name="sdk">NewSdk返回的sdk指针</param>
/// <param name="indexbuf">从GetChatData返回的聊天消息中,媒体消息包括的sdkfileid</param>
/// <param name="sdkField">使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081</param>
/// <param name="proxy">代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123</param>
/// <param name="passwd">媒体消息分片拉取,需要填入每次拉取的索引信息。首次不需要填写,默认拉取512k,后续每次调用只需要将上次调用返回的outindexbuf填入即可。</param>
/// <param name="timeout">超时时间,单位秒</param>
/// <param name="mediaData">返回本次拉取的媒体数据.MediaData结构体.内容包括data(数据内容)/outindexbuf(下次索引)/is_finish(拉取完成标记)</param>
/// <returns>返回是否初始化成功 0 - 成功 !=0 - 失败</returns>
[DllImport(DllName)]
public extern static int GetMediaData(IntPtr sdk, string indexbuf, string sdkField, string proxy, string passwd, long timeout, IntPtr mediaData);
/// <summary>
/// 解析密文
/// Return值=0表示该API调用成功
/// </summary>
/// <param name="encrypt_key">getchatdata返回的encrypt_random_key,使用企业自持对应版本秘钥RSA解密后的内容</param>
/// <param name="encrypt_msg">getchatdata返回的encrypt_chat_msg</param>
/// <param name="msg">解密的消息明文</param>
/// <returns>返回是否初始化成功 0 - 成功 !=0 - 失败</returns>
[DllImport(DllName)]
public extern static int DecryptData(string encrypt_key, string encrypt_msg, IntPtr msg);
[DllImport(DllName)]
public extern static void DestroySdk(IntPtr sdk);
[DllImport(DllName)]
public extern static IntPtr NewSlice();
/// <summary>
/// 释放slice,和NewSlice成对使用
/// </summary>
/// <param name="slice"></param>
[DllImport(DllName)]
public extern static void FreeSlice(IntPtr slice);
/// <summary>
/// 获取slice内容
/// </summary>
/// <param name="slice"></param>
/// <returns>内容</returns>
[DllImport(DllName)]
// IntPtr 换成 String 就需要将下面这个注释启用
//[return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(UTF8Marshaler))]
public extern static IntPtr GetContentFromSlice(IntPtr slice);
/// <summary>
/// 获取slice内容长度
/// </summary>
/// <param name="slice"></param>
/// <returns>内容长度</returns>
[DllImport(DllName)]
public extern static int GetSliceLen(IntPtr slice);
[DllImport(DllName)]
public extern static IntPtr NewMediaData();
[DllImport(DllName)]
public extern static void FreeMediaData(IntPtr mediaData);
/// <summary>
/// 获取mediadata outindex
/// </summary>
/// <param name="mediaData"></param>
/// <returns>outindex</returns>
[DllImport(DllName)]
public extern static IntPtr GetOutIndexBuf(IntPtr mediaData);
/// <summary>
/// 获取mediadata data数据
/// </summary>
/// <param name="mediaData"></param>
/// <returns>data</returns>
[DllImport(DllName)]
public extern static IntPtr GetData(IntPtr mediaData);
[DllImport(DllName)]
public extern static int GetIndexLen(IntPtr mediaData);
[DllImport(DllName)]
public extern static int GetDataLen(IntPtr mediaData);
/// <summary>
/// 判断mediadata是否结束
/// </summary>
/// <param name="mediaData"></param>
/// <returns>1完成、0未完成</returns>
[DllImport(DllName)]
public extern static int IsMediaDataFinish(IntPtr mediaData);
}
}
2.FinanceHelper 类,这个就是我们可以实例化调用方法获取会话记录内容的类
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using WeWork.Common;
using WeWork.Model;
namespace WeWork.Service
{
/// <summary>
/// 会话内容存档
/// 媒体消息的格式分别是什么?
/// 图片是jpg格式、语音是amr格式、视频/音频是mp4格式、文件格式类型包括在消息体内,表情分为动图(gif)与静态图(png),在消息体内定义。
/// </summary>
public class FinanceHelper
{
/// <summary>
/// 调用企业的企业id,例如:wwd08c8exxxx5ab44d,可以在企业微信管理端--我的企业--企业信息查看
/// </summary>
public string CorpId = "";
/// <summary>
/// 聊天内容存档的Secret,可以在企业微信管理端--管理工具--聊天内容存档查看
/// </summary>
public string Secret = "";
/// <summary>
/// 会话加密私钥
/// </summary>
public Dictionary<int, string> VerKey = null;
/// <summary>
/// 企业微信SDK
/// </summary>
public IntPtr SDK = IntPtr.Zero;
/// <summary>
/// 每次拉去会话记录数上限为1000(主要是为了防止记录太多爆内存)
/// </summary>
public int MaxLimit = 1000;
/// <summary>
/// 初始化SDK
/// </summary>
/// <param name="corpid">调用企业的企业id,例如:wwd08c8exxxx5ab44d,可以在企业微信管理端--我的企业--企业信息查看</param>
/// <param name="secret">会话内容存档的Secret,可以在企业微信管理端--管理工具--聊天内容存档查看</param>
/// <param name="verKey">会话内容存档加密版本的私钥集合, 企业微信的会话内容存档,提供的解密密钥是一段文本字符串,需要将其转换为xml格式。可将文本字符串保存为.pem文件,然后通过以下地址转换: https://the-x.cn/certificate/PemToXml.aspx </param>
public FinanceHelper(string corpid, string secret, Dictionary<int, string> verKey)
{
this.CorpId = corpid;
this.Secret = secret;
this.VerKey = verKey;
IntPtr sdk = Finance.NewSdk();
//企业微信 corpid,secret
var result = Finance.Init(sdk, CorpId, Secret);
if (result == 0)
{
this.SDK = sdk;
}
else
{
Log.Error("FinanceHelper", "企业微信-会话内容存档:初始化SDK阶段返回失败。");
}
}
#region 同步方法
/// <summary>
/// 循环获取会话记录数据 以及媒体列表
/// </summary>
/// <param name="seq">本次请求获取消息记录开始的seq值。首次访问填写0,非首次使用上次企业微信返回的最大seq。允许从任意seq重入拉取。Uint64类型,范围0-pow(2,64)-1</param>
/// <param name="limit">一次调用限制的limit值,不能超过1000.uint32类型</param>
/// <param name="proxy">使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081.如不使用代理可以设置为空. 支持sock5跟http代理</param>
/// <param name="passwd">代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123</param>
/// <param name="timeout">超时时长,单位 秒</param>
/// <returns></returns>
public Tuple<List<IMsgBase>, List<FileData>> GetChatList(long seq = 0, long limit = 1000, string proxy = "", string passwd = "", long timeout = 60)
{
List<IMsgBase> msgList = new List<IMsgBase>();
List<FileData> fileList = new List<FileData>();
DateTime startTime = DateTime.Now;
int pullNumber = 0; //拉取的次数
//获取聊天信息
bool isGet = true;
while (isGet)
{
var result = GetChatData(seq, limit, proxy, passwd, timeout);
List<IMsgBase> list1 = result.Item1; //企业微信 会话内容 列表
List<FileData> list2 = result.Item2; //媒体内容 列表
var count = list1.Count;
if (count > 0)
{
foreach (var item in list1)
{
msgList.Add(item);
}
foreach (var item in list2)
{
fileList.Add(item);
}
if (count < MaxLimit)
{
//总数没有超过 限制拉取的数量时 说明已经拉取完成 不在循环拉取了
if (count >= limit)
{
seq = list1.OrderByDescending(t => t.seq).Select(q => q.seq).FirstOrDefault();
}
else
{
isGet = false;
}
}
else
{
isGet = false;
}
}
else
{
isGet = false;
}
//看看 调用频率不可超过600次 / 分钟。
pullNumber++;
DateTime endTime = DateTime.Now;
TimeSpan timeDifference = endTime - startTime;
double secondsDifference = timeDifference.TotalSeconds;
if (secondsDifference < 60)
{
if (pullNumber >= 600)
{
isGet = false;
}
}
}
return Tuple.Create(msgList, fileList);
}
/// <summary>
/// 获取会话记录数据 以及媒体列表
/// 注:获取会话记录内容不能超过5天,如果企业需要全量数据,则企业需要定期拉取聊天消息。返回的ChatDatas内容为json格式。
/// </summary>
/// <param name="seq">本次请求获取消息记录开始的seq值。首次访问填写0,非首次使用上次企业微信返回的最大seq。允许从任意seq重入拉取。Uint64类型,范围0-pow(2,64)-1</param>
/// <param name="limit">一次调用限制的limit值,不能超过1000.uint32类型</param>
/// <param name="proxy">使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081.如不使用代理可以设置为空. 支持sock5跟http代理</param>
/// <param name="passwd">代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123</param>
/// <param name="timeout">超时时长,单位 秒</param>
/// <returns></returns>
public Tuple<List<IMsgBase>, List<FileData>> GetChatData(long seq = 0, long limit = 1000, string proxy = "", string passwd = "", long timeout = 300)
{
List<IMsgBase> msgList = new List<IMsgBase>();
List<FileData> fileList = new List<FileData>();
var slice = Finance.NewSlice();
try
{
var result = Finance.GetChatData(this.SDK, seq, limit, proxy, passwd, timeout, slice);
if (result == 0)
{
var content = GetContentFromSlice(slice);
var encryptChatData = JsonHelper.JsonToT<GetChatData>(content);
if (encryptChatData.errcode == 0)
{
foreach (var encryptChat in encryptChatData.chatdata)
{
var msgseq = encryptChat.seq;
string fileName = "";
string md5Sum = "";
string sdkFileId = "";
IMsgBase msgModel = null;
FileData fileData = null;
//根据加密此条消息使用的公钥版本号匹配私钥
var flag = VerKey.TryGetValue(encryptChat.publickey_ver, out var privateKey);
if (flag)
{
if (!string.IsNullOrWhiteSpace(privateKey))
{
var key = Decrypt(privateKey, encryptChat.encrypt_random_key);
if (!string.IsNullOrWhiteSpace(key))
{
var msgSlice = Finance.NewSlice();
var deRes = Finance.DecryptData(key, encryptChat.encrypt_chat_msg, msgSlice);
var chatJsonStr = GetContentFromSlice(msgSlice);
Finance.FreeSlice(msgSlice);
if (!string.IsNullOrWhiteSpace(chatJsonStr))
{
msgModel = JsonHelper.JsonToT<MsgBase>(chatJsonStr);
#region
var action = msgModel.action;//消息动作,目前有send(发送消息)/recall(撤回消息)/switch(切换企业日志)三种类型。
switch (action)
{
case "switch"://切换企业日志
msgModel = JsonHelper.JsonToT<ChatSwitch>(chatJsonStr);
break;
case "recall"://撤回消息
msgModel = JsonHelper.JsonToT<ChatRevoke>(chatJsonStr);
break;
case "send"://发送消息
var sendChat = JsonHelper.JsonToT<MsgChatBase>(chatJsonStr);
var msgtype = sendChat.msgtype;
switch (msgtype)
{
case MsgType.Text://文本
msgModel = JsonHelper.JsonToT<ChatText>(chatJsonStr);
break;
case MsgType.Image://图片
{
var chatImage = JsonHelper.JsonToT<ChatImage>(chatJsonStr);
msgModel = chatImage;
fileName = $"{Guid.NewGuid()}.jpg";
md5Sum = chatImage.image.md5sum;
sdkFileId = chatImage.image.sdkfileid;
if (!fileList.Any(t => t.SdkFileId == sdkFileId))
{
fileData = new FileData()
{
MsgType = "image",
MsgDate = TimeHelper.GetUnixTimeToDateTime(chatImage.msgtime.ToString()),
FileName = fileName,
Md5Sum = md5Sum,
SdkFileId = sdkFileId
};
fileList.Add(fileData);
}
}
break;
case MsgType.Agree://同意会话聊天内容
msgModel = JsonHelper.JsonToT<ChatAgree>(chatJsonStr);
break;
case MsgType.Disagree://不同意会话聊天内容
msgModel = JsonHelper.JsonToT<ChatDisAgree>(chatJsonStr);
break;
case MsgType.Voice://语音
{
var chatVoice = JsonHelper.JsonToT<ChatVoice>(chatJsonStr);
msgModel = chatVoice;
fileName = $"{Guid.NewGuid()}.amr";
md5Sum = chatVoice.voice.md5sum;
sdkFileId = chatVoice.voice.sdkfileid;
if (!fileList.Any(t => t.SdkFileId == sdkFileId))
{
fileData = new FileData()
{
MsgType = "voice",
MsgDate = TimeHelper.GetUnixTimeToDateTime(chatVoice.msgtime.ToString()),
FileName = fileName,
Md5Sum = md5Sum,
SdkFileId = sdkFileId
};
fileList.Add(fileData);
}
}
break;
case MsgType.Video://视频
{
var chatVideo = JsonHelper.JsonToT<ChatVideo>(chatJsonStr);
msgModel = chatVideo;
fileName = $"{Guid.NewGuid()}.mp4";
md5Sum = chatVideo.video.md5sum;
sdkFileId = chatVideo.video.sdkfileid;
if (!fileList.Any(t => t.SdkFileId == sdkFileId))
{
fileData = new FileData()
{
MsgType = "video",
MsgDate = TimeHelper.GetUnixTimeToDateTime(chatVideo.msgtime.ToString()),
FileName = fileName,
Md5Sum = md5Sum,
SdkFileId = sdkFileId
};
fileList.Add(fileData);
}
}
break;
case MsgType.Card://名片
msgModel = JsonHelper.JsonToT<ChatCard>(chatJsonStr);
break;
case MsgType.Location://位置
msgModel = JsonHelper.JsonToT<ChatLocation>(chatJsonStr);
break;
case MsgType.Emotion://表情
{
var chatEmotion = JsonHelper.JsonToT<ChatEmotion>(chatJsonStr);
msgModel = chatEmotion;
switch (chatEmotion.emotion.type)
{
case 1:
fileName = $"{Guid.NewGuid()}.gif";
break;
case 2:
fileName = $"{Guid.NewGuid()}.png";
break;
}
md5Sum = chatEmotion.emotion.md5sum;
sdkFileId = chatEmotion.emotion.sdkfileid;
if (!fileList.Any(t => t.SdkFileId == sdkFileId))
{
fileData = new FileData()
{
MsgType = "image",
MsgDate = TimeHelper.GetUnixTimeToDateTime(chatEmotion.msgtime.ToString()),
FileName = fileName,
Md5Sum = md5Sum,
SdkFileId = sdkFileId
};
fileList.Add(fileData);
}
}
break;
case MsgType.File://文件
{
var chatFile = JsonHelper.JsonToT<ChatFile>(chatJsonStr);
msgModel = chatFile;
fileName = $"{Guid.NewGuid()}.{chatFile.file.fileext}";
md5Sum = chatFile.file.md5sum;
sdkFileId = chatFile.file.sdkfileid;
if (!fileList.Any(t => t.SdkFileId == sdkFileId))
{
fileData = new FileData()
{
MsgType = "file",
MsgDate = TimeHelper.GetUnixTimeToDateTime(chatFile.msgtime.ToString()),
FileName = fileName,
Md5Sum = md5Sum,
SdkFileId = sdkFileId
};
fileList.Add(fileData);
}
}
break;
case MsgType.Link://链接
msgModel = JsonHelper.JsonToT<ChatLink>(chatJsonStr);
break;
case MsgType.WeApp://小程序消息
msgModel = JsonHelper.JsonToT<ChatWeapp>(chatJsonStr);
break;
case MsgType.ChatRecord://会话记录消息
msgModel = JsonHelper.JsonToT<ChatRecord>(chatJsonStr);
break;
case MsgType.Todo://待办消息
msgModel = JsonHelper.JsonToT<ChatTodo>(chatJsonStr);
break;
case MsgType.Vote://投票消息
msgModel = JsonHelper.JsonToT<ChatVote>(chatJsonStr);
break;
case MsgType.Collect://填表消息
msgModel = JsonHelper.JsonToT<ChatCollect>(chatJsonStr);
break;
case MsgType.RedPacket://红包消息
msgModel = JsonHelper.JsonToT<ChatRedPacket>(chatJsonStr);
break;
case MsgType.Meeting://会议邀请消息
msgModel = JsonHelper.JsonToT<ChatMeeting>(chatJsonStr);
break;
case MsgType.DocMsg://在线文档消息
msgModel = JsonHelper.JsonToT<ChatDocMsg>(chatJsonStr);
break;
case MsgType.Markdown://MarkDown格式消息
msgModel = JsonHelper.JsonToT<ChatMarkDown>(chatJsonStr);
break;
case MsgType.News://图文消息
msgModel = JsonHelper.JsonToT<ChatNews>(chatJsonStr);
break;
case MsgType.Calendar://日程消息
msgModel = JsonHelper.JsonToT<ChatCalendar>(chatJsonStr);
break;
case MsgType.Mixed://混合消息
msgModel = JsonHelper.JsonToT<ChatMixed>(chatJsonStr);
break;
case MsgType.MeetingVoiceCall://音频存档消息
{
var chatMeetingVoiceCall = JsonHelper.JsonToT<ChatMeetingVoiceCall>(chatJsonStr);
msgModel = chatMeetingVoiceCall;
fileName = $"{Guid.NewGuid()}.mp4";
md5Sum = "";
sdkFileId = chatMeetingVoiceCall.meeting_voice_call.sdkfileid;
if (!fileList.Any(t => t.SdkFileId == sdkFileId))
{
fileData = new FileData()
{
MsgType = "video",
MsgDate = TimeHelper.GetUnixTimeToDateTime(chatMeetingVoiceCall.msgtime.ToString()),
FileName = fileName,
Md5Sum = md5Sum,
SdkFileId = sdkFileId
};
fileList.Add(fileData);
}
}
break;
case MsgType.VoipDocShare://音频共享文档消息
{
var chatVoipDocShare = JsonHelper.JsonToT<ChatVoipDocShare>(chatJsonStr);
msgModel = chatVoipDocShare;
fileName = $"{chatVoipDocShare.voip_doc_share.filename}";
md5Sum = chatVoipDocShare.voip_doc_share.md5sum; ;
sdkFileId = chatVoipDocShare.voip_doc_share.sdkfileid;
if (!fileList.Any(t => t.SdkFileId == chatVoipDocShare.voip_doc_share.sdkfileid))
{
fileData = new FileData()
{
MsgType = "file",
MsgDate = TimeHelper.GetUnixTimeToDateTime(chatVoipDocShare.msgtime.ToString()),
FileName = fileName,
Md5Sum = md5Sum,
SdkFileId = sdkFileId
};
fileList.Add(fileData);
}
}
break;
case MsgType.ExternalRedPacket://互通红包消息
msgModel = JsonHelper.JsonToT<ChatExternalRedPacket>(chatJsonStr);
break;
case MsgType.SphFeed://视频号消息
msgModel = JsonHelper.JsonToT<ChatSphFeed>(chatJsonStr);
break;
case MsgType.VoipText://音视频通话
msgModel = JsonHelper.JsonToT<ChatVoipText>(chatJsonStr);
break;
case MsgType.QyDiskFile://微盘文件
msgModel = JsonHelper.JsonToT<ChatQyDiskFile>(chatJsonStr);
break;
}
break;
default:
break;
}
/*
图片
{
"msgid":"CAQQvPnc4QUY0On2rYSAgAMgooLa0Q8=","action":"send","from":"XuJinSheng","tolist":["icefog"],"roomid":"",
"msgtime":0,"msgtype":"image",
"image":{"md5sum":"50de8e5ae8ffe4f1df7a93841f71993a","filesize":70961,"sdkfileid":"CtYBMzA2OTAyMDEwMjA0NjIzMDYwMDIwMTAwMDIwNGI3ZmU0MDZlMDIwMzBmNTliMTAyMDQ1YzliNTQ3NzAyMDQ1YzM3M2NiYzA0MjQ2NjM0MzgzNTM0NjEzNTY1MmQzNDYxMzQzODJkMzQzMTYxNjEyZDM5NjEzOTM2MmQ2MTM2NjQ2NDY0NjUzMDY2NjE2NjM1MzcwMjAxMDAwMjAzMDExNTQwMDQxMDUwZGU4ZTVhZThmZmU0ZjFkZjdhOTM4NDFmNzE5OTNhMDIwMTAyMDIwMTAwMDQwMBI4TkRkZk1UWTRPRGcxTVRBek1ETXlORFF6TWw4eE9UUTVOamN6TkRZMlh6RTFORGN4TWpNNU1ERT0aIGEwNGQwYWUyM2JlYzQ3NzQ5MjZhNWZjMjk0ZTEyNTkz"}
}
语音
{
"msgid":"10958372969718811103_1603875609","action":"send","from":"wmGAgeDQAAdBjb8CK4ieMPRm7Cqm-9VA","tolist":["kenshin"],"roomid":"",
"msgtime":1603875609704,"msgtype":"voice",
"voice":{"md5sum":"9db09c7fa627c9e53f17736c786a74d5","voice_size":6810,"play_length":10,"sdkfileid":"kcyZjZqOXhETGYxajB2Zkp5Rk8zYzh4RVF3ZzZGdXlXNWRjMUoxVGZxbzFTTDJnQ2YxL0NraVcxUUJNK3VUamhEVGxtNklCbjZmMEEwSGRwN0h2cU1GQTU1MDRSMWdTSmN3b25ZMkFOeG5hMS90Y3hTQ0VXRlVxYkR0Ymt5c3JmV2VVcGt6UlNXR1ZuTFRWVGtudXVldDRjQ3hscDBrMmNhMFFXVnAwT3Y5NGVqVGpOcWNQV2wrbUJwV01TRm9xWmNDRVVrcFY5Nk9OUS9GbXIvSmZvOVVZZjYxUXBkWnMvUENkVFQxTHc2N0drb2pJT0FLZnhVekRKZ1FSNDU3ZnZtdmYvTzZDOG9DRXl2SUNIOHc9PRI0TkRkZk56ZzRNVE13TVRjMk5qQTRNak0yTmw4ek5qRTVOalExTjE4eE5qQXpPRGMxTmpBNRogNzM3MDY2NmM2YTc5Njg3NDdhNzU3NDY0NzY3NTY4NjY="}
}
视频
{
"msgid":"17955920891003447432_1603875627","action":"send","from":"kenshin","tolist":["wmGAgeDQAAHuRJbt4ZQI_1cqoQcf41WQ"],"roomid":"",
"msgtime":1603875626823,"msgtype":"video",
"video":{"md5sum":"d06fc80c01d6fbffcca3b229ba41eac6","filesize":15169724,"play_length":108,"sdkfileid":"MzAzMjYxMzAzNTYzMzgzMjMyMzQwMjAxMDAwMjA0MDBlNzc4YzAwNDEwZDA2ZmM4MGMwMWQ2ZmJmZmNjYTNiMjI5YmE0MWVhYzYwMjAxMDQwMjAxMDAwNDAwEjhORGRmTVRZNE9EZzFNREEyTlRjM056QXpORjgxTWpZeE9USTBOek5mTVRZd016ZzNOVFl5Tnc9PRogNTIzNGQ1NTQ5N2RhNDM1ZDhlZTU5ODk4NDQ4NzRhNDk="}
}
表情
{
"msgid":"6623217619416669654_1603875612","action":"send","from":"icef","tolist":["wmErxtDgAAhteCglUZH2kUt3rq431qmg"],"roomid":"",
"msgtime":1603875611148,"msgtype":"emotion",
"emotion":{"type":1,"width":290,"height":290,"imagesize":962604,"md5sum":"94c2b0bba52cc456cb8221b248096612","sdkfileid":"4eE1ESTVNalE1TnprMFh6RTJNRE00TnpVMk1UST0aIDc0NzI2NjY1NzE3NTc0Nzg2ZDZlNzg2YTY5NjY2MTYx"}
}
文件
{
"msgid":"18039699423706571225_1603875608","action":"send","from":"kens","tolist":["wmErxtDgAArDlFIhf76O6w4GxU81al8w"],"roomid":"",
"msgtime":1603875608214,"msgtype":"file",
"file":{"md5sum":"18e93fc2ea884df23b3d2d3b8667b9f0","filename":"资料.docx","fileext":"docx","filesize":18181,"sdkfileid":"E4ODRkZjIzYjNkMmQzYjg2NjdiOWYwMDIwMTA1MDIwMTAwMDQwMBI4TkRkZk1UWTRPRGcxTURrek9UZzBPVEF6TTE4eE1EUXpOVGcxTlRVNVh6RTJNRE00TnpVMk1EZz0aIDMwMzkzMzY0NjEzNjM3NjY2NDY1NjMzNjYxMzIzNzYx"}
}
会话记录消息
ype 每条聊天记录的具体消息类型:ChatRecordText/ ChatRecordFile/ ChatRecordImage/ ChatRecordVideo/ ChatRecordLink/ ChatRecordLocation/ ChatRecordMixed ….
{
"msgid":"11354299838102555191_1603875658","action":"send","from":"ken","tolist":["icef"],"roomid":"",
"msgtime":1603875657905,"msgtype":"chatrecord",
"chatrecord":{
"title":"群聊",
"item":[
{"type":"ChatRecordText","msgtime":1603875610,"content":"{\"content\":\"test\"}","from_chatroom":false},
{"type":"ChatRecordText","msgtime":1603875620,"content":"{\"content\":\"test2\"}","from_chatroom":false}
]
}
}
混合消息
mixed 消息内容。可包含图片、文字、表情等多种消息。Object类型
{
"msgid":"DAQQluDa4QUY0On4kYSABAMgzPrShAE=","action":"send","from":"HeMiao","tolist":["HeChangTian","LiuZeYu"],"roomid":"wr_tZ2BwAAUwHpYMwy9cIWqnlU3Hzqfg",
"msgtime":1577414359072,"msgtype":"mixed",
"mixed":{
"item":[
{"type":"text","content":"{\"content\":\"你好[微笑]\\n\"}"},
{"type":"image","content":"{\"md5sum\":\"368b6c18c82e6441bfd89b343e9d2429\",\"filesize\":13177,\"sdkfileid\":\"CtYBMzA2OTAyMDEwMjA0NjIzMDYwMDIwMTAwMDWwNDVmYWY4Y2Q3MDIwMzBmNTliMTAyMDQwYzljNTQ3NzAyMDQ1ZTA1NmFlMjA0MjQ2NjM0NjIzNjY2MzYzNTMyMmQzNzYxMzQ2NDJkMzQ2MjYxNjQyZDM4MzMzMzM4MmQ3MTYyMzczMTM4NjM2NDYxMzczMjY2MzkwMjAxMDAwMjAzMDIwMDEwMDQxMDM2OGI2YzE4YzgyZTY0NDFiZmQ4OWIyNDNlOWQyNDI4MDIwMTAyMDIwMTAwMDQwMBI4TkRkZk2UWTRPRGcxTVRneE5URTFNRGc1TVY4eE1UTTFOak0yTURVeFh6RTFOemMwTVRNek5EYz0aIDQzMTY5NDFlM2MxZDRmZjhhMjEwY2M0NDQzZGUXOTEy\"}"}
]
}
}
音频存档消息
{
"msgid":"17952229780246929345_1594197637","action":"send","from":"wo137MCgAAYW6pIiKKrDe5SlzEhSgwbA","tolist":["wo137MCgAAYW6pIiKKrDe5SlzEhSgwbA"],
"msgtime":1594197581203,"msgtype":"meeting_voice_call","voiceid":"grb8a4c48a3c094a70982c518d55e40557",
"meeting_voice_call":{
"endtime":1594197635,"sdkfileid":"CpsBKjAqd0xhb2JWRUJldGtwcE5DVTB6UjRUalN6c09vTjVyRnF4YVJ5M24rZC9YcHF3cHRPVzRwUUlaMy9iTytFcnc0SlBkZDU1YjRNb0MzbTZtRnViOXV5WjUwZUIwKzhjbU9uRUlxZ3pyK2VXSVhUWVN2ejAyWFJaTldGSkRJVFl0aUhkcVdjbDJ1L2RPbjJsRlBOamJaVDNnPT0SOE5EZGZNVFk0T0RnMU16YzVNVGt5T1RJMk9GOHhNalk0TXpBeE9EZzJYekUxT1RReE9UYzJNemM9GiA3YTYyNzA3NTY4Nzc2MTY3NzQ2MTY0NzA2ZTc4NjQ2OQ==",
"demofiledata":[{"filename":"65eb1cdd3e7a3c1740ecd74220b6c627.docx","demooperator":"wo137MCgAAYW6pIiKKrDe5SlzEhSgwbA","starttime":1594197599,"endtime":1594197609}],
"sharescreendata":[{"share":"wo137MCgAAYW6pIiKKrDe5SlzEhSgwbA","starttime":1594197624,"endtime":1594197624}]
}
}
音频共享文档消息
{
"msgid":"16527954622422422847_1594199256","action":"send","from":"18002520162","tolist":["wo137MCgAAYW6pIiKKrDe5SlzEhSgwbA"],
"msgtime":1594199235014,"msgtype":"voip_doc_share","voipid":"gr2751c98b19300571f8afb3b74514bd32",
"voip_doc_share":{
"filename":"欢迎使用微盘.pdf.pdf","md5sum":"ff893900f24e55e216e617a40e5c4648","filesize":4400654,"sdkfileid":"CpsBKjAqZUlLdWJMd2gvQ1JxMzd0ZjlpdW5mZzJOOE9JZm5kbndvRmRqdnBETjY0QlcvdGtHSFFTYm95dHM2VlllQXhkUUN5KzRmSy9KT3pudnA2aHhYZFlPemc2aVZ6YktzaVh3YkFPZHlqNnl2L2MvcGlqcVRjRTlhZEZsOGlGdHJpQ2RWSVNVUngrVFpuUmo3TGlPQ1BJemlRPT0SOE5EZGZNVFk0T0RnMU16YzVNVGt5T1RJMk9GODFNelUyTlRBd01qQmZNVFU1TkRFNU9USTFOZz09GiA3YTcwNmQ2Zjc5NjY3MDZjNjY2Zjc4NzI3NTZmN2E2YQ=="
}
}
*/
#endregion
if (msgModel != null)
{
msgModel.seq = msgseq;
msgList.Add(msgModel);
}
}
}
else
{
Log.Error("FinanceHelper", "企业微信-会话内容存档:会话数据解密失败。");
}
}
else
{
Log.Error("FinanceHelper", "企业微信-会话内容存档:会话数据解密私钥为空。");
}
}
}
}
else
{
Log.Error("FinanceHelper", $"企业微信-会话内容存档:转换会话数据阶段返回值错误\n[{encryptChatData.errcode}]{encryptChatData.errmsg}");
}
}
else
{
Log.Error("FinanceHelper", "企业微信-会话内容存档:获取会话数据阶段返回失败。");
}
}
catch (Exception ex)
{
Log.Error("FinanceHelper", "企业微信-会话内容存档:获取会话数据数据异常。" + ex.ToString());
}
finally
{
Finance.FreeSlice(slice);
}
return Tuple.Create(msgList, fileList);
}
/// <summary>
/// 读取媒体数据
/// 也只能拉取5天内的媒体消息内容
/// </summary>
/// <param name="sdkfileid">消息体内容中的sdkfileid信息。</param>
/// <param name="proxy">使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081.如不使用代理可以设置为空. 支持sock5跟http代理</param>
/// <param name="passwd">代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123</param>
/// <param name="timeout">超时时间,单位秒</param>
/// <returns>byte[]</returns>
public byte[] GetMediaData(string sdkfileid, string proxy = "", string passwd = "", long timeout = 300)
{
/*
错误码:
返回值 说明 建议
10000 请求参数错误 检查Init接口corpid、secret参数;检查GetChatData接口limit参数是否未填或大于1000;检查GetMediaData接口sdkfileid是否为空,indexbuf是否正常
10001 网络请求错误 检查是否网络有异常、波动;检查使用代理的情况下代理参数是否设置正确的用户名与密码
10002 数据解析失败 建议重试请求。若仍失败,可以反馈给企业微信进行查询,请提供sdk接口参数与调用时间点等信息
10003 系统调用失败 GetMediaData调用失败,建议重试请求。若仍失败,可以反馈给企业微信进行查询,请提供sdk接口参数与调用时间点等信息
10004 已废弃 目前不会返回此错误码
10005 fileid错误 检查在GetMediaData接口传入的sdkfileid是否正确
10006 解密失败 请检查是否先进行base64decode再进行rsa私钥解密,再进行DecryptMsg调用
10007 已废弃 目前不会返回此错误码
10008 DecryptMsg错误 建议重试请求。若仍失败,可以反馈给企业微信进行查询,请提供sdk接口参数与调用时间点等信息
10009 ip非法 请检查sdk访问外网的ip是否与管理端设置的可信ip匹配,若不匹配会返回此错误码
10010 请求的数据过期 用户欲拉取的数据已过期,仅支持近5天内的数据拉取
10011 ssl证书错误 使用openssl版本sdk,校验ssl证书失败
*/
var byteList = new List<byte>();
try
{
var outIndexBuf = "";//媒体消息分片拉取,需要填入每次拉取的索引信息。首次不需要填写,后续每次调用只需要将上次调用返回的outindexbuf填入即可。
while (true)
{
// int retryCount = 0;
//RetryGetMedia:
var mediaData = Finance.NewMediaData();
var res = Finance.GetMediaData(this.SDK, outIndexBuf, sdkfileid, proxy, passwd, timeout, mediaData);
if (res == 0)
{
var dataIntPtr = Finance.GetData(mediaData);
var dataLen = Finance.GetDataLen(mediaData);
var bytes = new byte[dataLen];
Marshal.Copy(dataIntPtr, bytes, 0, bytes.Length);
byteList.AddRange(bytes);
// 校验文件是否已经读取完毕
if (Finance.IsMediaDataFinish(mediaData) == 1)
{
Finance.FreeMediaData(mediaData);
break;
}
else
{
var oibPtr = Finance.GetOutIndexBuf(mediaData);
outIndexBuf = Marshal.PtrToStringAnsi(oibPtr);
Finance.FreeMediaData(mediaData);
}
}
else
{
byteList = null;
//10001 网络请求错误
//10002 解析数据失败
//10003 系统调用失败
//if (res == 10002 && retryCount < 3)
//{
// retryCount++;
// System.Threading.Thread.Sleep(500);
// goto RetryGetMedia;
//}
//else
//{
Finance.FreeMediaData(mediaData);
//throw new Exception($"企微会话存档:获取会话媒体数据失败,res:{res}");
Log.Error("FinanceHelper", $"企业微信-会话内容存档:获取会话媒体数据失败,sdkfileid:{sdkfileid};res:{res}");
break;
//}
}
}
}
catch (Exception ex)
{
Log.Error("FinanceHelper", "企业微信-会话内容存档:获取会话媒体数据失败!" + ex.ToString());
}
if (byteList != null && byteList.Count > 0)
{
return byteList.ToArray();
}
else
{
return null;
}
}
/// <summary>
/// 获取媒体数据列表(含文件byte[])
/// </summary>
/// <param name="list">媒体数据列表</param>
/// <param name="proxy">使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081.如不使用代理可以设置为空. 支持sock5跟http代理</param>
/// <param name="passwd">代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123</param>
/// <param name="timeout">超时时间,单位秒</param>
/// <returns></returns>
public List<FileData> GetFileList(List<FileData> list, string proxy = "", string passwd = "", long timeout = 300)
{
List<FileData> listNew = null;
if (list.Count > 0)
{
listNew = new List<FileData>();
foreach (var file in list)
{
byte[] buffer = null;
//也只能拉取5天内的媒体消息内容
if (file.MsgDate > DateTime.Now.AddDays(-5))
{
buffer = GetMediaData(file.SdkFileId, proxy, passwd, timeout);
}
file.FileByte = buffer;
listNew.Add(file);
}
}
return listNew;
}
#endregion 同步方法
#region 异步方法
/// <summary>
/// 获取会话记录数据(异步方法)
/// 注:获取会话记录内容不能超过5天,如果企业需要全量数据,则企业需要定期拉取聊天消息。返回的ChatDatas内容为json格式。
/// </summary>
/// <param name="seq">本次请求获取消息记录开始的seq值。首次访问填写0,非首次使用上次企业微信返回的最大seq。允许从任意seq重入拉取。Uint64类型,范围0-pow(2,64)-1</param>
/// <param name="limit">一次调用限制的limit值,不能超过1000.uint32类型</param>
/// <param name="proxy">使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081.如不使用代理可以设置为空. 支持sock5跟http代理</param>
/// <param name="passwd">代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123</param>
/// <param name="timeout">超时时长,单位 秒</param>
/// <returns>byte[]</returns>
public async Task<Tuple<List<IMsgBase>, List<FileData>>> GetChatDataAsync(long seq = 0, long limit = 1000, string proxy = "", string passwd = "", long timeout = 300)
{
// 调用同步方法并转换为异步
var list = GetChatData(seq, limit, proxy, passwd, timeout);
return await Task.FromResult(list);
}
/// <summary>
/// 循环获取会话记录数据
/// </summary>
/// <param name="seq">本次请求获取消息记录开始的seq值。首次访问填写0,非首次使用上次企业微信返回的最大seq。允许从任意seq重入拉取。Uint64类型,范围0-pow(2,64)-1</param>
/// <param name="limit">一次调用限制的limit值,不能超过1000.uint32类型</param>
/// <param name="proxy">使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081.如不使用代理可以设置为空. 支持sock5跟http代理</param>
/// <param name="passwd">代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123</param>
/// <param name="timeout">超时时长,单位 秒</param>
/// <returns></returns>
public async Task<Tuple<List<IMsgBase>, List<FileData>>> GetChatListAsync(long seq = 0, long limit = 1000, string proxy = "", string passwd = "", long timeout = 60)
{
List<IMsgBase> msgList = new List<IMsgBase>();
List<FileData> fileList = new List<FileData>();
DateTime startTime = DateTime.Now;
int pullNumber = 0; //拉取的次数
//获取聊天信息
bool isGet = true;
while (isGet)
{
var result = await GetChatDataAsync(seq, limit, proxy, passwd, timeout);
List<IMsgBase> list1 = result.Item1; //企业微信 会话内容 列表
List<FileData> list2 = result.Item2; //媒体内容 列表
var count = list1.Count;
if (count > 0)
{
foreach (var item in list1)
{
msgList.Add(item);
}
foreach (var item in list2)
{
fileList.Add(item);
}
if (count < MaxLimit)
{
//总数没有超过 限制拉取的数量时 说明已经拉取完成 不在循环拉取了
if (count >= limit)
{
seq = list1.OrderByDescending(t => t.seq).Select(q => q.seq).FirstOrDefault();
}
else
{
isGet = false;
}
}
else
{
isGet = false;
}
}
else
{
isGet = false;
}
//看看 调用频率不可超过600次 / 分钟。
pullNumber++;
DateTime endTime = DateTime.Now;
TimeSpan timeDifference = endTime - startTime;
double secondsDifference = timeDifference.TotalSeconds;
if (secondsDifference < 60)
{
if (pullNumber >= 600)
{
isGet = false;
}
}
}
return Tuple.Create(msgList, fileList);
}
/// <summary>
/// 读取媒体数据(异步方法)
/// 也只能拉取5天内的媒体消息内容
/// </summary>
/// <param name="sdkfileid">消息体内容中的sdkfileid信息。</param>
/// <param name="proxy">使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081.如不使用代理可以设置为空. 支持sock5跟http代理</param>
/// <param name="passwd">代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123</param>
/// <param name="timeout">超时时间,单位秒</param>
/// <returns>byte[]</returns>
public async Task<byte[]> GetMediaDataAsync(string sdkfileid, string proxy = "", string passwd = "", long timeout = 300)
{
// 调用同步方法并转换为异步
var bytes = GetMediaData(sdkfileid, proxy, passwd, timeout);
return await Task.FromResult(bytes);
}
/// <summary>
/// 获取媒体数据列表(含文件byte[])
/// </summary>
/// <param name="list">媒体数据列表</param>
/// <param name="proxy">使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081.如不使用代理可以设置为空. 支持sock5跟http代理</param>
/// <param name="passwd">代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123</param>
/// <param name="timeout">超时时间,单位秒</param>
/// <returns></returns>
public async Task<List<FileData>> GetFileListAync(List<FileData> list, string proxy = "", string passwd = "", long timeout = 300)
{
List<FileData> listNew = null;
if (list.Count > 0)
{
listNew = new List<FileData>();
foreach (var file in list)
{
byte[] buffer = null;
//也只能拉取5天内的媒体消息内容
if (file.MsgDate > DateTime.Now.AddDays(-5))
{
buffer = await GetMediaDataAsync(file.SdkFileId, proxy, passwd, timeout);
}
file.FileByte = buffer;
listNew.Add(file);
}
}
return listNew;
}
#endregion 异步方法
/// <summary>
/// 获取文本
/// </summary>
/// <param name="slice"></param>
/// <returns></returns>
public string GetContentFromSlice(IntPtr slice)
{
var length = Finance.GetSliceLen(slice);
var bytes = new byte[length];
var ptr = Finance.GetContentFromSlice(slice);
Marshal.Copy(ptr, bytes, 0, bytes.Length);
return Encoding.UTF8.GetString(bytes);
}
/// <summary>
/// 解密
/// </summary>
/// <param name="privateKey">私钥 “-----BEGIN RSA PRIVATE KEY-----”开头 “-----END RSA PRIVATE KEY-----”结尾</param>
/// <param name="text">使用publickey_ver指定版本的公钥进行非对称加密后base64加密的内容</param>
/// <returns></returns>
public string Decrypt(string privateKey, string text)
{
var rsa = new RSACryptoServiceProvider();
var bytes = Convert.FromBase64String(text);
rsa.FromXmlString(RSAKeyConvert.PrivateKeyPemToXml(privateKey));
var result = rsa.Decrypt(bytes, false);
return Encoding.UTF8.GetString(result);
}
}
}
三、代码调用实例
string corpid = ""; //企业微信ID
string secret = ""; //会话内容存档 secret
string privateKey1 = @"-----BEGIN RSA PRIVATE KEY----- 开头 -----END RSA PRIVATE KEY----- 结尾"; //会话内容存档 加密 私钥及对应版本号 pem格式的私钥
string privateKey2 = @"-----BEGIN RSA PRIVATE KEY----- 开头 -----END RSA PRIVATE KEY----- 结尾"; //会话内容存档 加密 私钥及对应版本号 pem格式的私钥
Dictionary<int, string> verKey = new Dictionary<int, string>();
verKey.Add(1, privateKey1);
verKey.Add(2, privateKey2);
var seq = 0; //这个可以第一次使用为0 下一次使用为上一次拉取时保存的最大值(下面有注释)
var client = new FinanceHelper(corpid, secret, verKey);
//同步方法
var result = client.GetChatList(seq);
//异步方法
//var result = await client.GetChatListAsync(seq);
List<IMsgBase> msgList = result.Item1; //企业微信 会话内容 列表
List<FileData> fileList = result.Item2; //媒体内容 列表
//msgList 中最大的seq 值 要保存好 下次拉取数据时要带入
//同一个企业要根据 MsgId 消息去重
//fileList 这个媒体数据 也要根据 SdkFileId 去重
四、相关项目源代码,开源地址:
1.gitee地址:WeWork: 企业微信会话内容存档 (gitee.com)