注:本文档是在Linux环境下进行测试的。
今天终于有空来聊聊企业微信“会话内容存档”,虽然官方有给出开发文档,但确实是有点晦涩难懂啊,对于我这种菜鸟来说。
在网上翻阅许多教程,也有点摸不着头脑,直至后面在CSDN上看到2位大神的文档,才整出个所以然。
下面就说一下我的整个开发流程:
一、申请会话内容存档接口,有1个月的试用期可申请,然后配置相关的属性。
这里需要注意的是“消息加密公钥”,这是用于加密和解密聊天记录的,相当重要。那个“版本号”,没更新一次,版本号就会+1,个人建议没啥必要就不要经常更换,若要更换也要把历史秘钥对保存起来。因为更新了秘钥对,之前的信息就无法解密了。
秘钥对可以通过此网站生成:http://web.chacuo.net/netrsakeypair
定义类RSAEncrypt做加解密处理,代码如下:
package com.tencent.wework;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import javax.crypto.Cipher;
import java.io.Reader;
import java.io.StringReader;
import java.security.*;
public class RSAEncrypt {
public static String decryptRSA(String str, String privateKey) throws Exception {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
Cipher rsa = Cipher.getInstance("RSA/ECB/PKCS1Padding", "BC");
rsa.init(Cipher.DECRYPT_MODE, getPrivateKey(privateKey));
byte[] utf8 = rsa.doFinal(Base64.decodeBase64(str));
String result = new String(utf8,"UTF-8");
return result;
}
public static PrivateKey getPrivateKey (String privateKey) throws Exception {
Reader privateKeyReader = new StringReader(privateKey);
PEMParser privatePemParser = new PEMParser(privateKeyReader);
Object privateObject = privatePemParser.readObject();
if (privateObject instanceof PEMKeyPair) {
PEMKeyPair pemKeyPair = (PEMKeyPair) privateObject;
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
PrivateKey privKey = converter.getPrivateKey(pemKeyPair.getPrivateKeyInfo());
return privKey;
}
return null;
}
}
需要添加的Maven依赖:
<!--<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.64</version>
</dependency>(这个好像可以不要)-->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpg-jdk16</artifactId>
<version>1.46</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.64</version>
</dependency>
二、大致看一下官方给出的整个业务流程:
三、下载官方提供的SDK(小编用的是Linux环境的SDK,至于Windows的至今还没搞懂为啥报错,所以没有使用),主要是使用到libWeWorkFinanceSdk_Java.so文件,把该文件放到某目录下,可以让程序加载到就行(小编就直接放到/root/workwx/目录下了)。
项目目录结构:
注意:Finance类必须放在com.tencent.wework目录下,不然会报错(虽然没验证过,但很多都这样说,你们可以测试一下)
四、将官方提供的Finance类进行稍微修改:
package com.tencent.wework;
public class Finance {
public native static long NewSdk();
/**
* 初始化函数
* Return值=0表示该API调用成功
*
* @param [in] sdk NewSdk返回的sdk指针
* @param [in] corpid 调用企业的企业id,例如:wwd08c8exxxx5ab44d,可以在企业微信管理端--我的企业--企业信息查看
* @param [in] secret 聊天内容存档的Secret,可以在企业微信管理端--管理工具--聊天内容存档查看
*
* @return 返回是否初始化成功
* 0 - 成功
* !=0 - 失败
*/
public native static int Init(long sdk, String corpid, String secret);
/**
* 拉取聊天记录函数
* Return值=0表示该API调用成功
*
*
* @param [in] sdk NewSdk返回的sdk指针
* @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 [out] chatDatas 返回本次拉取消息的数据,slice结构体.内容包括errcode/errmsg,以及每条消息内容。
*
* @return 返回是否调用成功
* 0 - 成功
* !=0 - 失败
*/
public native static int GetChatData(long sdk, long seq, long limit, String proxy, String passwd, long timeout, long chatData);
/**
* 拉取媒体消息函数
* 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 [out] media_data 返回本次拉取的媒体数据.MediaData结构体.内容包括data(数据内容)/outindexbuf(下次索引)/is_finish(拉取完成标记)
*
* @return 返回是否调用成功
* 0 - 成功
* !=0 - 失败
*/
public native static int GetMediaData(long sdk, String indexbuf, String sdkField, String proxy, String passwd, long timeout, long mediaData);
/**
* @brief 解析密文
* @param [in] encrypt_key, getchatdata返回的encrypt_key
* @param [in] encrypt_msg, getchatdata返回的content
* @param [out] msg, 解密的消息明文
* @return 返回是否调用成功
* 0 - 成功
* !=0 - 失败
*/
public native static int DecryptData(long sdk, String encrypt_key, String encrypt_msg, long msg);
public native static void DestroySdk(long sdk);
public native static long NewSlice();
/**
* @brief 释放slice,和NewSlice成对使用
* @return
*/
public native static void FreeSlice(long slice);
/**
* @brief 获取slice内容
* @return 内容
*/
public native static String GetContentFromSlice(long slice);
/**
* @brief 获取slice内容长度
* @return 内容
*/
public native static int GetSliceLen(long slice);
public native static long NewMediaData();
public native static void FreeMediaData(long mediaData);
/**
* @brief 获取mediadata outindex
* @return outindex
*/
public native static String GetOutIndexBuf(long mediaData);
/**
* @brief 获取mediadata data数据
* @return data
*/
public native static byte[] GetData(long mediaData);
public native static int GetIndexLen(long mediaData);
public native static int GetDataLen(long mediaData);
/**
* @brief 判断mediadata是否结束
* @return 1完成、0未完成
*/
public native static int IsMediaDataFinish(long mediaData);
static {
System.load("/root/workwx/libWeWorkFinanceSdk_Java.so");
}
}
五、主要业务代码:
package com.tencent.wework;
import java.io.File;
import java.io.FileOutputStream;
import java.util.Arrays;
import java.util.List;
import org.json.JSONArray;
import org.json.JSONObject;
public class FinanceDemo {
private static String priKey = "-----BEGIN RSA PRIVATE KEY-----\n"
+ "..."
+ "-----END RSA PRIVATE KEY-----";
public void demo() {
long sdk = Finance.NewSdk();
Finance.Init(sdk, "corpid", "secret"); // 初始化
long ret = 0;
int seq = 0; // 从指定的seq开始拉取消息,注意的是返回的消息从seq+1开始返回,seq为之前接口返回的最大seq值。首次使用请使用seq:0(这个值需要记录下来,以便下一次的拉去)
int limit = 60;
long slice = Finance.NewSlice();
ret = Finance.GetChatData(sdk, seq, limit, null, null, 3, slice);
if (ret != 0) {
System.out.println("getchatdata ret " + ret);
return;
}
String getchatdata = Finance.GetContentFromSlice(slice);
System.out.println(seq + ",拉去的聊天记录密文结果:" + getchatdata);
JSONObject jo = new JSONObject(getchatdata);
JSONArray chatdata = jo.getJSONArray("chatdata");
System.out.println("消息数:" + chatdata.length());
for (int i = 0; i < chatdata.length(); i++) {
JSONObject data = new JSONObject(chatdata.get(i).toString());
String encryptRandomKey = data.getString("encrypt_random_key");
String encryptChatMsg = data.getString("encrypt_chat_msg");
long msg = Finance.NewSlice();
try {
// 聊天记录密文解密
String message = RSAEncrypt.decryptRSA(encryptRandomKey, priKey);
ret = Finance.DecryptData(sdk, message, encryptChatMsg, msg);
if (ret != 0) {
System.out.println("getchatdata ret " + ret);
return;
}
String plaintext = Finance.GetContentFromSlice(msg);
System.out.println("decrypt ret:" + ret + " msg:" + plaintext);
Finance.FreeSlice(msg);
JSONObject plaintextJson = new JSONObject(plaintext);
// 拉去媒体文件解密
String msgtype = plaintextJson.getString("msgtype");
if ("mixed".equals(msgtype)) {
// 混合消息
JSONArray array = new JSONArray();
JSONObject mixed = new JSONObject(plaintextJson.get("mixed").toString());
JSONArray items = mixed.getJSONArray("item");
for (int j = 0; j < items.length(); j++) {
JSONObject item = new JSONObject(items.get(j).toString());
JSONObject content = new JSONObject(item.getString("content"));
String type = item.getString("type");
if ("text".equals(type)) {
item.put("content", content.getString("content"));
} else {
String url = pullMediaFiles(sdk, type, content);
item.put("content", url);
}
array.put(item);
}
JSONObject content = new JSONObject();
content.put(msgtype, array.toString());
plaintextJson.put(msgtype, content.toString());
} else {
pullMediaFiles(sdk, msgtype, plaintextJson);
}
// 会话内容写入数据库
System.out.println(plaintextJson);
// save(plaintextJson);
} catch (Exception e) {
e.printStackTrace();
return;
}
}
}
// 拉去媒体信息
private String pullMediaFiles(long sdk, String msgtype, JSONObject plaintextJson) {
String[] msgtypeStr = {"image", "voice", "video", "emotion", "file"};
List<String> msgtypeList = Arrays.asList(msgtypeStr);
if (msgtypeList.contains(msgtype)) {
String savefileName = "";
JSONObject file = new JSONObject();
if (!plaintextJson.isNull("msgid")) {
file = plaintextJson.getJSONObject(msgtype);
savefileName = plaintextJson.getString("msgid");
} else {
// 混合消息
file = plaintextJson;
savefileName = file.getString("md5sum");
}
System.out.println("媒体文件信息:" + file);
/* ============ 文件存储目录及文件名 Start ============ */
String suffix = "";
switch (msgtype) {
case "image" : suffix = ".jpg"; break;
case "voice" : suffix = ".amr"; break;
case "video" : suffix = ".mp4"; break;
case "emotion" :
int type = (int) file.get("type");
if (type == 1) suffix = ".gif";
else if (type == 2) suffix = ".png";
break;
case "file" :
suffix = "." + file.getString("fileext");
break;
}
savefileName += suffix;
String path = "/var/data/workwx/";
String savefile = path + savefileName;
File targetFile = new File(savefile);
if (!targetFile.getParentFile().exists())
//创建父级文件路径
targetFile.getParentFile().mkdirs();
/* ============ 文件存储目录及文件名 End ============ */
/* ============ 拉去文件 Start ============ */
int i = 0; boolean isSave = true;
String indexbuf = "", sdkfileid = file.getString("sdkfileid");
while (true) {
long mediaData = Finance.NewMediaData();
int ret = Finance.GetMediaData(sdk, indexbuf, sdkfileid, null, null, 3, mediaData);
if (ret != 0) {
System.out.println("getmediadata ret:" + ret);
Finance.FreeMediaData(mediaData);
return null;
}
System.out.printf("getmediadata outindex len:%d, data_len:%d, is_finis:%d\n",
Finance.GetIndexLen(mediaData), Finance.GetDataLen(mediaData),
Finance.IsMediaDataFinish(mediaData));
try {
// 大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。
FileOutputStream outputStream = new FileOutputStream(new File(savefile), true);
outputStream.write(Finance.GetData(mediaData));
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
if (Finance.IsMediaDataFinish(mediaData) == 1) {
// 已经拉取完成最后一个分片
Finance.FreeMediaData(mediaData);
break;
} else {
// 获取下次拉取需要使用的indexbuf
indexbuf = Finance.GetOutIndexBuf(mediaData);
Finance.FreeMediaData(mediaData);
}
// 若文件大于50M则不保存
if (++i > 100) {
isSave = false;
break;
}
}
/* ============ 拉去文件 End ============ */
if (isSave) {
file.put("sdkfileid", savefile);
return savefile;
}
}
return null;
}
}
此时,可以拉取到聊天记录并入库了,要将这堆聊天记录对应的展示出来,还需做很多工作,如:获取内部成员、客户列表、客户群列表等等(后续如果大家有需要再分享出来吧)。
总之,开发完整个功能,小编真的是脱了一层皮,所以开发时跟自己说,开发完一定要把教程分享出来,让大家少走点弯路。