2026-04-21 22:07:29 +08:00
|
|
|
import webSocket from '@ohos.net.webSocket'
|
2026-04-28 16:55:11 +08:00
|
|
|
import { HttpClient } from '../core/HttpClient'
|
2026-04-21 22:07:29 +08:00
|
|
|
import { SDKContext } from '../core/SDKContext'
|
2026-04-29 15:46:39 +08:00
|
|
|
import { DEFAULT_IM_WS_URL } from '../core/Endpoints'
|
2026-04-28 20:11:37 +08:00
|
|
|
import type {
|
|
|
|
|
ChatType,
|
|
|
|
|
ConversationData,
|
|
|
|
|
FriendRequest,
|
|
|
|
|
GroupJoinRequest,
|
|
|
|
|
HistoryQuery,
|
|
|
|
|
ImGroup,
|
|
|
|
|
ImMessage,
|
|
|
|
|
MsgType,
|
|
|
|
|
PageResult,
|
|
|
|
|
SendMessageParams,
|
|
|
|
|
UserProfile,
|
|
|
|
|
} from '../core/Types'
|
2026-04-21 22:07:29 +08:00
|
|
|
|
|
|
|
|
export interface ImEventDelegate {
|
|
|
|
|
onConnected?(): void
|
|
|
|
|
onDisconnected?(code: number, reason: string): void
|
|
|
|
|
onMessage?(msg: ImMessage): void
|
2026-04-28 22:32:20 +08:00
|
|
|
onRead?(msg: ImMessage): void
|
2026-04-21 22:07:29 +08:00
|
|
|
onRevoke?(data: RevokeData): void
|
|
|
|
|
onError?(message: string): void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface RevokeData {
|
|
|
|
|
msgId: string
|
|
|
|
|
operatorId: string
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 20:11:37 +08:00
|
|
|
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 = ''
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 22:32:20 +08:00
|
|
|
class EditMessageBody {
|
|
|
|
|
content: string = ''
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 20:11:37 +08:00
|
|
|
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 = ''
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 22:07:29 +08:00
|
|
|
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() ?? ''
|
2026-04-29 15:46:39 +08:00
|
|
|
const url = DEFAULT_IM_WS_URL.replace(/\/$/, '') + '?token=' + encodeURIComponent(token)
|
2026-04-21 22:07:29 +08:00
|
|
|
|
|
|
|
|
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 {
|
2026-04-28 20:11:37 +08:00
|
|
|
if (typeof value !== 'string') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const frame = JSON.parse(value) as WebSocketFrame
|
2026-04-21 22:07:29 +08:00
|
|
|
if (frame.type === 'MESSAGE') {
|
2026-04-28 22:32:20 +08:00
|
|
|
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)
|
2026-04-21 22:07:29 +08:00
|
|
|
} else if (frame.type === 'REVOKE') {
|
|
|
|
|
this.delegate?.onRevoke?.(frame.payload as RevokeData)
|
|
|
|
|
}
|
|
|
|
|
} 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, {})
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 16:55:11 +08:00
|
|
|
send(params: SendMessageParams): ImMessage {
|
|
|
|
|
const outgoing = this.buildOutgoingMessage(params)
|
|
|
|
|
if (!this.ws) {
|
2026-04-28 20:11:37 +08:00
|
|
|
return this.markFailed(outgoing)
|
2026-04-28 16:55:11 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-28 20:11:37 +08:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
})
|
2026-04-28 16:55:11 +08:00
|
|
|
return outgoing
|
2026-04-21 22:07:29 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
revoke(msgId: string): void {
|
2026-04-28 16:55:11 +08:00
|
|
|
if (!this.ws) {
|
|
|
|
|
throw new Error('WebSocket not connected')
|
|
|
|
|
}
|
2026-04-28 20:11:37 +08:00
|
|
|
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)
|
|
|
|
|
})
|
2026-04-28 16:55:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fetchHistory(
|
|
|
|
|
toId: string,
|
|
|
|
|
page: number = 0,
|
|
|
|
|
size: number = 20,
|
|
|
|
|
query: HistoryQuery = {},
|
|
|
|
|
): Promise<PageResult<ImMessage>> {
|
2026-04-28 20:11:37 +08:00
|
|
|
const queryString = this.buildHistoryQuery(page, size, query)
|
|
|
|
|
return HttpClient.get<PageResult<ImMessage>>('/api/im/messages/history/' + encodeURIComponent(toId), queryString)
|
2026-04-28 16:55:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fetchGroupHistory(
|
|
|
|
|
groupId: string,
|
|
|
|
|
page: number = 0,
|
|
|
|
|
size: number = 50,
|
|
|
|
|
query: HistoryQuery = {},
|
|
|
|
|
): Promise<PageResult<ImMessage>> {
|
2026-04-28 20:11:37 +08:00
|
|
|
const queryString = this.buildHistoryQuery(page, size, query)
|
|
|
|
|
return HttpClient.get<PageResult<ImMessage>>('/api/im/messages/group-history/' + encodeURIComponent(groupId), queryString)
|
2026-04-28 16:55:11 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-28 22:32:20 +08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 16:55:11 +08:00
|
|
|
async listConversations(size: number = 20): Promise<ConversationData[]> {
|
2026-04-28 20:11:37 +08:00
|
|
|
return HttpClient.get<ConversationData[]>('/api/im/conversations', this.buildConversationQuery(size))
|
2026-04-28 16:55:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async markRead(targetId: string, chatType: ChatType = 'SINGLE'): Promise<void> {
|
2026-04-28 20:11:37 +08:00
|
|
|
await HttpClient.put<void>('/api/im/conversations/' + encodeURIComponent(targetId) + '/read', undefined, this.buildConversationActionQuery(chatType))
|
2026-04-28 16:55:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async setDraft(targetId: string, chatType: ChatType, draft: string): Promise<void> {
|
2026-04-28 20:11:37 +08:00
|
|
|
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)
|
2026-04-28 16:55:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async deleteConversation(targetId: string, chatType: ChatType): Promise<void> {
|
2026-04-28 20:11:37 +08:00
|
|
|
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())
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 00:37:18 +08:00
|
|
|
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),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:05:06 +08:00
|
|
|
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),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 22:32:20 +08:00
|
|
|
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(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 00:37:18 +08:00
|
|
|
async revokeMessage(messageId: string): Promise<ImMessage> {
|
|
|
|
|
return HttpClient.post<ImMessage>(
|
|
|
|
|
'/api/im/messages/' + encodeURIComponent(messageId) + '/revoke',
|
|
|
|
|
this.buildAppBody(),
|
|
|
|
|
this.buildAppQuery(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 20:11:37 +08:00
|
|
|
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(),
|
|
|
|
|
)
|
2026-04-28 16:55:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getProfile(userId: string): Promise<UserProfile> {
|
2026-04-28 20:11:37 +08:00
|
|
|
return HttpClient.get<UserProfile>('/api/im/accounts/' + encodeURIComponent(userId), this.buildAppQuery())
|
2026-04-28 16:55:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateProfile(
|
|
|
|
|
userId: string,
|
|
|
|
|
nickname: string | null = null,
|
|
|
|
|
avatar: string | null = null,
|
|
|
|
|
gender: string | null = null,
|
|
|
|
|
): Promise<UserProfile> {
|
2026-04-28 20:11:37 +08:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 21:05:06 +08:00
|
|
|
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('&')
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-28 20:11:37 +08:00
|
|
|
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('&')
|
2026-04-21 22:07:29 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2026-04-28 20:11:37 +08:00
|
|
|
if (SDKContext.getConfig().debug) console.log('[ImClient] reconnect in ' + delay + 'ms')
|
2026-04-21 22:07:29 +08:00
|
|
|
this.reconnectTimer = setTimeout(() => {
|
|
|
|
|
this.connect()
|
|
|
|
|
this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY)
|
|
|
|
|
}, delay)
|
|
|
|
|
}
|
2026-04-28 16:55:11 +08:00
|
|
|
|
|
|
|
|
private buildOutgoingMessage(params: SendMessageParams): ImMessage {
|
|
|
|
|
const messageId = params.messageId ?? this.generateMessageId()
|
|
|
|
|
const userId = SDKContext.getUserId() ?? ''
|
|
|
|
|
const appId = SDKContext.getConfig().appKey
|
2026-04-28 20:11:37 +08:00
|
|
|
const message: ImMessage = {
|
2026-04-28 16:55:11 +08:00
|
|
|
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(),
|
|
|
|
|
}
|
2026-04-28 20:11:37 +08:00
|
|
|
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
|
2026-04-28 16:55:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private normalizeMessage(message: ImMessage): ImMessage {
|
2026-04-28 20:11:37 +08:00
|
|
|
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,
|
2026-04-28 16:55:11 +08:00
|
|
|
}
|
2026-04-28 20:11:37 +08:00
|
|
|
return normalized
|
2026-04-28 16:55:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private generateMessageId(): string {
|
2026-04-28 20:11:37 +08:00
|
|
|
return 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000000).toString(16)
|
2026-04-28 16:55:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private formatDateTime(value: Date): string {
|
2026-04-28 20:11:37 +08:00
|
|
|
const pad = (n: number): string => {
|
|
|
|
|
return n < 10 ? '0' + n : String(n)
|
|
|
|
|
}
|
2026-04-28 16:55:11 +08:00
|
|
|
return [
|
|
|
|
|
value.getFullYear(),
|
|
|
|
|
'-',
|
|
|
|
|
pad(value.getMonth() + 1),
|
|
|
|
|
'-',
|
|
|
|
|
pad(value.getDate()),
|
|
|
|
|
'T',
|
|
|
|
|
pad(value.getHours()),
|
|
|
|
|
':',
|
|
|
|
|
pad(value.getMinutes()),
|
|
|
|
|
':',
|
|
|
|
|
pad(value.getSeconds()),
|
|
|
|
|
].join('')
|
|
|
|
|
}
|
2026-04-28 20:11:37 +08:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-04-21 22:07:29 +08:00
|
|
|
}
|