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

今天终于有空来聊聊企业微信“会话内容存档”,虽然官方有给出开发文档,但确实是有点晦涩难懂啊,对于我这种菜鸟来说。
在网上翻阅许多教程,也有点摸不着头脑,直至后面在CSDN上看到2位大神的文档,才整出个所以然。
下面就说一下我的整个开发流程:

一、申请会话内容存档接口,有1个月的试用期可申请,然后配置相关的属性。

\


这里需要注意的是“消息加密公钥”,这是用于加密和解密聊天记录的,相当重要。那个“版本号”,没更新一次,版本号就会+1,个人建议没啥必要就不要经常更换,若要更换也要把历史秘钥对保存起来。因为更新了秘钥对,之前的信息就无法解密了。
秘钥对可以通过此网站生成:http://web.chacuo.net/netrsakeypair

\


定义类RSAEncrypt做加解密处理,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    <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类进行稍微修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
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");
    }
}


五、主业务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
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文件
下一篇:没有了