XuqmGroup-HarmonySDK/xuqm-sdk/src/main/ets/im/ImClient.ets

271 行
7.9 KiB
Plaintext

2026-04-21 22:07:29 +08:00
import webSocket from '@ohos.net.webSocket'
import { HttpClient } from '../core/HttpClient'
2026-04-21 22:07:29 +08:00
import { SDKContext } from '../core/SDKContext'
import type { ChatType, ConversationData, HistoryQuery, 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
onRevoke?(data: RevokeData): void
onError?(message: string): void
}
export interface RevokeData {
msgId: string
operatorId: 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 = `${config.imBaseUrl}/ws/im?token=${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 {
const text = typeof value === 'string' ? value : new TextDecoder().decode(value)
const frame = JSON.parse(text) as { type: string; payload: unknown }
2026-04-21 22:07:29 +08:00
if (frame.type === 'MESSAGE') {
this.delegate?.onMessage?.(this.normalizeMessage(frame.payload as ImMessage))
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, {})
}
send(params: SendMessageParams): ImMessage {
const outgoing = this.buildOutgoingMessage(params)
if (!this.ws) {
return { ...outgoing, status: 'FAILED' }
}
this.ws.send(
JSON.stringify({
destination: '/app/chat.send',
payload: {
...params,
messageId: outgoing.id,
},
}),
(_err: Error) => {
if (_err) {
this.delegate?.onError?.(_err.message)
}
},
)
return outgoing
2026-04-21 22:07:29 +08:00
}
revoke(msgId: string): void {
if (!this.ws) {
throw new Error('WebSocket not connected')
}
this.ws.send(
JSON.stringify({
destination: '/app/chat.revoke',
payload: { msgId },
}),
(_err: Error) => {
if (_err) this.delegate?.onError?.(_err.message)
},
)
}
async fetchHistory(
toId: string,
page: number = 0,
size: number = 20,
query: HistoryQuery = {},
): Promise<PageResult<ImMessage>> {
return HttpClient.get<PageResult<ImMessage>>(`/api/im/messages/history/${encodeURIComponent(toId)}`, {
appId: SDKContext.getConfig().appKey,
page,
size,
msgType: query.msgType,
keyword: query.keyword,
startTime: query.startTime instanceof Date
? this.formatDateTime(query.startTime)
: query.startTime,
endTime: query.endTime instanceof Date
? this.formatDateTime(query.endTime)
: query.endTime,
})
}
async fetchGroupHistory(
groupId: string,
page: number = 0,
size: number = 50,
query: HistoryQuery = {},
): Promise<PageResult<ImMessage>> {
return HttpClient.get<PageResult<ImMessage>>(`/api/im/messages/group-history/${encodeURIComponent(groupId)}`, {
appId: SDKContext.getConfig().appKey,
page,
size,
msgType: query.msgType,
keyword: query.keyword,
startTime: query.startTime instanceof Date
? this.formatDateTime(query.startTime)
: query.startTime,
endTime: query.endTime instanceof Date
? this.formatDateTime(query.endTime)
: query.endTime,
})
}
async listConversations(size: number = 20): Promise<ConversationData[]> {
return HttpClient.get<ConversationData[]>('/api/im/conversations', {
appId: SDKContext.getConfig().appKey,
page: 0,
size,
})
}
async markRead(targetId: string, chatType: ChatType = 'SINGLE'): Promise<void> {
await HttpClient.put<void>(`/api/im/conversations/${encodeURIComponent(targetId)}/read`, undefined, {
appId: SDKContext.getConfig().appKey,
chatType,
})
}
async setDraft(targetId: string, chatType: ChatType, draft: string): Promise<void> {
await HttpClient.put<void>(`/api/im/conversations/${encodeURIComponent(targetId)}/draft`, undefined, {
appId: SDKContext.getConfig().appKey,
chatType,
draft,
})
}
async deleteConversation(targetId: string, chatType: ChatType): Promise<void> {
await HttpClient.delete<void>(`/api/im/conversations/${encodeURIComponent(targetId)}`, {
appId: SDKContext.getConfig().appKey,
chatType,
})
}
async getProfile(userId: string): Promise<UserProfile> {
return HttpClient.get<UserProfile>(`/api/im/accounts/${encodeURIComponent(userId)}`, {
appId: SDKContext.getConfig().appKey,
})
}
async updateProfile(
userId: string,
nickname: string | null = null,
avatar: string | null = null,
gender: string | null = null,
): Promise<UserProfile> {
return HttpClient.put<UserProfile>(`/api/im/accounts/${encodeURIComponent(userId)}`, undefined, {
appId: SDKContext.getConfig().appKey,
...(nickname !== null ? { nickname } : {}),
...(avatar !== null ? { avatar } : {}),
...(gender !== null ? { gender } : {}),
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
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
return {
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(),
}
}
private normalizeMessage(message: ImMessage): ImMessage {
return {
...message,
fromId: message.fromId ?? message.fromUserId,
revoked: message.revoked ?? message.status === 'REVOKED',
appId: message.appId ?? SDKContext.getConfig().appKey,
}
}
private generateMessageId(): string {
const cryptoId = globalThis.crypto?.randomUUID?.()
if (cryptoId) return cryptoId
return `msg_${Date.now()}_${Math.random().toString(16).slice(2)}`
}
private formatDateTime(value: Date): string {
const pad = (n: number) => String(n).padStart(2, '0')
return [
value.getFullYear(),
'-',
pad(value.getMonth() + 1),
'-',
pad(value.getDate()),
'T',
pad(value.getHours()),
':',
pad(value.getMinutes()),
':',
pad(value.getSeconds()),
].join('')
}
2026-04-21 22:07:29 +08:00
}