Java 企业微信会话内容(聊天记录)存档及获取媒体文件
发表时间: 2021-08-06 16:54:03 | 浏览次数:
今天终于有空来聊聊企业微信“会话内容存档”,虽然官方有给出开发文档,但确实是有点晦涩难懂啊,对于我这种菜鸟来说。在网上翻阅许多教程,也有点摸不着头脑,直至后面在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; } }
需要添加的依赖:
<!--<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; } }
此时,可以拉取到聊天记录并入库了,要将这堆聊天记录对应的展示出来,还需做很多工作,如:获取内部成员、客户列表、客户群列表等等。
总之,开发完整个功能,小编真的是脱了一层皮,所以开发时跟自己说,开发完一定要把教程分享出来,让大家少走点弯路。
本教程主要参考了:
https://blog.csdn.net/weixin_42932323/article/details/118326236
https://blog.csdn.net/u011056339/article/details/105704995
上一篇:Java 根据URL,将网页转存为PDF文件
下一篇:没有了