Today, I finally have time to talk about the "conversation content archiving" of enterprise wechat. Although the official has given the development documents, it is really a little obscure and difficult to understand, for a rookie like me.
After reading many tutorials on the Internet, I was a little confused until I saw the documents of two great gods on CSDN.
Let's talk about my whole development process:
1, Apply for the session content archiving interface. You can apply for a 1-month trial period, and then configure relevant properties.
Note that the "message encryption public key", which is used to encrypt and decrypt chat records, is very important. If the "version number" is not updated once, the version number will be + 1. I suggest that it is not necessary to replace it often. If you want to replace it, you should also save the historical secret key pair. Because the secret key pair is updated, the previous information cannot be decrypted.
Secret key pairs can be generated through this website: http://web.chacuo.net/netrsakeypair
Define the class RSAEncrypt for encryption and decryption. The code is as follows:
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 dependencies to be added:
<!--<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.64</version> </dependency>(This seems OK (no)--> <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>
2, Take a general look at the whole business process officially given:
3, Download the SDK provided by the government (the SDK of Linux environment is used by the Xiaobian, but the SDK of Windows has not been used yet. The main reason is to use the libWeWorkFinanceSdk_Java.so file. Put the file in a directory so that the program can be loaded (the Xiaobian can be directly placed in the / root/workwx / directory).
Project directory structure:
Note: the Finance class must be placed on COM tencent. Under the Wework directory, otherwise an error will be reported (although it has not been verified, many say so, you can test it)
4, Slightly modify the officially provided Finance class:
package com.tencent.wework; public class Finance { public native static long NewSdk(); /** * Initialization function * Return A value of = 0 indicates that the API call was successful * * @param [in] sdk NewSdk sdk pointer returned * @param [in] corpid Call the enterprise id of the enterprise, for example: wwd08c8exx5ab44d, which can be viewed at the enterprise wechat management end - my enterprise - enterprise information * @param [in] secret The Secret of chat content archiving can be viewed on the enterprise wechat management side - management tools - chat content archiving * * @return Returns whether initialization succeeded * 0 - success * !=0 - fail */ public native static int Init(long sdk, String corpid, String secret); /** * Pull chat record function * Return A value of = 0 indicates that the API call was successful * * * @param [in] sdk NewSdk sdk pointer returned * @param [in] seq Pull the message from the specified seq. Note that the returned message starts from seq+1. seq is the maximum seq value returned by the previous interface. Please use seq:0 for the first time * @param [in] limit The maximum number of messages pulled at one time is 1000. If more than 1000, an error will be returned * @param [in] proxy A request to use a proxy requires a link to the incoming proxy. For example: Socks5: / / 10.0 0.1:8081 or http://10.0.0.1:8081 * @param [in] passwd The proxy account password needs to be passed in. Such as user_ name:passwd_ one hundred and twenty-three * @param [out] chatDatas Return the data of this pull message, slice structure The content includes errcode/errmsg and the content of each message. * * @return Returns whether the call was successful * 0 - success * !=0 - fail */ public native static int GetChatData(long sdk, long seq, long limit, String proxy, String passwd, long timeout, long chatData); /** * Pull media message function * Return A value of = 0 indicates that the API call was successful * * * @param [in] sdk NewSdk sdk pointer returned * @param [in] sdkFileid The media message includes the sdkfileid of the chat message returned from GetChatData * @param [in] proxy A request to use a proxy requires a link to the incoming proxy. For example: Socks5: / / 10.0 0.1:8081 or http://10.0.0.1:8081 * @param [in] passwd The proxy account password needs to be passed in. Such as user_ name:passwd_ one hundred and twenty-three * @param [in] indexbuf When media messages are pulled in pieces, you need to fill in the index information for each pull. It is not required to fill in the first time. 512k is pulled by default. For each subsequent call, you only need to fill in the outindexbuf returned from the last call. * @param [out] media_data Returns the media data pulled this time MediaData structure The contents include data / outindexbuf / is_ Finish (pull completion mark) * * @return Returns whether the call was successful * 0 - success * !=0 - fail */ public native static int GetMediaData(long sdk, String indexbuf, String sdkField, String proxy, String passwd, long timeout, long mediaData); /** * @brief Parsing ciphertext * @param [in] encrypt_key, getchatdata Returned encrypt_key * @param [in] encrypt_msg, getchatdata Returned content * @param [out] msg, Decrypted message plaintext * @return Returns whether the call was successful * 0 - success * !=0 - fail */ 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 Release slice and use it in pairs with NewSlice * @return */ public native static void FreeSlice(long slice); /** * @brief Get slice content * @return content */ public native static String GetContentFromSlice(long slice); /** * @brief Get slice content length * @return content */ public native static int GetSliceLen(long slice); public native static long NewMediaData(); public native static void FreeMediaData(long mediaData); /** * @brief Get mediadata outindex * @return outindex */ public native static String GetOutIndexBuf(long mediaData); /** * @brief Get 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 Judge whether mediadata ends * @return 1 Completed, 0 incomplete */ public native static int IsMediaDataFinish(long mediaData); static { System.load("/root/workwx/libWeWorkFinanceSdk_Java.so"); } }
5, Main business code:
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"); // initialization long ret = 0; int seq = 0; // Pull the message from the specified SEQ. Note that the returned message starts from seq+1, and seq is the maximum SEQ value returned by the previous interface. Please use seq:0 for the first time (this value needs to be recorded for the next pull) 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 + ",Chat record ciphertext result pulled:" + getchatdata); JSONObject jo = new JSONObject(getchatdata); JSONArray chatdata = jo.getJSONArray("chatdata"); System.out.println("Number of messages:" + 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 { // Decryption of chat record ciphertext 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); // Pull media file decryption String msgtype = plaintextJson.getString("msgtype"); if ("mixed".equals(msgtype)) { // Mixed message 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); } // Session content written to database System.out.println(plaintextJson); // save(plaintextJson); } catch (Exception e) { e.printStackTrace(); return; } } } // Pull media information 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 { // Mixed message file = plaintextJson; savefileName = file.getString("md5sum"); } System.out.println("Media file information:" + file); /* ============ File storage directory and file name============ */ 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()) //Create parent file path targetFile.getParentFile().mkdirs(); /* ============ File storage directory and file name End============ */ /* ============ Pull file 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 { // Files larger than 512k will be pulled in pieces. Additional writing is required here to avoid subsequent pieces overwriting the previous data. 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) { // The last slice has been pulled Finance.FreeMediaData(mediaData); break; } else { // Get the indexbuf to be used for the next pull indexbuf = Finance.GetOutIndexBuf(mediaData); Finance.FreeMediaData(mediaData); } // If the file is larger than 50M, it will not be saved if (++i > 100) { isSave = false; break; } } /* ============ Pull file End============ */ if (isSave) { file.put("sdkfileid", savefile); return savefile; } } return null; } }
At this time, you can pull the chat records and store them. To display the corresponding chat records, you still need to do a lot of work, such as obtaining internal members, customer lists, customer group lists, etc. (you can share them later if you need to).
In short, the development of a complete function, Xiaobian really took off a layer of skin, so when developing, tell yourself that you must share the tutorial after development, so that everyone can avoid detours.
This tutorial mainly refers to:
https://blog.csdn.net/weixin_42932323/article/details/118326236
https://blog.csdn.net/u011056339/article/details/105704995
More articles can be found on Xiaobian's personal blog website: www.lrfun.com com