mirror of
https://gitee.com/binary/weixin-java-tools.git
synced 2026-03-10 00:13:40 +08:00
🆕 #1596 【企业微信】新增会话存档相关接口
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
package me.chanjar.weixin.cp.api;
|
||||
|
||||
import lombok.NonNull;
|
||||
import me.chanjar.weixin.common.error.WxErrorException;
|
||||
import me.chanjar.weixin.cp.bean.msgaudit.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 会话内容存档接口.
|
||||
* 官方文档:https://developer.work.weixin.qq.com/document/path/91360
|
||||
* <p>
|
||||
* 如需自行实现,亦可调用Finance类库函数,进行实现:
|
||||
* com.tencent.wework.Finance
|
||||
*
|
||||
* @author Wang_Wong
|
||||
* @date 2022-01-14
|
||||
*/
|
||||
public interface WxCpMsgAuditService {
|
||||
|
||||
/**
|
||||
* 拉取聊天记录函数
|
||||
*
|
||||
* @param seq 从指定的seq开始拉取消息,注意的是返回的消息从seq+1开始返回,seq为之前接口返回的最大seq值。首次使用请使用seq:0
|
||||
* @param limit 一次拉取的消息条数,最大值1000条,超过1000条会返回错误
|
||||
* @param proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081,如果没有传null
|
||||
* @param passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123,如果没有传null
|
||||
* @param timeout 超时时间,根据实际需要填写
|
||||
* @return 返回是否调用成功
|
||||
*/
|
||||
WxCpChatDatas getChatDatas(long seq, @NonNull long limit, String proxy, String passwd, @NonNull long timeout) throws Exception;
|
||||
|
||||
/**
|
||||
* 获取解密的聊天数据Model
|
||||
*
|
||||
* @param chatData getChatDatas()获取到的聊天数据
|
||||
* @return 解密后的聊天数据
|
||||
* @throws Exception
|
||||
*/
|
||||
WxCpChatModel getDecryptData(@NonNull WxCpChatDatas.WxCpChatData chatData) throws Exception;
|
||||
|
||||
/**
|
||||
* 获取解密的聊天数据明文
|
||||
*
|
||||
* @param chatData getChatDatas()获取到的聊天数据
|
||||
* @return 解密后的明文
|
||||
* @throws Exception
|
||||
*/
|
||||
String getChatPlainText(@NonNull WxCpChatDatas.WxCpChatData chatData) throws Exception;
|
||||
|
||||
/**
|
||||
* 获取媒体文件
|
||||
* 针对图片、文件等媒体数据,提供sdk接口拉取数据内容。
|
||||
*
|
||||
* 注意:
|
||||
* 根据上面返回的文件类型,拼接好存放文件的绝对路径即可。此时绝对路径写入文件流,来达到获取媒体文件的目的。
|
||||
* 详情可以看官方文档,亦可阅读此接口源码。
|
||||
*
|
||||
* @param sdkfileid 消息体内容中的sdkfileid信息
|
||||
* @param proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081,如果没有传null
|
||||
* @param passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123,如果没有传null
|
||||
* @param timeout 超时时间,分片数据需累加到文件存储。单次最大返回512K字节,如果文件比较大,自行设置长一点,比如timeout=10000
|
||||
* @param targetFilePath 目标文件绝对路径+实际文件名,比如:/usr/local/file/20220114/474f866b39d10718810d55262af82662.gif
|
||||
* @throws WxErrorException
|
||||
*/
|
||||
void getMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout, @NonNull String targetFilePath) throws WxErrorException;
|
||||
|
||||
/**
|
||||
* 获取会话内容存档开启成员列表
|
||||
* 企业可通过此接口,获取企业开启会话内容存档的成员列表
|
||||
* <p>
|
||||
* 请求方式:POST(HTTPS)
|
||||
* 请求地址:https://qyapi.weixin.qq.com/cgi-bin/msgaudit/get_permit_user_list?access_token=ACCESS_TOKEN
|
||||
*
|
||||
* @param type 拉取对应版本的开启成员列表。1表示办公版;2表示服务版;3表示企业版。非必填,不填写的时候返回全量成员列表。
|
||||
* @return
|
||||
* @throws WxErrorException
|
||||
*/
|
||||
List<String> getPermitUserList(Integer type) throws WxErrorException;
|
||||
|
||||
/**
|
||||
* 获取会话内容存档内部群信息
|
||||
* 企业可通过此接口,获取会话内容存档本企业的内部群信息,包括群名称、群主id、公告、群创建时间以及所有群成员的id与加入时间。
|
||||
* <p>
|
||||
* 请求方式:POST(HTTPS)
|
||||
* 请求地址:https://qyapi.weixin.qq.com/cgi-bin/msgaudit/groupchat/get?access_token=ACCESS_TOKEN
|
||||
*
|
||||
* @param roomid 待查询的群id
|
||||
* @return
|
||||
* @throws WxErrorException
|
||||
*/
|
||||
WxCpGroupChat getGroupChat(@NonNull String roomid) throws WxErrorException;
|
||||
|
||||
/**
|
||||
* 获取会话同意情况
|
||||
* 企业可通过下述接口,获取会话中外部成员的同意情况
|
||||
* <p>
|
||||
* 单聊请求地址:https://qyapi.weixin.qq.com/cgi-bin/msgaudit/check_single_agree?access_token=ACCESS_TOKEN
|
||||
* <p>
|
||||
* 请求方式:POST(HTTPS)
|
||||
*
|
||||
* @param checkAgreeRequest 待查询的会话信息
|
||||
* @return
|
||||
* @throws WxErrorException
|
||||
*/
|
||||
WxCpAgreeInfo checkSingleAgree(@NonNull WxCpCheckAgreeRequest checkAgreeRequest) throws WxErrorException;
|
||||
|
||||
}
|
||||
@@ -399,6 +399,13 @@ public interface WxCpService extends WxService {
|
||||
*/
|
||||
WxCpLivingService getLivingService();
|
||||
|
||||
/**
|
||||
* 获取会话存档相关接口的服务类对象
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
WxCpMsgAuditService getMsgAuditService();
|
||||
|
||||
/**
|
||||
* 获取日历相关接口的服务类对象
|
||||
*
|
||||
|
||||
@@ -50,6 +50,7 @@ public abstract class BaseWxCpServiceImpl<H, P> implements WxCpService, RequestH
|
||||
private WxCpAgentService agentService = new WxCpAgentServiceImpl(this);
|
||||
private WxCpOaService oaService = new WxCpOaServiceImpl(this);
|
||||
private WxCpLivingService livingService = new WxCpLivingServiceImpl(this);
|
||||
private WxCpMsgAuditService msgAuditService = new WxCpMsgAuditServiceImpl(this);
|
||||
private WxCpTaskCardService taskCardService = new WxCpTaskCardServiceImpl(this);
|
||||
private WxCpExternalContactService externalContactService = new WxCpExternalContactServiceImpl(this);
|
||||
private WxCpGroupRobotService groupRobotService = new WxCpGroupRobotServiceImpl(this);
|
||||
@@ -484,6 +485,11 @@ public abstract class BaseWxCpServiceImpl<H, P> implements WxCpService, RequestH
|
||||
return livingService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WxCpMsgAuditService getMsgAuditService() {
|
||||
return msgAuditService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WxCpOaCalendarService getOaCalendarService() {
|
||||
return this.oaCalendarService;
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
package me.chanjar.weixin.cp.api.impl;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.tencent.wework.Finance;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.chanjar.weixin.common.error.WxErrorException;
|
||||
import me.chanjar.weixin.common.util.json.GsonParser;
|
||||
import me.chanjar.weixin.cp.api.WxCpMsgAuditService;
|
||||
import me.chanjar.weixin.cp.api.WxCpService;
|
||||
import me.chanjar.weixin.cp.bean.msgaudit.*;
|
||||
import me.chanjar.weixin.cp.util.crypto.WxCpCryptUtil;
|
||||
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.util.List;
|
||||
|
||||
import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.MsgAudit.*;
|
||||
|
||||
/**
|
||||
* 会话内容存档接口实现类.
|
||||
*
|
||||
* @author Wang_Wong
|
||||
* @date 2022-01-17
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService {
|
||||
private final WxCpService cpService;
|
||||
|
||||
@Override
|
||||
public WxCpChatDatas getChatDatas(long seq, @NonNull long limit, String proxy, String passwd, @NonNull long timeout) throws Exception {
|
||||
String configPath = cpService.getWxCpConfigStorage().getMsgAuditLibPath();
|
||||
if (StringUtils.isEmpty(configPath)) {
|
||||
throw new WxErrorException("请配置会话存档sdk文件的路径,不要配错了!!");
|
||||
}
|
||||
|
||||
// 替换斜杠
|
||||
String replacePath = configPath.replace("\\", "/");
|
||||
// 所有的后缀文件
|
||||
String suffixFiles = replacePath.substring(replacePath.lastIndexOf("/") + 1);
|
||||
// 获取的前缀路径
|
||||
String prefixPath = replacePath.substring(0, replacePath.lastIndexOf("/") + 1);
|
||||
|
||||
// 包含so文件
|
||||
String[] libFiles = suffixFiles.split(",");
|
||||
if (libFiles.length <= 0) {
|
||||
throw new WxErrorException("请仔细配置会话存档文件路径!!");
|
||||
}
|
||||
|
||||
Finance.loadingLibraries(libFiles, prefixPath);
|
||||
long sdk = Finance.SingletonSDK();
|
||||
|
||||
long ret = Finance.Init(sdk, cpService.getWxCpConfigStorage().getCorpId(), cpService.getWxCpConfigStorage().getCorpSecret());
|
||||
if (ret != 0) {
|
||||
Finance.DestroySingletonSDK(sdk);
|
||||
throw new WxErrorException("init sdk err ret " + ret);
|
||||
}
|
||||
|
||||
long slice = Finance.NewSlice();
|
||||
ret = Finance.GetChatData(sdk, seq, limit, proxy, passwd, timeout, slice);
|
||||
if (ret != 0) {
|
||||
Finance.FreeSlice(slice);
|
||||
Finance.DestroySingletonSDK(sdk);
|
||||
throw new WxErrorException("getchatdata err ret " + ret);
|
||||
}
|
||||
|
||||
// 拉取会话存档
|
||||
String content = Finance.GetContentFromSlice(slice);
|
||||
Finance.FreeSlice(slice);
|
||||
WxCpChatDatas chatDatas = WxCpChatDatas.fromJson(content);
|
||||
if (chatDatas.getErrCode().intValue() != 0) {
|
||||
throw new WxErrorException(chatDatas.toJson());
|
||||
}
|
||||
|
||||
return chatDatas;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WxCpChatModel getDecryptData(@NonNull WxCpChatDatas.WxCpChatData chatData) throws Exception {
|
||||
String plainText = this.decryptChatData(chatData);
|
||||
return WxCpChatModel.fromJson(plainText);
|
||||
}
|
||||
|
||||
public String decryptChatData(WxCpChatDatas.WxCpChatData chatData) throws Exception {
|
||||
// 企业获取的会话内容将用公钥加密,企业可用自行保存的私钥解开会话内容数据,aeskey不能为空
|
||||
String priKey = cpService.getWxCpConfigStorage().getAesKey();
|
||||
if (StringUtils.isEmpty(priKey)) {
|
||||
throw new WxErrorException("请配置会话存档私钥【aesKey】");
|
||||
}
|
||||
|
||||
String decryptByPriKey = WxCpCryptUtil.decryptByPriKey(chatData.getEncryptRandomKey(), priKey);
|
||||
// 每次使用DecryptData解密会话存档前需要调用NewSlice获取一个slice,在使用完slice中数据后,还需要调用FreeSlice释放。
|
||||
long sdk = Finance.SingletonSDK();
|
||||
long msg = Finance.NewSlice();
|
||||
|
||||
int ret = Finance.DecryptData(sdk, decryptByPriKey, chatData.getEncryptChatMsg(), msg);
|
||||
if (ret != 0) {
|
||||
Finance.FreeSlice(msg);
|
||||
Finance.DestroySingletonSDK(sdk);
|
||||
throw new WxErrorException("msg err ret " + ret);
|
||||
}
|
||||
|
||||
// 明文
|
||||
String plainText = Finance.GetContentFromSlice(msg);
|
||||
Finance.FreeSlice(msg);
|
||||
return plainText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getChatPlainText(WxCpChatDatas.@NonNull WxCpChatData chatData) throws Exception {
|
||||
return this.decryptChatData(chatData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout, @NonNull String targetFilePath) throws WxErrorException {
|
||||
/**
|
||||
* 1、媒体文件每次拉取的最大size为512k,因此超过512k的文件需要分片拉取。
|
||||
* 2、若该文件未拉取完整,sdk的IsMediaDataFinish接口会返回0,同时通过GetOutIndexBuf接口返回下次拉取需要传入GetMediaData的indexbuf。
|
||||
* 3、indexbuf一般格式如右侧所示,”Range:bytes=524288-1048575“:表示这次拉取的是从524288到1048575的分片。单个文件首次拉取填写的indexbuf为空字符串,拉取后续分片时直接填入上次返回的indexbuf即可。
|
||||
*/
|
||||
File targetFile = new File(targetFilePath);
|
||||
if (!targetFile.getParentFile().exists()) {
|
||||
targetFile.getParentFile().mkdirs();
|
||||
}
|
||||
|
||||
String indexbuf = "";
|
||||
int ret, data_len = 0;
|
||||
while (true) {
|
||||
long mediaData = Finance.NewMediaData();
|
||||
long sdk = Finance.SingletonSDK();
|
||||
ret = Finance.GetMediaData(sdk, indexbuf, sdkfileid, proxy, passwd, timeout, mediaData);
|
||||
if (ret != 0) {
|
||||
Finance.FreeMediaData(mediaData);
|
||||
Finance.DestroySingletonSDK(sdk);
|
||||
throw new WxErrorException("getmediadata err ret " + ret);
|
||||
}
|
||||
|
||||
data_len += Finance.GetDataLen(mediaData);
|
||||
log.info("正在分片拉取媒体文件 len:{}, data_len:{}, is_finis:{} \n", Finance.GetIndexLen(mediaData), data_len, Finance.IsMediaDataFinish(mediaData));
|
||||
|
||||
try {
|
||||
// 大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。
|
||||
FileOutputStream outputStream = new FileOutputStream(new File(targetFilePath), 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getPermitUserList(Integer type) throws WxErrorException {
|
||||
final String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_PERMIT_USER_LIST);
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
if (type != null) {
|
||||
jsonObject.addProperty("type", type);
|
||||
}
|
||||
String responseContent = this.cpService.post(apiUrl, jsonObject.toString());
|
||||
return WxCpGsonBuilder.create().fromJson(GsonParser.parse(responseContent).getAsJsonArray("ids"),
|
||||
new TypeToken<List<String>>() {
|
||||
}.getType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public WxCpGroupChat getGroupChat(@NonNull String roomid) throws WxErrorException {
|
||||
final String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_GROUP_CHAT);
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.addProperty("roomid", roomid);
|
||||
String responseContent = this.cpService.post(apiUrl, jsonObject.toString());
|
||||
return WxCpGroupChat.fromJson(responseContent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WxCpAgreeInfo checkSingleAgree(@NonNull WxCpCheckAgreeRequest checkAgreeRequest) throws WxErrorException {
|
||||
String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(CHECK_SINGLE_AGREE);
|
||||
String responseContent = this.cpService.post(apiUrl, checkAgreeRequest.toJson());
|
||||
return WxCpAgreeInfo.fromJson(responseContent);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user