前言
最近有一个需求是在小程序上开发一个在线聊天的功能,调研了一下觉得腾讯云的IM服务比较合适。
IM介绍
简介
腾讯云IM与应用之间的交互逻辑如图所示
架构
IM 提供全球接入、单聊、群聊、消息推送、资料关系链托管、账号鉴权等全方位解决方案,并提供完备的 App 接入、后台管理接口。
应用场景
包括了社交沟通,互动直播,智能客服,物联网通信、企业通讯等等。
IM服务端接入(spring boot)
接口类
用的是Feign接入
@FeignClient(name = "tencentIMService", url = "${application.tencent-im.request-url}",
configuration = OpenFeignConfig.class, fallback = TencentIMServiceFallback.class)
public interface TencentIMService {
/**
* 查询账号
*
* @param sdkappid sdkApId
* @param identifier 必须为 App 管理员账号
* @param contenttype 请求格式固定值为json
* @param usersig App 管理员账号生成的签名
* @param random 请输入随机的32位无符号整数,取值范围0 - 4294967295
* @param accounts 用户列表
* @return 响应体
*/
@PostMapping("/v4/im_open_login_svc/account_check")
JSONObject checkAccount(@RequestParam("sdkappid") Long sdkappid, @RequestParam("identifier") String identifier, @RequestParam("contenttype") String contenttype,
@RequestParam("usersig") String usersig, @RequestParam("random") Integer random,
@RequestBody IMCheckAccountVM accounts);
/**
* 导入单个账号
*
* @param sdkappid sdkApId
* @param identifier 必须为 App 管理员账号
* @param contenttype 请求格式固定值为json
* @param usersig App 管理员账号生成的签名
* @param random 请输入随机的32位无符号整数,取值范围0 - 4294967295
* @param account 用户
* @return 响应体
*/
@PostMapping("/v4/im_open_login_svc/account_import")
JSONObject importSingleAccount(@RequestParam("sdkappid") Long sdkappid, @RequestParam("identifier") String identifier, @RequestParam("contenttype") String contenttype,
@RequestParam("usersig") String usersig, @RequestParam("random") Integer random,
@RequestBody IMSingleAccountVM account);
/**
* 设置账号资料
*
* @param sdkappid sdkApId
* @param identifier 必须为 App 管理员账号
* @param contenttype 请求格式固定值为json
* @param usersig App 管理员账号生成的签名
* @param random 请输入随机的32位无符号整数,取值范围0 - 4294967295
* @param account 用户
* @return 响应体
*/
@PostMapping("/v4/profile/portrait_set")
JSONObject setAccount(@RequestParam("sdkappid") Long sdkappid, @RequestParam("identifier") String identifier, @RequestParam("contenttype") String contenttype,
@RequestParam("usersig") String usersig, @RequestParam("random") Integer random,
@RequestBody IMProfileVM account);
/**
* 导入多个账号
*
* @param sdkappid sdkApId
* @param identifier 必须为 App 管理员账号
* @param contenttype 请求格式固定值为json
* @param usersig App 管理员账号生成的签名
* @param random 请输入随机的32位无符号整数,取值范围0 - 4294967295
* @param accounts 用户账号列表
* @return 响应体
*/
@PostMapping("/v4/im_open_login_svc/multiaccount_import")
JSONObject importMultiAccount(@RequestParam("sdkappid") Long sdkappid, @RequestParam("identifier") String identifier, @RequestParam("contenttype") String contenttype,
@RequestParam("usersig") String usersig, @RequestParam("random") Integer random,
@RequestBody IMMultiAccountVM accounts);
/**
* 创建群组
*
* @param sdkappid sdkApId
* @param identifier 必须为 App 管理员账号
* @param contenttype 请求格式固定值为json
* @param usersig App 管理员账号生成的签名
* @param random 请输入随机的32位无符号整数,取值范围0 - 4294967295
* @param group 群组
* @return 响应体
*/
@PostMapping("/v4/group_open_http_svc/create_group")
JSONObject createGroup(@RequestParam("sdkappid") Long sdkappid, @RequestParam("identifier") String identifier, @RequestParam("contenttype") String contenttype,
@RequestParam("usersig") String usersig, @RequestParam("random") Integer random,
@RequestBody IMGroupVM group);
/**
* 导入群组成员
*
* @param sdkappid sdkApId
* @param identifier 必须为 App 管理员账号
* @param contenttype 请求格式固定值为json
* @param usersig App 管理员账号生成的签名
* @param random 请输入随机的32位无符号整数,取值范围0 - 4294967295
* @param group 群组
* @return 响应体
*/
@PostMapping("/v4/group_open_http_svc/import_group_member")
JSONObject importGroupMember(@RequestParam("sdkappid") Long sdkappid, @RequestParam("identifier") String identifier, @RequestParam("contenttype") String contenttype,
@RequestParam("usersig") String usersig, @RequestParam("random") Integer random,
@RequestBody IMGroupVM group);
/**
* 在群组中发送普通消息
*
* @param sdkappid sdkApId
* @param identifier 必须为 App 管理员账号
* @param contenttype 请求格式固定值为json
* @param usersig App 管理员账号生成的签名
* @param random 请输入随机的32位无符号整数,取值范围0 - 4294967295
* @param msg 普通消息
* @return 响应体
*/
@PostMapping("/v4/group_open_http_svc/send_group_msg")
JSONObject sendGroupMsg(@RequestParam("sdkappid") Long sdkappid, @RequestParam("identifier") String identifier, @RequestParam("contenttype") String contenttype,
@RequestParam("usersig") String usersig, @RequestParam("random") Integer random,
@RequestBody IMGroupMsgVM msg);
/**
* 在群组中发送系统通知
*
* @param sdkappid sdkApId
* @param identifier 必须为 App 管理员账号
* @param contenttype 请求格式固定值为json
* @param usersig App 管理员账号生成的签名
* @param random 请输入随机的32位无符号整数,取值范围0 - 4294967295
* @param msg 系统消息
* @return 响应体
*/
@PostMapping("/v4/group_open_http_svc/send_group_system_notification")
JSONObject sendGroupSystemNotification(@RequestParam("sdkappid") Long sdkappid, @RequestParam("identifier") String identifier, @RequestParam("contenttype") String contenttype,
@RequestParam("usersig") String usersig, @RequestParam("random") Integer random,
@RequestBody IMGroupSystemMsgVM msg);
/**
* 创建机器人账号
*
* @param sdkappid sdkApId
* @param identifier 必须为 App 管理员账号
* @param contenttype 请求格式固定值为json
* @param usersig App 管理员账号生成的签名
* @param random 请输入随机的32位无符号整数,取值范围0 - 4294967295
* @param msg 系统消息
* @return 响应体
*/
@PostMapping("/v4/openim_robot_http_svc/create_robot")
JSONObject createRobot(@RequestParam("sdkappid") Long sdkappid, @RequestParam("identifier") String identifier, @RequestParam("contenttype") String contenttype,
@RequestParam("usersig") String usersig, @RequestParam("random") Integer random,
@RequestBody IMSingleAccountVM msg);
/**
* 在群组中发送普通消息
*
* @param sdkappid sdkApId
* @param identifier 必须为 App 管理员账号
* @param contenttype 请求格式固定值为json
* @param usersig App 管理员账号生成的签名
* @param random 请输入随机的32位无符号整数,取值范围0 - 4294967295
* @param msg 普通消息
* @return 响应体
*/
@PostMapping("/v4/openim/sendmsg")
JSONObject sendMsg(@RequestParam("sdkappid") Long sdkappid, @RequestParam("identifier") String identifier, @RequestParam("contenttype") String contenttype,
@RequestParam("usersig") String usersig, @RequestParam("random") Integer random,
@RequestBody IMMsgVM msg);
/**
* 在群组中发送普通消息
*
* @param sdkappid sdkApId
* @param identifier 必须为 App 管理员账号
* @param contenttype 请求格式固定值为json
* @param usersig App 管理员账号生成的签名
* @param random 请输入随机的32位无符号整数,取值范围0 - 4294967295
* @param content 内容
* @return 响应体
*/
@PostMapping("/v4/openim/admin_set_msg_read")
JSONObject adminSetMsgRead(@RequestParam("sdkappid") Long sdkappid, @RequestParam("identifier") String identifier, @RequestParam("contenttype") String contenttype,
@RequestParam("usersig") String usersig, @RequestParam("random") Integer random,
@RequestBody String content);
}
签名类
@Component
@Slf4j
@RequiredArgsConstructor
public class IMUserSigService {
private final ApplicationProperties applicationProperties;
/**
* 【功能说明】用于签发 TRTC 和 IM 服务中必须要使用的 UserSig 鉴权票据
* <p>
* 【参数说明】
*
* @param userid - 用户id,限制长度为32字节,只允许包含大小写英文字母(a-zA-Z)、数字(0-9)及下划线和连词符。
* @param expire - UserSig 票据的过期时间,单位是秒,比如 86400 代表生成的 UserSig 票据在一天后就无法再使用了。
* @return usersig -生成的签名
*/
/**
* Function: Used to issue UserSig that is required by the TRTC and IM services.
* <p>
* Parameter description:
*
* @param userid - User ID. The value can be up to 32 bytes in length and contain letters (a-z and A-Z), digits (0-9), underscores (_), and hyphens (-).
* @param expire - UserSig expiration time, in seconds. For example, 86400 indicates that the generated UserSig will expire one day after being generated.
* @return usersig - Generated signature.
*/
public String genUserSig(String userid, long expire) {
return genUserSig(userid, expire, null);
}
/**
* 【功能说明】
* 用于签发 TRTC 进房参数中可选的 PrivateMapKey 权限票据。
* PrivateMapKey 需要跟 UserSig 一起使用,但 PrivateMapKey 比 UserSig 有更强的权限控制能力:
* - UserSig 只能控制某个 UserID 有无使用 TRTC 服务的权限,只要 UserSig 正确,其对应的 UserID 可以进出任意房间。
* - PrivateMapKey 则是将 UserID 的权限控制的更加严格,包括能不能进入某个房间,能不能在该房间里上行音视频等等。
* 如果要开启 PrivateMapKey 严格权限位校验,需要在【实时音视频控制台】/【应用管理】/【应用信息】中打开“启动权限密钥”开关。
* <p>
* 【参数说明】
*
* @param userid - 用户id,限制长度为32字节,只允许包含大小写英文字母(a-zA-Z)、数字(0-9)及下划线和连词符。
* @param expire - PrivateMapKey 票据的过期时间,单位是秒,比如 86400 生成的 PrivateMapKey 票据在一天后就无法再使用了。
* @param roomid - 房间号,用于指定该 userid 可以进入的房间号
* @param privilegeMap - 权限位,使用了一个字节中的 8 个比特位,分别代表八个具体的功能权限开关:
* - 第 1 位:0000 0001 = 1,创建房间的权限
* - 第 2 位:0000 0010 = 2,加入房间的权限
* - 第 3 位:0000 0100 = 4,发送语音的权限
* - 第 4 位:0000 1000 = 8,接收语音的权限
* - 第 5 位:0001 0000 = 16,发送视频的权限
* - 第 6 位:0010 0000 = 32,接收视频的权限
* - 第 7 位:0100 0000 = 64,发送辅路(也就是屏幕分享)视频的权限
* - 第 8 位:1000 0000 = 200,接收辅路(也就是屏幕分享)视频的权限
* - privilegeMap == 1111 1111 == 255 代表该 userid 在该 roomid 房间内的所有功能权限。
* - privilegeMap == 0010 1010 == 42 代表该 userid 拥有加入房间和接收音视频数据的权限,但不具备其他权限。
* @return usersig - 生成带userbuf的签名
*/
/**
* Function:
* Used to issue PrivateMapKey that is optional for room entry.
* PrivateMapKey must be used together with UserSig but with more powerful permission control capabilities.
* - UserSig can only control whether a UserID has permission to use the TRTC service. As long as the UserSig is correct, the user with the corresponding UserID can enter or leave any room.
* - PrivateMapKey specifies more stringent permissions for a UserID, including whether the UserID can be used to enter a specific room and perform audio/video upstreaming in the room.
* To enable stringent PrivateMapKey permission bit verification, you need to enable permission key in TRTC console > Application Management > Application Info.
* <p>
* Parameter description:
*
* @param userid - User ID. The value can be up to 32 bytes in length and contain letters (a-z and A-Z), digits (0-9), underscores (_), and hyphens (-).
* @param roomid - ID of the room to which the specified UserID can enter.
* @param expire - PrivateMapKey expiration time, in seconds. For example, 86400 indicates that the generated PrivateMapKey will expire one day after being generated.
* @param privilegeMap - Permission bits. Eight bits in the same byte are used as the permission switches of eight specific features:
* - Bit 1: 0000 0001 = 1, permission for room creation
* - Bit 2: 0000 0010 = 2, permission for room entry
* - Bit 3: 0000 0100 = 4, permission for audio sending
* - Bit 4: 0000 1000 = 8, permission for audio receiving
* - Bit 5: 0001 0000 = 16, permission for video sending
* - Bit 6: 0010 0000 = 32, permission for video receiving
* - Bit 7: 0100 0000 = 64, permission for substream video sending (screen sharing)
* - Bit 8: 1000 0000 = 200, permission for substream video receiving (screen sharing)
* - privilegeMap == 1111 1111 == 255: Indicates that the UserID has all feature permissions of the room specified by roomid.
* - privilegeMap == 0010 1010 == 42: Indicates that the UserID has only the permissions to enter the room and receive audio/video data.
* @return usersig - Generate signature with userbuf
*/
public String genPrivateMapKey(String userid, long expire, long roomid, long privilegeMap) {
byte[] userbuf = genUserBuf(userid, roomid, expire, privilegeMap, 0, ""); //生成userbuf
return genUserSig(userid, expire, userbuf);
}
/**
* 【功能说明】
* 用于签发 TRTC 进房参数中可选的 PrivateMapKey 权限票据。
* PrivateMapKey 需要跟 UserSig 一起使用,但 PrivateMapKey 比 UserSig 有更强的权限控制能力:
* - UserSig 只能控制某个 UserID 有无使用 TRTC 服务的权限,只要 UserSig 正确,其对应的 UserID 可以进出任意房间。
* - PrivateMapKey 则是将 UserID 的权限控制的更加严格,包括能不能进入某个房间,能不能在该房间里上行音视频等等。
* 如果要开启 PrivateMapKey 严格权限位校验,需要在【实时音视频控制台】/【应用管理】/【应用信息】中打开“启动权限密钥”开关。
* <p>
* 【参数说明】
*
* @param userid - 用户id,限制长度为32字节,只允许包含大小写英文字母(a-zA-Z)、数字(0-9)及下划线和连词符。
* @param expire - PrivateMapKey 票据的过期时间,单位是秒,比如 86400 生成的 PrivateMapKey 票据在一天后就无法再使用了。
* @param roomstr - 字符串房间号,用于指定该 userid 可以进入的房间号
* @param privilegeMap - 权限位,使用了一个字节中的 8 个比特位,分别代表八个具体的功能权限开关:
* - 第 1 位:0000 0001 = 1,创建房间的权限
* - 第 2 位:0000 0010 = 2,加入房间的权限
* - 第 3 位:0000 0100 = 4,发送语音的权限
* - 第 4 位:0000 1000 = 8,接收语音的权限
* - 第 5 位:0001 0000 = 16,发送视频的权限
* - 第 6 位:0010 0000 = 32,接收视频的权限
* - 第 7 位:0100 0000 = 64,发送辅路(也就是屏幕分享)视频的权限
* - 第 8 位:1000 0000 = 200,接收辅路(也就是屏幕分享)视频的权限
* - privilegeMap == 1111 1111 == 255 代表该 userid 在该 roomid 房间内的所有功能权限。
* - privilegeMap == 0010 1010 == 42 代表该 userid 拥有加入房间和接收音视频数据的权限,但不具备其他权限。
* @return usersig - 生成带userbuf的签名
*/
/**
* Function:
* Used to issue PrivateMapKey that is optional for room entry.
* PrivateMapKey must be used together with UserSig but with more powerful permission control capabilities.
* - UserSig can only control whether a UserID has permission to use the TRTC service. As long as the UserSig is correct, the user with the corresponding UserID can enter or leave any room.
* - PrivateMapKey specifies more stringent permissions for a UserID, including whether the UserID can be used to enter a specific room and perform audio/video upstreaming in the room.
* To enable stringent PrivateMapKey permission bit verification, you need to enable permission key in TRTC console > Application Management > Application Info.
* <p>
* Parameter description:
*
* @param userid - User ID. The value can be up to 32 bytes in length and contain letters (a-z and A-Z), digits (0-9), underscores (_), and hyphens (-).
* @param roomstr - ID of the room to which the specified UserID can enter.
* @param expire - PrivateMapKey expiration time, in seconds. For example, 86400 indicates that the generated PrivateMapKey will expire one day after being generated.
* @param privilegeMap - Permission bits. Eight bits in the same byte are used as the permission switches of eight specific features:
* - Bit 1: 0000 0001 = 1, permission for room creation
* - Bit 2: 0000 0010 = 2, permission for room entry
* - Bit 3: 0000 0100 = 4, permission for audio sending
* - Bit 4: 0000 1000 = 8, permission for audio receiving
* - Bit 5: 0001 0000 = 16, permission for video sending
* - Bit 6: 0010 0000 = 32, permission for video receiving
* - Bit 7: 0100 0000 = 64, permission for substream video sending (screen sharing)
* - Bit 8: 1000 0000 = 200, permission for substream video receiving (screen sharing)
* - privilegeMap == 1111 1111 == 255: Indicates that the UserID has all feature permissions of the room specified by roomid.
* - privilegeMap == 0010 1010 == 42: Indicates that the UserID has only the permissions to enter the room and receive audio/video data.
* @return usersig - Generate signature with userbuf
*/
public String genPrivateMapKeyWithStringRoomID(String userid, long expire, String roomstr, long privilegeMap) {
byte[] userbuf = genUserBuf(userid, 0, expire, privilegeMap, 0, roomstr); //生成userbuf
return genUserSig(userid, expire, userbuf);
}
private String hmacsha256(String identifier, long currTime, long expire, String base64Userbuf) {
ApplicationProperties.TencentIm tencentIm = applicationProperties.getTencentIm();
String contentToBeSigned = "TLS.identifier:" + identifier + "\n"
+ "TLS.sdkappid:" + tencentIm.getSdkAppId() + "\n"
+ "TLS.time:" + currTime + "\n"
+ "TLS.expire:" + expire + "\n";
if (null != base64Userbuf) {
contentToBeSigned += "TLS.userbuf:" + base64Userbuf + "\n";
}
try {
byte[] byteKey = tencentIm.getKey().getBytes(StandardCharsets.UTF_8);
Mac hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(byteKey, "HmacSHA256");
hmac.init(keySpec);
byte[] byteSig = hmac.doFinal(contentToBeSigned.getBytes(StandardCharsets.UTF_8));
return (Base64.getEncoder().encodeToString(byteSig)).replaceAll("\\s*", "");
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
return "";
}
}
private String genUserSig(String userid, long expire, byte[] userbuf) {
ApplicationProperties.TencentIm tencentIm = applicationProperties.getTencentIm();
long currTime = System.currentTimeMillis() / 1000;
JSONObject sigDoc = new JSONObject();
sigDoc.put("TLS.ver", "2.0");
sigDoc.put("TLS.identifier", userid);
sigDoc.put("TLS.sdkappid", tencentIm.getSdkAppId());
sigDoc.put("TLS.expire", expire);
sigDoc.put("TLS.time", currTime);
String base64UserBuf = null;
if (null != userbuf) {
base64UserBuf = Base64.getEncoder().encodeToString(userbuf).replaceAll("\\s*", "");
sigDoc.put("TLS.userbuf", base64UserBuf);
}
String sig = hmacsha256(userid, currTime, expire, base64UserBuf);
if (sig.length() == 0) {
return "";
}
sigDoc.put("TLS.sig", sig);
Deflater compressor = new Deflater();
compressor.setInput(sigDoc.toString().getBytes(StandardCharsets.UTF_8));
compressor.finish();
byte[] compressedBytes = new byte[2048];
int compressedBytesLength = compressor.deflate(compressedBytes);
compressor.end();
return (new String(base64EncodeUrl(Arrays.copyOfRange(compressedBytes,
0, compressedBytesLength)))).replaceAll("\\s*", "");
}
public byte[] genUserBuf(String account, long dwAuthID, long dwExpTime,
long dwPrivilegeMap, long dwAccountType, String RoomStr) {
//视频校验位需要用到的字段,按照网络字节序放入buf中
/*
cVer unsigned char/1 版本号,填0
wAccountLen unsigned short /2 第三方自己的帐号长度
account wAccountLen 第三方自己的帐号字符
dwSdkAppid unsigned int/4 sdkappid
dwAuthID unsigned int/4 群组号码
dwExpTime unsigned int/4 过期时间 ,直接使用填入的值
dwPrivilegeMap unsigned int/4 权限位,主播0xff,观众0xab
dwAccountType unsigned int/4 第三方帐号类型
*/
//The fields required for the video check digit are placed in buf according to the network byte order.
/*
cVer unsigned char/1 Version number, fill in 0
wAccountLen unsigned short /2 Third party's own account length
account wAccountLen Third party's own account characters
dwSdkAppid unsigned int/4 sdkappid
dwAuthID unsigned int/4 group number
dwExpTime unsigned int/4 Expiration time , use the filled value directly
dwPrivilegeMap unsigned int/4 Permission bits, host 0xff, audience 0xab
dwAccountType unsigned int/4 Third-party account type
*/
long sdkAppId = applicationProperties.getTencentIm().getSdkAppId();
int accountLength = account.length();
int roomStrLength = RoomStr.length();
int offset = 0;
int bufLength = 1 + 2 + accountLength + 20;
if (roomStrLength > 0) {
bufLength = bufLength + 2 + roomStrLength;
}
byte[] userbuf = new byte[bufLength];
//cVer
if (roomStrLength > 0) {
userbuf[offset++] = 1;
} else {
userbuf[offset++] = 0;
}
//wAccountLen
userbuf[offset++] = (byte) ((accountLength & 0xFF00) >> 8);
userbuf[offset++] = (byte) (accountLength & 0x00FF);
//account
for (; offset < 3 + accountLength; ++offset) {
userbuf[offset] = (byte) account.charAt(offset - 3);
}
//dwSdkAppid
userbuf[offset++] = (byte) ((sdkAppId & 0xFF000000) >> 24);
userbuf[offset++] = (byte) ((sdkAppId & 0x00FF0000) >> 16);
userbuf[offset++] = (byte) ((sdkAppId & 0x0000FF00) >> 8);
userbuf[offset++] = (byte) (sdkAppId & 0x000000FF);
//dwAuthId,房间号
//dwAuthId, room number
userbuf[offset++] = (byte) ((dwAuthID & 0xFF000000) >> 24);
userbuf[offset++] = (byte) ((dwAuthID & 0x00FF0000) >> 16);
userbuf[offset++] = (byte) ((dwAuthID & 0x0000FF00) >> 8);
userbuf[offset++] = (byte) (dwAuthID & 0x000000FF);
//expire,过期时间,当前时间 + 有效期(单位:秒)
//expire,Expiration time, current time + validity period (unit: seconds)
long currTime = System.currentTimeMillis() / 1000;
long expire = currTime + dwExpTime;
userbuf[offset++] = (byte) ((expire & 0xFF000000) >> 24);
userbuf[offset++] = (byte) ((expire & 0x00FF0000) >> 16);
userbuf[offset++] = (byte) ((expire & 0x0000FF00) >> 8);
userbuf[offset++] = (byte) (expire & 0x000000FF);
//dwPrivilegeMap,权限位
//dwPrivilegeMap,Permission bits
userbuf[offset++] = (byte) ((dwPrivilegeMap & 0xFF000000) >> 24);
userbuf[offset++] = (byte) ((dwPrivilegeMap & 0x00FF0000) >> 16);
userbuf[offset++] = (byte) ((dwPrivilegeMap & 0x0000FF00) >> 8);
userbuf[offset++] = (byte) (dwPrivilegeMap & 0x000000FF);
//dwAccountType,账户类型
//dwAccountType,account type
userbuf[offset++] = (byte) ((dwAccountType & 0xFF000000) >> 24);
userbuf[offset++] = (byte) ((dwAccountType & 0x00FF0000) >> 16);
userbuf[offset++] = (byte) ((dwAccountType & 0x0000FF00) >> 8);
userbuf[offset++] = (byte) (dwAccountType & 0x000000FF);
if (roomStrLength > 0) {
//roomStrLen
userbuf[offset++] = (byte) ((roomStrLength & 0xFF00) >> 8);
userbuf[offset++] = (byte) (roomStrLength & 0x00FF);
//roomStr
for (; offset < bufLength; ++offset) {
userbuf[offset] = (byte) RoomStr.charAt(offset - (bufLength - roomStrLength));
}
}
return userbuf;
}
public static byte[] base64EncodeUrl(byte[] input) {
byte[] base64 = Base64.getEncoder().encode(input);
for (int i = 0; i < base64.length; ++i)
switch (base64[i]) {
case '+':
base64[i] = '*';
break;
case '/':
base64[i] = '-';
break;
case '=':
base64[i] = '_';
break;
default:
break;
}
return base64;
}
}
服务类
@Component
@Slf4j
@RequiredArgsConstructor
public class IMService {
private final IMUserSigService userSigService;
private final TencentIMService tencentIMService;
private final ApplicationProperties applicationProperties;
/**
* 生成腾讯云用户的UserSig
*
* @param userid 腾讯IM用户ID
* @return UserSig
*/
public String genUserSig(String userid) {
return userSigService.genUserSig(userid, 30 * 24 * 3600);
}
/**
* 查询用户
*
* @param users 用户列表
* @return List 不存在的IM用户
*/
public List<String> checkAccount(List<String> users) {
ApplicationProperties.TencentIm tencentIm = applicationProperties.getTencentIm();
String userSig = genUserSig(tencentIm.getIdentifier());
List<IMCheckAccountItemVM> accountItems = users.stream().map(u -> {
IMCheckAccountItemVM single = new IMCheckAccountItemVM();
single.setUserId(u);
return single;
}).toList();
IMCheckAccountVM accounts = IMCheckAccountVM.builder().items(accountItems).build();
int random = RandomUtil.randomInt(0, Integer.MAX_VALUE);
JSONObject result = tencentIMService.checkAccount(tencentIm.getSdkAppId(), tencentIm.getIdentifier(), tencentIm.getContenttype(), userSig, random,
accounts);
log.info("im -> account-> import -> result:[{}],[{}]", result.getStr("ErrorCode"), result.getStr("ErrorInfo"));
if (!CharSequenceUtil.equals("OK", result.getStr("ActionStatus"))) {
throw new SystemProblem(AppStatus.FAILURE, "查询IM账号出现错误!");
}
List<String> noExistAccounts = new ArrayList<>();
JSONArray resultItem = result.getJSONArray("ResultItem");
resultItem.forEach(item -> {
JSONObject itemObj = JSONUtil.parseObj(item);
if (itemObj.getInt("ResultCode") == 0 && CharSequenceUtil.equals("NotImported", itemObj.getStr("AccountStatus"))) {
noExistAccounts.add(itemObj.getStr("UserID"));
}
});
return noExistAccounts;
}
/**
* 导入单个用户
*
* @param userId 用户ID
* @param nick 名称
* @param faceUrl 头像
* @return boolean
*/
public boolean importSingleAccount(String userId, String nick, String faceUrl) {
ApplicationProperties.TencentIm tencentIm = applicationProperties.getTencentIm();
String userSig = genUserSig(tencentIm.getIdentifier());
IMSingleAccountVM singleAccount = IMSingleAccountVM.builder().userId(userId).nick(nick).faceUrl(faceUrl).build();
int random = RandomUtil.randomInt(0, Integer.MAX_VALUE);
JSONObject result = tencentIMService.importSingleAccount(tencentIm.getSdkAppId(), tencentIm.getIdentifier(), tencentIm.getContenttype(), userSig, random,
singleAccount);
log.info("im -> account-> import -> result:[{}],[{}]", result.getStr("ErrorCode"), result.getStr("ErrorInfo"));
return CharSequenceUtil.equals("OK", result.getStr("ActionStatus"));
}
/**
* 导入多个用户
*
* @param userIds 用户ID列表
* @return boolean
*/
public boolean importMultiAccount(List<IMUserInfo> userIds) {
ApplicationProperties.TencentIm tencentIm = applicationProperties.getTencentIm();
String userSig = genUserSig(tencentIm.getIdentifier());
List<IMSingleAccountVM> accounts = userIds.stream().map(u -> {
IMSingleAccountVM single = new IMSingleAccountVM();
single.setUserId(u.getUserId());
single.setNick(u.getNick());
single.setFaceUrl(u.getFaceUrl());
return single;
}).toList();
IMMultiAccountVM multiAccount = IMMultiAccountVM.builder().accounts(accounts).build();
int random = RandomUtil.randomInt(0, Integer.MAX_VALUE);
JSONObject result = tencentIMService.importMultiAccount(tencentIm.getSdkAppId(), tencentIm.getIdentifier(), tencentIm.getContenttype(), userSig, random,
multiAccount);
log.info("im -> account-> import -> result:[{}],[{}]", result.getStr("ErrorCode"), result.getStr("ErrorInfo"));
return CharSequenceUtil.equals("OK", result.getStr("ActionStatus"));
}
/**
* 创建群组
*
* @param owner 群主
* @param groupName 群组名称
* @param type 群组类型:Private/Public/ChatRoom/Community(不支持AVChatRoom)(必填)
* @param members 成员列表
* @return boolean
*/
public String createGroup(String owner, String groupName, String type, List<String> members) {
ApplicationProperties.TencentIm tencentIm = applicationProperties.getTencentIm();
String userSig = genUserSig(tencentIm.getIdentifier());
//群组信息
List<IMGroupMemberVM> memberList = members.stream().map(
m -> {
IMGroupMemberVM member = new IMGroupMemberVM();
member.setMemberAccount(m);
return member;
}).collect(Collectors.toList());
IMGroupVM group = IMGroupVM.builder().ownerAccount(owner).name(groupName).type(type).memberList(memberList).build();
int random = RandomUtil.randomInt(0, Integer.MAX_VALUE);
JSONObject result = tencentIMService.createGroup(tencentIm.getSdkAppId(), tencentIm.getIdentifier(), tencentIm.getContenttype(), userSig, random,
group);
String errorInfo = result.getStr("ErrorInfo");
log.info("im -> create-> group -> result:[{}],[{}]", result.getStr("ErrorCode"), result.getStr("ErrorInfo"));
if (!CharSequenceUtil.equals("OK", result.getStr("ActionStatus"))) {
throw new SystemProblem(AppStatus.FAILURE, "create group occurs to exception : " + errorInfo);
}
return result.getStr("GroupId");
}
/**
* 导入群组成员
*
* @param groupId 群组ID
* @param members 群组成员
* @return boolean
*/
public String importGroupMember(String groupId, List<String> members) {
ApplicationProperties.TencentIm tencentIm = applicationProperties.getTencentIm();
String userSig = genUserSig(tencentIm.getIdentifier());
//群组信息
List<IMGroupMemberVM> memberList = members.stream().map(
m -> {
IMGroupMemberVM member = new IMGroupMemberVM();
member.setMemberAccount(m);
return member;
}).collect(Collectors.toList());
IMGroupVM group = IMGroupVM.builder().groupId(groupId).memberList(memberList).build();
int random = RandomUtil.randomInt(0, Integer.MAX_VALUE);
JSONObject result = tencentIMService.importGroupMember(tencentIm.getSdkAppId(), tencentIm.getIdentifier(), tencentIm.getContenttype(), userSig, random,
group);
String errorInfo = result.getStr("ErrorInfo");
log.info("im -> import-> group-member -> result:[{}],[{}]", result.getStr("ErrorCode"), result.getStr("ErrorInfo"));
if (!CharSequenceUtil.equals("OK", result.getStr("ActionStatus"))) {
throw new SystemProblem(AppStatus.FAILURE, "create group occurs to exception : " + errorInfo);
}
return result.getStr("GroupId");
}
/**
* 向群组发送普通消息
*
* @param groupId 群组ID
* @param fromAccount 指定账号发送
* @param msgBodyList 消息体
* @return boolean
*/
public boolean sendGroupMsg(String groupId, String fromAccount, List<IMGroupMsgBodyVM> msgBodyList) {
ApplicationProperties.TencentIm tencentIm = applicationProperties.getTencentIm();
String userSig = genUserSig(tencentIm.getIdentifier());
//群组信息
IMGroupMsgVM groupMsg = IMGroupMsgVM.builder().groupId(groupId).fromAccount(fromAccount)
.msgBody(msgBodyList).build();
int random = RandomUtil.randomInt(0, Integer.MAX_VALUE);
JSONObject result = tencentIMService.sendGroupMsg(tencentIm.getSdkAppId(), tencentIm.getIdentifier(), tencentIm.getContenttype(), userSig, random,
groupMsg);
log.info("im -> send-> group-msg -> result:[{}],[{}]", result.getStr("ErrorCode"), result.getStr("ErrorInfo"));
return CharSequenceUtil.equals("OK", result.getStr("ActionStatus"));
}
/**
* 向群组发送系统消息
*
* @param groupId 群组ID
* @param content 系统消息
* @return boolean
*/
public boolean sendGroupSystemMsg(String groupId, String content) {
ApplicationProperties.TencentIm tencentIm = applicationProperties.getTencentIm();
String userSig = genUserSig(tencentIm.getIdentifier());
//系统信息
IMGroupSystemMsgVM groupSystemMsg = IMGroupSystemMsgVM.builder().groupId(groupId).content(content).build();
int random = RandomUtil.randomInt(0, Integer.MAX_VALUE);
JSONObject result = tencentIMService.sendGroupSystemNotification(tencentIm.getSdkAppId(), tencentIm.getIdentifier(), tencentIm.getContenttype(), userSig, random,
groupSystemMsg);
log.info("im -> send-> system-msg -> result:[{}],[{}]", result.getStr("ErrorCode"), result.getStr("ErrorInfo"));
return CharSequenceUtil.equals("OK", result.getStr("ActionStatus"));
}
/**
* 创建机器人用户
*
* @param userId 用户ID
* @param nick 名称
* @param faceUrl 头像
* @return boolean
*/
public boolean createRobot(String userId, String nick, String faceUrl) {
ApplicationProperties.TencentIm tencentIm = applicationProperties.getTencentIm();
String userSig = genUserSig(tencentIm.getIdentifier());
IMSingleAccountVM singleAccount = IMSingleAccountVM.builder().userId(userId).nick(nick).faceUrl(faceUrl).build();
int random = RandomUtil.randomInt(0, Integer.MAX_VALUE);
JSONObject result = tencentIMService.createRobot(tencentIm.getSdkAppId(), tencentIm.getIdentifier(), tencentIm.getContenttype(), userSig, random,
singleAccount);
log.info("im -> create-> robot -> result:[{}],[{}]", result.getStr("ErrorCode"), result.getStr("ErrorInfo"));
return CharSequenceUtil.equals("OK", result.getStr("ActionStatus"));
}
/**
* 发送单聊消息
*
* @param fromAccount 指定账号发送
* @param toAccount 指定账号接受
* @param msgBodyList 消息体
* @return boolean
*/
public boolean sendMsg(String fromAccount, String toAccount, List<IMGroupMsgBodyVM> msgBodyList) {
ApplicationProperties.TencentIm tencentIm = applicationProperties.getTencentIm();
String userSig = genUserSig(tencentIm.getIdentifier());
int random = RandomUtil.randomInt(0, Integer.MAX_VALUE);
//群组信息
IMMsgVM msgVM = IMMsgVM.builder().fromAccount(fromAccount).toAccount(toAccount)
.msgBody(msgBodyList).random(random).build();
JSONObject result = tencentIMService.sendMsg(tencentIm.getSdkAppId(), tencentIm.getIdentifier(), tencentIm.getContenttype(), userSig, random,
msgVM);
log.info("im -> send-> msg -> result:[{}],[{}]", result.getStr("ErrorCode"), result.getStr("ErrorInfo"));
return CharSequenceUtil.equals("OK", result.getStr("ActionStatus"));
}
/**
* 设置用户资料
*
* @param userId 用户ID
* @param nick 昵称
* @param faceUrl 头像
*/
public void setAccount(Long userId, String nick, String faceUrl) {
List<String> accounts = new ArrayList<>();
accounts.add("PATIENT_" + userId);
//不存在账号
List<String> noExists = checkAccount(accounts);
accounts.removeAll(noExists);
if (CollUtil.isNotEmpty(accounts)) {
List<IMProfileItemVM> profileItems = new ArrayList<>();
profileItems.add(IMProfileItemVM.builder().tag("Tag_Profile_IM_Nick").value(nick).build());
// profileItems.add(IMProfileItemVM.builder().tag("Tag_Profile_IM_Image").value(faceUrl).build());
for (String account : accounts) {
ApplicationProperties.TencentIm tencentIm = applicationProperties.getTencentIm();
String userSig = genUserSig(tencentIm.getIdentifier());
int random = RandomUtil.randomInt(0, Integer.MAX_VALUE);
IMProfileVM profileVM = IMProfileVM.builder().fromAccount(account).profileItem(profileItems).build();
JSONObject result = tencentIMService.setAccount(tencentIm.getSdkAppId(), tencentIm.getIdentifier(), tencentIm.getContenttype(), userSig, random,
profileVM);
log.info("im -> send-> msg -> result:[{}],[{}]", result.getStr("ErrorCode"), result.getStr("ErrorInfo"));
}
}
}
/**
* 管理员指定 reportAccount 将会话 peerAccount 的单聊未读计数清除。
*
* @param reportAccount 进行会话未读计数清理的用户 UserId
* @param peerAccount 单聊会话的另一方用户 UserId
*/
public void setMsgRead(String reportAccount, String peerAccount) {
ApplicationProperties.TencentIm tencentIm = applicationProperties.getTencentIm();
String userSig = genUserSig(tencentIm.getIdentifier());
int random = RandomUtil.randomInt(0, Integer.MAX_VALUE);
Map<String, String> content = new HashMap<>();
content.put("Report_Account", reportAccount);
content.put("Peer_Account", peerAccount);
JSONObject result = tencentIMService.adminSetMsgRead(tencentIm.getSdkAppId(), tencentIm.getIdentifier(), tencentIm.getContenttype(), userSig, random,
JSONUtil.toJsonStr(content));
log.info("im -> send-> msg -> result:[{}],[{}]", result.getStr("ErrorCode"), result.getStr("ErrorInfo"));
}
}