XuqmGroup-HarmonySDK/xuqm-sdk/src/main/ets/im/ImClient.ets
XuqmGroup f423a2acb2 feat(im): 添加即时消息SDK核心功能实现
- 实现了聊天消息发送功能,支持文本、图片、视频、音频、文件等多种消息类型
- 集成了文件上传下载功能,支持多媒体文件的传输和管理
- 添加了群组管理功能,包括创建群组、成员管理、权限控制等操作
- 实现了好友系统,支持好友添加、删除、分组等功能
- 集成了黑名单管理,提供用户屏蔽和解除屏蔽功能
- 添加了会话管理功能,支持对话列表、未读消息统计等
- 实现了历史消息查询和搜索功能
- 添加了实时连接状态管理和自动重连机制
2026-05-03 00:11:05 +08:00

899 行
32 KiB
Plaintext

import webSocket from '@ohos.net.webSocket'
import http from '@ohos.net.http'
import { HttpClient } from '../core/HttpClient'
import { SDKContext } from '../core/SDKContext'
import { DEFAULT_API_BASE_URL, DEFAULT_IM_WS_URL } from '../core/Endpoints'
import type {
ChatType,
ConversationData,
FriendRequest,
GroupJoinRequest,
HistoryQuery,
ImGroup,
ImMessage,
MsgType,
PageResult,
SendMessageParams,
UserProfile,
} from '../core/Types'
export interface ImEventDelegate {
onConnected?(): void
onDisconnected?(code: number, reason: string): void
onMessage?(msg: ImMessage): void
onRead?(msg: ImMessage): void
onRevoke?(data: RevokeData): void
onError?(message: string): void
onConversationsChange?(conversations: ConversationData[]): void
}
export interface RevokeData {
msgId: string
operatorId: string
}
class WebSocketFrame {
type: string = ''
payload: Object = new Object()
}
class WebSocketEnvelope {
destination: string = ''
payload: Object = new Object()
}
class AppBody {
appId: string = ''
}
class HistoryQueryParams {
appId: string = ''
page: number = 0
size: number = 0
msgType: MsgType = 'TEXT'
keyword: string = ''
startTime: string = ''
endTime: string = ''
}
class ConversationActionBody {
appId: string = ''
chatType: ChatType = 'SINGLE'
}
class FriendRequestQueryBody {
appId: string = ''
direction: 'incoming' | 'outgoing' = 'incoming'
}
class FriendRequestBody {
appId: string = ''
toUserId: string = ''
remark: string = ''
}
class GroupJoinRequestBody {
appId: string = ''
remark: string = ''
}
class EditMessageBody {
content: string = ''
}
class UpdateProfileBody {
appId: string = ''
nickname: string = ''
avatar: string = ''
gender: string = ''
}
class DraftBody {
appId: string = ''
chatType: ChatType = 'SINGLE'
draft: string = ''
}
class PinBody {
appId: string = ''
chatType: ChatType = 'SINGLE'
pinned: boolean = false
}
class MuteBody {
appId: string = ''
chatType: ChatType = 'SINGLE'
muted: boolean = false
}
class SendEnvelopePayload {
messageId: string = ''
toId: string = ''
chatType: ChatType = 'SINGLE'
msgType: MsgType = 'TEXT'
content: string = ''
mentionedUserIds: string = ''
}
class RevokeEnvelopePayload {
msgId: string = ''
}
const MAX_RECONNECT_DELAY = 30_000
export class ImClient {
private ws: webSocket.WebSocket | null = null
private reconnectDelay: number = 3_000
private reconnectTimer: number | null = null
private destroyed: boolean = false
delegate: ImEventDelegate | null = null
connect(): void {
if (this.destroyed) return
const config = SDKContext.getConfig()
const token = SDKContext.getToken() ?? ''
const url = DEFAULT_IM_WS_URL.replace(/\/$/, '') + '?token=' + encodeURIComponent(token)
this.ws = webSocket.createWebSocket()
this.ws.on('open', (_err: Error, _value: Object) => {
this.reconnectDelay = 3_000
if (config.debug) console.log('[ImClient] connected')
this.delegate?.onConnected?.()
})
this.ws.on('message', (_err: Error, value: string | ArrayBuffer) => {
try {
if (typeof value !== 'string') {
return
}
const frame = JSON.parse(value) as WebSocketFrame
if (frame.type === 'MESSAGE') {
const message = this.normalizeMessage(frame.payload as ImMessage)
if (message.status === 'READ') {
this.delegate?.onRead?.(message)
}
if (message.revoked || message.status === 'REVOKED' || message.msgType === 'REVOKED') {
this.delegate?.onRevoke?.({ msgId: message.id, operatorId: message.fromId })
}
this.delegate?.onMessage?.(message)
this.notifyConversations()
} else if (frame.type === 'REVOKE') {
this.delegate?.onRevoke?.(frame.payload as RevokeData)
this.notifyConversations()
}
} catch {
// ignore malformed frames
}
})
this.ws.on('close', (_err: Error, value: webSocket.CloseResult) => {
this.delegate?.onDisconnected?.(value.code, value.reason)
if (!this.destroyed) this.scheduleReconnect()
})
this.ws.on('error', (_err: Error) => {
this.delegate?.onError?.(_err.message)
})
this.ws.connect(url, {})
}
send(params: SendMessageParams): ImMessage {
const outgoing = this.buildOutgoingMessage(params)
if (!this.ws) {
return this.markFailed(outgoing)
}
const payload = new SendEnvelopePayload()
payload.messageId = outgoing.id
payload.toId = params.toId
payload.chatType = params.chatType
payload.msgType = params.msgType
payload.content = params.content
if (params.mentionedUserIds !== undefined) {
payload.mentionedUserIds = params.mentionedUserIds
}
const envelope = new WebSocketEnvelope()
envelope.destination = '/app/chat.send'
envelope.payload = payload
this.ws.send(JSON.stringify(envelope), (_err: Error) => {
if (_err) {
this.delegate?.onError?.(_err.message)
}
})
return outgoing
}
revoke(msgId: string): void {
if (!this.ws) {
throw new Error('WebSocket not connected')
}
const payload = new RevokeEnvelopePayload()
payload.msgId = msgId
const envelope = new WebSocketEnvelope()
envelope.destination = '/app/chat.revoke'
envelope.payload = payload
this.ws.send(JSON.stringify(envelope), (_err: Error) => {
if (_err) this.delegate?.onError?.(_err.message)
})
}
async fetchHistory(
toId: string,
page: number = 0,
size: number = 20,
query: HistoryQuery = {},
): Promise<PageResult<ImMessage>> {
const queryString = this.buildHistoryQuery(page, size, query)
return HttpClient.get<PageResult<ImMessage>>('/api/im/messages/history/' + encodeURIComponent(toId), queryString)
}
async fetchGroupHistory(
groupId: string,
page: number = 0,
size: number = 50,
query: HistoryQuery = {},
): Promise<PageResult<ImMessage>> {
const queryString = this.buildHistoryQuery(page, size, query)
return HttpClient.get<PageResult<ImMessage>>('/api/im/messages/group-history/' + encodeURIComponent(groupId), queryString)
}
async locateHistoryPage(
toId: string,
messageId: string,
pageSize: number = 20,
maxPages: number = 20,
): Promise<ImMessage[] | null> {
const pageCount = Math.max(maxPages, 1)
for (let page = 0; page < pageCount; page += 1) {
const result = await this.fetchHistory(toId, page, pageSize)
const messages = result.content ?? []
if (messages.some(item => item.id === messageId)) {
return messages
}
if (messages.length < pageSize) {
return null
}
}
return null
}
async locateGroupHistoryPage(
groupId: string,
messageId: string,
pageSize: number = 50,
maxPages: number = 20,
): Promise<ImMessage[] | null> {
const pageCount = Math.max(maxPages, 1)
for (let page = 0; page < pageCount; page += 1) {
const result = await this.fetchGroupHistory(groupId, page, pageSize)
const messages = result.content ?? []
if (messages.some(item => item.id === messageId)) {
return messages
}
if (messages.length < pageSize) {
return null
}
}
return null
}
async listConversations(size: number = 20): Promise<ConversationData[]> {
return HttpClient.get<ConversationData[]>('/api/im/conversations', this.buildConversationQuery(size))
}
async markRead(targetId: string, chatType: ChatType = 'SINGLE'): Promise<void> {
await HttpClient.put<void>('/api/im/conversations/' + encodeURIComponent(targetId) + '/read', undefined, this.buildConversationActionQuery(chatType))
}
async setDraft(targetId: string, chatType: ChatType, draft: string): Promise<void> {
const params = new DraftBody()
params.appId = SDKContext.getConfig().appKey
params.chatType = chatType
params.draft = draft
await HttpClient.put<void>('/api/im/conversations/' + encodeURIComponent(targetId) + '/draft', params)
}
async setConversationPinned(targetId: string, chatType: ChatType, pinned: boolean): Promise<void> {
const params = new PinBody()
params.appId = SDKContext.getConfig().appKey
params.chatType = chatType
params.pinned = pinned
await HttpClient.put<void>('/api/im/conversations/' + encodeURIComponent(targetId) + '/pinned', params)
}
async setConversationMuted(targetId: string, chatType: ChatType, muted: boolean): Promise<void> {
const params = new MuteBody()
params.appId = SDKContext.getConfig().appKey
params.chatType = chatType
params.muted = muted
await HttpClient.put<void>('/api/im/conversations/' + encodeURIComponent(targetId) + '/muted', params)
}
async deleteConversation(targetId: string, chatType: ChatType): Promise<void> {
await HttpClient.delete<void>('/api/im/conversations/' + encodeURIComponent(targetId), this.buildConversationActionQuery(chatType))
}
async listFriends(): Promise<string[]> {
return HttpClient.get<string[]>('/api/im/friends', this.buildAppQuery())
}
async listGroups(): Promise<ImGroup[]> {
return HttpClient.get<ImGroup[]>('/api/im/groups', this.buildAppQuery())
}
async getGroupInfo(groupId: string): Promise<ImGroup> {
return HttpClient.get<ImGroup>('/api/im/groups/' + encodeURIComponent(groupId), this.buildAppQuery())
}
async listGroupMembers(groupId: string): Promise<UserProfile[]> {
return HttpClient.get<UserProfile[]>(
'/api/im/groups/' + encodeURIComponent(groupId) + '/members',
this.buildAppQuery(),
)
}
async searchGroupMembers(groupId: string, keyword: string, size: number = 20): Promise<UserProfile[]> {
return HttpClient.get<UserProfile[]>(
'/api/im/groups/' + encodeURIComponent(groupId) + '/members/search',
this.buildSearchQuery(keyword, size),
)
}
async searchUsers(keyword: string, size: number = 20): Promise<UserProfile[]> {
return HttpClient.get<UserProfile[]>(
'/api/im/admin/users/search',
this.buildSearchQuery(keyword, size),
)
}
async searchGroups(keyword: string, size: number = 20): Promise<ImGroup[]> {
return HttpClient.get<ImGroup[]>(
'/api/im/admin/groups/search',
this.buildSearchQuery(keyword, size),
)
}
async searchMessages(
keyword: string = '',
chatType: ChatType | '' = '',
msgType: MsgType | '' = '',
page: number = 0,
size: number = 20,
): Promise<PageResult<ImMessage>> {
return HttpClient.get<PageResult<ImMessage>>(
'/api/im/admin/messages/search',
this.buildMessageSearchQuery(keyword, chatType, msgType, page, size),
)
}
async editMessage(messageId: string, content: string): Promise<ImMessage> {
const body = new EditMessageBody()
body.content = content
return HttpClient.put<ImMessage>(
'/api/im/messages/' + encodeURIComponent(messageId),
body,
this.buildAppQuery(),
)
}
async revokeMessage(messageId: string): Promise<ImMessage> {
return HttpClient.post<ImMessage>(
'/api/im/messages/' + encodeURIComponent(messageId) + '/revoke',
this.buildAppBody(),
this.buildAppQuery(),
)
}
async sendFriendRequest(toUserId: string, remark: string | null = null): Promise<FriendRequest> {
const params = new FriendRequestBody()
params.appId = SDKContext.getConfig().appKey
params.toUserId = toUserId
if (remark !== null && remark !== '') {
params.remark = remark
}
return HttpClient.post<FriendRequest>('/api/im/friend-requests', params)
}
async listFriendRequests(direction: 'incoming' | 'outgoing' = 'incoming'): Promise<FriendRequest[]> {
return HttpClient.get<FriendRequest[]>('/api/im/friend-requests', this.buildFriendRequestQuery(direction))
}
async acceptFriendRequest(requestId: string): Promise<FriendRequest> {
return HttpClient.post<FriendRequest>('/api/im/friend-requests/' + encodeURIComponent(requestId) + '/accept', this.buildAppBody())
}
async rejectFriendRequest(requestId: string): Promise<FriendRequest> {
return HttpClient.post<FriendRequest>('/api/im/friend-requests/' + encodeURIComponent(requestId) + '/reject', this.buildAppBody())
}
async sendGroupJoinRequest(groupId: string, remark: string | null = null): Promise<GroupJoinRequest> {
const params = new GroupJoinRequestBody()
params.appId = SDKContext.getConfig().appKey
if (remark !== null && remark !== '') {
params.remark = remark
}
return HttpClient.post<GroupJoinRequest>('/api/im/groups/' + encodeURIComponent(groupId) + '/join-requests', params)
}
async listGroupJoinRequests(groupId: string): Promise<GroupJoinRequest[]> {
return HttpClient.get<GroupJoinRequest[]>('/api/im/groups/' + encodeURIComponent(groupId) + '/join-requests', this.buildAppQuery())
}
async acceptGroupJoinRequest(groupId: string, requestId: string): Promise<GroupJoinRequest> {
return HttpClient.post<GroupJoinRequest>(
'/api/im/groups/' + encodeURIComponent(groupId) + '/join-requests/' + encodeURIComponent(requestId) + '/accept',
this.buildAppBody(),
)
}
async rejectGroupJoinRequest(groupId: string, requestId: string): Promise<GroupJoinRequest> {
return HttpClient.post<GroupJoinRequest>(
'/api/im/groups/' + encodeURIComponent(groupId) + '/join-requests/' + encodeURIComponent(requestId) + '/reject',
this.buildAppBody(),
)
}
async getProfile(userId: string): Promise<UserProfile> {
return HttpClient.get<UserProfile>('/api/im/accounts/' + encodeURIComponent(userId), this.buildAppQuery())
}
async updateProfile(
userId: string,
nickname: string | null = null,
avatar: string | null = null,
gender: string | null = null,
): Promise<UserProfile> {
const params = new UpdateProfileBody()
params.appId = SDKContext.getConfig().appKey
if (nickname !== null) {
params.nickname = nickname
}
if (avatar !== null) {
params.avatar = avatar
}
if (gender !== null) {
params.gender = gender
}
return HttpClient.put<UserProfile>('/api/im/accounts/' + encodeURIComponent(userId), params)
}
/* ---------- 会话扩展 ---------- */
async setConversationHidden(targetId: string, hidden: boolean): Promise<void> {
const params = new AppBody()
params.appId = SDKContext.getConfig().appKey
await HttpClient.put<void>('/api/im/conversations/' + encodeURIComponent(targetId) + '/hidden', params)
}
async setConversationGroup(targetId: string, groupName: string): Promise<void> {
const params = { appId: SDKContext.getConfig().appKey, groupName }
await HttpClient.put<void>('/api/im/conversations/' + encodeURIComponent(targetId) + '/group', params)
}
async listConversationGroups(): Promise<string[]> {
return HttpClient.get<string[]>('/api/im/conversation-groups', this.buildAppQuery())
}
async listConversationGroupItems(groupName: string): Promise<ConversationData[]> {
return HttpClient.get<ConversationData[]>('/api/im/conversation-groups/' + encodeURIComponent(groupName), this.buildAppQuery())
}
/* ---------- 好友扩展 ---------- */
async addFriend(friendId: string): Promise<void> {
const params = { appId: SDKContext.getConfig().appKey, friendId }
await HttpClient.post<void>('/api/im/friends', params)
}
async removeFriend(friendId: string): Promise<void> {
await HttpClient.delete<void>('/api/im/friends/' + encodeURIComponent(friendId), this.buildAppQuery())
}
async removeAllFriends(): Promise<void> {
await HttpClient.delete<void>('/api/im/friends', this.buildAppQuery())
}
async setFriendGroup(friendId: string, groupName: string): Promise<void> {
const params = { appId: SDKContext.getConfig().appKey, groupName }
await HttpClient.put<void>('/api/im/friends/' + encodeURIComponent(friendId) + '/group', params)
}
async listFriendGroups(): Promise<string[]> {
return HttpClient.get<string[]>('/api/im/friends/groups', this.buildAppQuery())
}
async listFriendsByGroup(groupName: string): Promise<string[]> {
return HttpClient.get<string[]>('/api/im/friends/groups/' + encodeURIComponent(groupName), this.buildAppQuery())
}
async checkFriends(friendIds: string[]): Promise<Record<string, boolean>> {
const params = { appId: SDKContext.getConfig().appKey, friendIds }
return HttpClient.post<Record<string, boolean>>('/api/im/friends/check', params)
}
/* ---------- 黑名单 ---------- */
async listBlacklist(): Promise<string[]> {
return HttpClient.get<string[]>('/api/im/blacklist', this.buildAppQuery())
}
async addToBlacklist(userId: string): Promise<void> {
const params = { appId: SDKContext.getConfig().appKey, userId }
await HttpClient.post<void>('/api/im/blacklist', params)
}
async removeFromBlacklist(userId: string): Promise<void> {
await HttpClient.delete<void>('/api/im/blacklist', this.buildAppQuery() + '&userId=' + encodeURIComponent(userId))
}
async checkBlacklist(userId: string): Promise<{ meBlocked: boolean; theyBlocked: boolean; eitherBlocked: boolean }> {
return HttpClient.get<{ meBlocked: boolean; theyBlocked: boolean; eitherBlocked: boolean }>(
'/api/im/blacklist/check',
this.buildAppQuery() + '&userId=' + encodeURIComponent(userId),
)
}
/* ---------- 群组扩展 ---------- */
async createGroup(name: string, memberIds: string[], groupType: string = 'WORK'): Promise<ImGroup> {
const params = { appId: SDKContext.getConfig().appKey, name, memberIds, groupType }
return HttpClient.post<ImGroup>('/api/im/groups', params)
}
async updateGroupInfo(groupId: string, name?: string, announcement?: string): Promise<ImGroup> {
const params: Record<string, string> = { appId: SDKContext.getConfig().appKey }
if (name !== undefined) params.name = name
if (announcement !== undefined) params.announcement = announcement
return HttpClient.put<ImGroup>('/api/im/groups/' + encodeURIComponent(groupId), params)
}
async addGroupMember(groupId: string, userId: string): Promise<void> {
const params = { appId: SDKContext.getConfig().appKey, userId }
await HttpClient.post<void>('/api/im/groups/' + encodeURIComponent(groupId) + '/members', params)
}
async removeGroupMember(groupId: string, userId: string): Promise<void> {
await HttpClient.delete<void>('/api/im/groups/' + encodeURIComponent(groupId) + '/members/' + encodeURIComponent(userId), this.buildAppQuery())
}
async leaveGroup(groupId: string): Promise<void> {
await HttpClient.delete<void>('/api/im/groups/' + encodeURIComponent(groupId) + '/members/me', this.buildAppQuery())
}
async setGroupRole(groupId: string, userId: string, role: 'ADMIN' | 'MEMBER'): Promise<void> {
const params = { appId: SDKContext.getConfig().appKey, userId, role }
await HttpClient.post<void>('/api/im/groups/' + encodeURIComponent(groupId) + '/roles', params)
}
async muteGroupMember(groupId: string, userId: string, muted: boolean): Promise<void> {
const params = { appId: SDKContext.getConfig().appKey, userId, muted }
await HttpClient.post<void>('/api/im/groups/' + encodeURIComponent(groupId) + '/mute', params)
}
async transferGroupOwner(groupId: string, newOwnerId: string): Promise<ImGroup> {
const params = { appId: SDKContext.getConfig().appKey, newOwnerId }
return HttpClient.post<ImGroup>('/api/im/groups/' + encodeURIComponent(groupId) + '/owner', params)
}
async updateGroupAttributes(groupId: string, attributes: Record<string, string | number | boolean | null>): Promise<void> {
const params = { appId: SDKContext.getConfig().appKey, attributes }
await HttpClient.put<void>('/api/im/groups/' + encodeURIComponent(groupId) + '/attributes', params)
}
async removeGroupAttributes(groupId: string, keys: string[]): Promise<void> {
const params = { appId: SDKContext.getConfig().appKey, keys }
await HttpClient.post<void>('/api/im/groups/' + encodeURIComponent(groupId) + '/attributes/delete', params)
}
async dismissGroup(groupId: string): Promise<void> {
await HttpClient.delete<void>('/api/im/groups/' + encodeURIComponent(groupId), this.buildAppQuery())
}
/* ---------- 批量操作 ---------- */
async batchAddFriends(friendIds: string[]): Promise<void> {
const params = { appId: SDKContext.getConfig().appKey, friendIds }
await HttpClient.post<void>('/api/im/friends/batch', params)
}
async batchRemoveFriends(friendIds: string[]): Promise<void> {
const params = { appId: SDKContext.getConfig().appKey, friendIds }
await HttpClient.post<void>('/api/im/friends/batch/remove', params)
}
async batchAcceptFriendRequests(requestIds: string[]): Promise<void> {
const params = { appId: SDKContext.getConfig().appKey, requestIds }
await HttpClient.post<void>('/api/im/friend-requests/batch/accept', params)
}
async batchRejectFriendRequests(requestIds: string[]): Promise<void> {
const params = { appId: SDKContext.getConfig().appKey, requestIds }
await HttpClient.post<void>('/api/im/friend-requests/batch/reject', params)
}
async batchAddMembers(groupId: string, userIds: string[]): Promise<void> {
const params = { appId: SDKContext.getConfig().appKey, userIds }
await HttpClient.post<void>('/api/im/groups/' + encodeURIComponent(groupId) + '/members/batch', params)
}
async batchRemoveMembers(groupId: string, userIds: string[]): Promise<void> {
const params = { appId: SDKContext.getConfig().appKey, userIds }
await HttpClient.post<void>('/api/im/groups/' + encodeURIComponent(groupId) + '/members/batch/remove', params)
}
async batchAcceptJoinRequests(groupId: string, requestIds: string[]): Promise<void> {
const params = { appId: SDKContext.getConfig().appKey, requestIds }
await HttpClient.post<void>('/api/im/groups/' + encodeURIComponent(groupId) + '/join-requests/batch/accept', params)
}
async batchRejectJoinRequests(groupId: string, requestIds: string[]): Promise<void> {
const params = { appId: SDKContext.getConfig().appKey, requestIds }
await HttpClient.post<void>('/api/im/groups/' + encodeURIComponent(groupId) + '/join-requests/batch/reject', params)
}
async modifyMemberInfo(groupId: string, userId: string, nickname?: string, role?: string): Promise<void> {
const params: Record<string, string> = { appId: SDKContext.getConfig().appKey }
if (nickname !== undefined) params.nickname = nickname
if (role !== undefined) params.role = role
await HttpClient.put<void>('/api/im/groups/' + encodeURIComponent(groupId) + '/members/' + encodeURIComponent(userId) + '/info', params)
}
private buildAppBody(): AppBody {
const body = new AppBody()
body.appId = SDKContext.getConfig().appKey
return body
}
private buildAppQuery(): string {
return 'appId=' + encodeURIComponent(SDKContext.getConfig().appKey)
}
private buildConversationActionQuery(chatType: ChatType): string {
return 'appId=' + encodeURIComponent(SDKContext.getConfig().appKey) + '&chatType=' + encodeURIComponent(chatType)
}
private buildConversationQuery(size: number): string {
return 'appId=' + encodeURIComponent(SDKContext.getConfig().appKey) + '&page=0&size=' + encodeURIComponent(size)
}
private buildFriendRequestQuery(direction: 'incoming' | 'outgoing'): string {
return 'appId=' + encodeURIComponent(SDKContext.getConfig().appKey) + '&direction=' + encodeURIComponent(direction)
}
private buildSearchQuery(keyword: string, size: number): string {
return 'appId=' + encodeURIComponent(SDKContext.getConfig().appKey) +
'&keyword=' + encodeURIComponent(keyword) +
'&size=' + encodeURIComponent(size)
}
private buildMessageSearchQuery(
keyword: string,
chatType: ChatType | '',
msgType: MsgType | '',
page: number,
size: number,
): string {
const parts: string[] = []
parts.push('appId=' + encodeURIComponent(SDKContext.getConfig().appKey))
if (keyword !== '') {
parts.push('keyword=' + encodeURIComponent(keyword))
}
if (chatType !== '') {
parts.push('chatType=' + encodeURIComponent(chatType))
}
if (msgType !== '') {
parts.push('msgType=' + encodeURIComponent(msgType))
}
parts.push('page=' + encodeURIComponent(page))
parts.push('size=' + encodeURIComponent(size))
return parts.join('&')
}
private buildHistoryQuery(page: number, size: number, query: HistoryQuery): string {
const parts: string[] = []
parts.push('appId=' + encodeURIComponent(SDKContext.getConfig().appKey))
parts.push('page=' + encodeURIComponent(page))
parts.push('size=' + encodeURIComponent(size))
if (query.msgType !== undefined) {
parts.push('msgType=' + encodeURIComponent(query.msgType))
}
if (query.keyword !== undefined && query.keyword !== '') {
parts.push('keyword=' + encodeURIComponent(query.keyword))
}
if (query.startTime !== undefined) {
const startValue = query.startTime instanceof Date ? this.formatDateTime(query.startTime) : String(query.startTime)
parts.push('startTime=' + encodeURIComponent(startValue))
}
if (query.endTime !== undefined) {
const endValue = query.endTime instanceof Date ? this.formatDateTime(query.endTime) : String(query.endTime)
parts.push('endTime=' + encodeURIComponent(endValue))
}
return parts.join('&')
}
async sendImageMessage(toId: string, chatType: ChatType, filePath: string, width?: number, height?: number): Promise<ImMessage> {
const uploadResult = await this.uploadFile(filePath)
const url = (uploadResult as Record<string, string>)['url'] ?? ''
const thumbnailUrl = (uploadResult as Record<string, string>)['thumbnailUrl']
const contentObj: Record<string, unknown> = { url }
if (width !== undefined) contentObj.width = width
if (height !== undefined) contentObj.height = height
if (thumbnailUrl) contentObj.thumbnailUrl = thumbnailUrl
return this.send({ toId, chatType, msgType: 'IMAGE', content: JSON.stringify(contentObj) })
}
async sendVideoMessage(toId: string, chatType: ChatType, filePath: string, width?: number, height?: number, duration?: number): Promise<ImMessage> {
const uploadResult = await this.uploadFile(filePath)
const url = (uploadResult as Record<string, string>)['url'] ?? ''
const thumbnailUrl = (uploadResult as Record<string, string>)['thumbnailUrl']
const contentObj: Record<string, unknown> = { url }
if (width !== undefined) contentObj.width = width
if (height !== undefined) contentObj.height = height
if (duration !== undefined) contentObj.duration = duration
if (thumbnailUrl) contentObj.thumbnailUrl = thumbnailUrl
return this.send({ toId, chatType, msgType: 'VIDEO', content: JSON.stringify(contentObj) })
}
async sendFileMessage(toId: string, chatType: ChatType, filePath: string): Promise<ImMessage> {
const uploadResult = await this.uploadFile(filePath)
const url = (uploadResult as Record<string, string>)['url'] ?? ''
const name = (uploadResult as Record<string, string>)['originalName'] ?? ''
const size = (uploadResult as Record<string, number>)['size'] ?? 0
const contentObj: Record<string, unknown> = { url, name, size }
return this.send({ toId, chatType, msgType: 'FILE', content: JSON.stringify(contentObj) })
}
async sendAudioMessage(toId: string, chatType: ChatType, filePath: string, duration?: number): Promise<ImMessage> {
const uploadResult = await this.uploadFile(filePath)
const url = (uploadResult as Record<string, string>)['url'] ?? ''
const contentObj: Record<string, unknown> = { url }
if (duration !== undefined) contentObj.duration = duration
return this.send({ toId, chatType, msgType: 'AUDIO', content: JSON.stringify(contentObj) })
}
private async uploadFile(filePath: string): Promise<Object> {
const token = SDKContext.getToken()
const url = DEFAULT_API_BASE_URL.replace(/\/$/, '') + '/api/file/upload'
const client = http.createHttp()
try {
const header: Record<string, string> = {}
if (token) header.Authorization = 'Bearer ' + token
const multipartForm = new http.MultipartForm()
multipartForm.addFile('file', filePath, filePath.substring(filePath.lastIndexOf('/') + 1))
const options: http.HttpRequestOptions = {
method: http.RequestMethod.POST,
header,
extraData: multipartForm,
connectTimeout: 60000,
readTimeout: 60000,
}
const res = await client.request(url, options)
const json = JSON.parse(res.result as string) as { code: number; message: string; data: Object }
if (json.code !== 200) throw new Error(json.message)
return json.data
} finally {
client.destroy()
}
}
private async notifyConversations(): Promise<void> {
try {
const conversations = await this.listConversations()
this.delegate?.onConversationsChange?.(conversations)
} catch {
// ignore
}
}
disconnect(): void {
this.destroyed = true
if (this.reconnectTimer !== null) {
clearTimeout(this.reconnectTimer)
this.reconnectTimer = null
}
this.ws?.close((_err: Error) => {})
this.ws = null
}
private scheduleReconnect(): void {
if (this.destroyed) return
const delay = this.reconnectDelay
if (SDKContext.getConfig().debug) console.log('[ImClient] reconnect in ' + delay + 'ms')
this.reconnectTimer = setTimeout(() => {
this.connect()
this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY)
}, delay)
}
private buildOutgoingMessage(params: SendMessageParams): ImMessage {
const messageId = params.messageId ?? this.generateMessageId()
const userId = SDKContext.getUserId() ?? ''
const appId = SDKContext.getConfig().appKey
const message: ImMessage = {
id: messageId,
appId,
fromUserId: userId,
fromId: userId,
toId: params.toId,
chatType: params.chatType,
msgType: params.msgType,
content: params.content,
status: 'SENDING',
mentionedUserIds: params.mentionedUserIds,
groupReadCount: 0,
revoked: false,
createdAt: Date.now(),
}
return message
}
private markFailed(message: ImMessage): ImMessage {
const failed: ImMessage = {
id: message.id,
appId: message.appId,
fromUserId: message.fromUserId,
fromId: message.fromId,
toId: message.toId,
chatType: message.chatType,
msgType: message.msgType,
content: message.content,
status: 'FAILED',
mentionedUserIds: message.mentionedUserIds,
groupReadCount: message.groupReadCount,
revoked: message.revoked,
createdAt: message.createdAt,
}
return failed
}
private normalizeMessage(message: ImMessage): ImMessage {
const normalized: ImMessage = {
id: message.id,
appId: message.appId || SDKContext.getConfig().appKey,
fromUserId: message.fromUserId,
fromId: message.fromId || message.fromUserId,
toId: message.toId,
chatType: message.chatType,
msgType: message.msgType,
content: message.content,
status: message.status,
mentionedUserIds: message.mentionedUserIds,
groupReadCount: message.groupReadCount,
revoked: message.revoked || message.status === 'REVOKED',
createdAt: message.createdAt,
}
return normalized
}
private generateMessageId(): string {
return 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000000).toString(16)
}
private formatDateTime(value: Date): string {
const pad = (n: number): string => {
return n < 10 ? '0' + n : String(n)
}
return [
value.getFullYear(),
'-',
pad(value.getMonth() + 1),
'-',
pad(value.getDate()),
'T',
pad(value.getHours()),
':',
pad(value.getMinutes()),
':',
pad(value.getSeconds()),
].join('')
}
private toStringValue(value: Date | string | number | undefined): string | undefined {
if (value === undefined) {
return undefined
}
if (value instanceof Date) {
return this.formatDateTime(value)
}
return String(value)
}
}