Java 企业微信会话内容(聊天记录)存档及获取媒体文件

今天终于有空来聊聊企业微信“会话内容存档”,虽然官方有给出开发文档,但确实是有点晦涩难懂啊,对于我这种菜鸟来说。
在网上翻阅许多教程,也有点摸不着头脑,直至后面在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


欢迎转载,原文地址:http://www.lrfun.com/html/technology/java/2021/0806/144.html

上一篇:Java 根据URL,将网页转存为PDF文件
下一篇:没有了