diff --git a/xuqm-sdk/Index.ets b/xuqm-sdk/Index.ets index 99e351d..7b22cf7 100644 --- a/xuqm-sdk/Index.ets +++ b/xuqm-sdk/Index.ets @@ -13,6 +13,11 @@ export type { PushTokenInfo, MsgType, ChatType, + MsgStatus, + ConversationData, + HistoryQuery, + PageResult, + UserProfile, ApiResponse, } from './src/main/ets/core/Types' export type { ImEventDelegate, RevokeData } from './src/main/ets/im/ImClient' diff --git a/xuqm-sdk/src/main/ets/XuqmSDK.ets b/xuqm-sdk/src/main/ets/XuqmSDK.ets index a9010e9..e47779d 100644 --- a/xuqm-sdk/src/main/ets/XuqmSDK.ets +++ b/xuqm-sdk/src/main/ets/XuqmSDK.ets @@ -21,6 +21,14 @@ export class XuqmSDK { return SDKContext.getToken() } + static setUserId(userId: string | null): void { + SDKContext.setUserId(userId) + } + + static getUserId(): string | null { + return SDKContext.getUserId() + } + static get im(): ImClient { if (!XuqmSDK._imClient) { XuqmSDK._imClient = new ImClient() diff --git a/xuqm-sdk/src/main/ets/core/HttpClient.ets b/xuqm-sdk/src/main/ets/core/HttpClient.ets index 60e3409..ec3b297 100644 --- a/xuqm-sdk/src/main/ets/core/HttpClient.ets +++ b/xuqm-sdk/src/main/ets/core/HttpClient.ets @@ -3,10 +3,23 @@ import type { ApiResponse } from './Types' import { SDKContext } from './SDKContext' export class HttpClient { - static async request(method: http.RequestMethod, path: string, body?: object): Promise { + static async request( + method: http.RequestMethod, + path: string, + body?: object, + query?: Record, + ): Promise { const config = SDKContext.getConfig() const token = SDKContext.getToken() - const url = config.apiBaseUrl.replace(/\/$/, '') + path + const queryPairs: string[] = [] + if (query) { + for (const key of Object.keys(query)) { + const value = query[key] + if (value === undefined || value === null || value === '') continue + queryPairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value instanceof Date ? value.toISOString() : String(value))}`) + } + } + const url = config.apiBaseUrl.replace(/\/$/, '') + path + (queryPairs.length > 0 ? `?${queryPairs.join('&')}` : '') const client = http.createHttp() try { @@ -30,19 +43,19 @@ export class HttpClient { } } - static get(path: string): Promise { - return HttpClient.request(http.RequestMethod.GET, path) + static get(path: string, query?: Record): Promise { + return HttpClient.request(http.RequestMethod.GET, path, undefined, query) } - static post(path: string, body?: object): Promise { - return HttpClient.request(http.RequestMethod.POST, path, body) + static post(path: string, body?: object, query?: Record): Promise { + return HttpClient.request(http.RequestMethod.POST, path, body, query) } - static put(path: string, body?: object): Promise { - return HttpClient.request(http.RequestMethod.PUT, path, body) + static put(path: string, body?: object, query?: Record): Promise { + return HttpClient.request(http.RequestMethod.PUT, path, body, query) } - static delete(path: string): Promise { - return HttpClient.request(http.RequestMethod.DELETE, path) + static delete(path: string, query?: Record): Promise { + return HttpClient.request(http.RequestMethod.DELETE, path, undefined, query) } } diff --git a/xuqm-sdk/src/main/ets/core/SDKContext.ets b/xuqm-sdk/src/main/ets/core/SDKContext.ets index 499d9a2..7b2181c 100644 --- a/xuqm-sdk/src/main/ets/core/SDKContext.ets +++ b/xuqm-sdk/src/main/ets/core/SDKContext.ets @@ -7,6 +7,7 @@ const PREF_NAME = 'xuqm_sdk_prefs' export class SDKContext { private static _config: SDKConfig | null = null private static _token: string | null = null + private static _userId: string | null = null private static _pref: preferences.Preferences | null = null static init(config: SDKConfig): void { @@ -44,4 +45,12 @@ export class SDKContext { static getToken(): string | null { return SDKContext._token } + + static setUserId(userId: string | null): void { + SDKContext._userId = userId + } + + static getUserId(): string | null { + return SDKContext._userId + } } diff --git a/xuqm-sdk/src/main/ets/core/Types.ets b/xuqm-sdk/src/main/ets/core/Types.ets index 4072b71..12f5d2f 100644 --- a/xuqm-sdk/src/main/ets/core/Types.ets +++ b/xuqm-sdk/src/main/ets/core/Types.ets @@ -25,29 +25,81 @@ export type MsgType = | 'RICH_TEXT' | 'CALL_AUDIO' | 'CALL_VIDEO' + | 'QUOTE' + | 'MERGE' | 'REVOKED' | 'FORWARD' export type ChatType = 'SINGLE' | 'GROUP' +export type MsgStatus = 'SENDING' | 'SENT' | 'DELIVERED' | 'READ' | 'FAILED' | 'REVOKED' + export interface ImMessage { id: string + appId: string + fromUserId: string fromId: string toId: string chatType: ChatType msgType: MsgType content: string - extra?: string - revoked: boolean - createdAt: string + status: MsgStatus + mentionedUserIds?: string + groupReadCount?: number + revoked?: boolean + createdAt: number +} + +export interface ConversationData { + targetId: string + chatType: ChatType + lastMsgContent?: string | null + lastMsgType?: string | null + lastMsgTime: number + unreadCount: number + isMuted: boolean + isPinned: boolean +} + +export interface HistoryQuery { + msgType?: MsgType + keyword?: string + startTime?: Date | string | number + endTime?: Date | string | number + page?: number + size?: number +} + +export interface PageResult { + content: T[] + totalElements: number + totalPages: number + size: number + number: number + numberOfElements: number + first: boolean + last: boolean + empty: boolean +} + +export interface UserProfile { + id?: string + appId?: string + userId: string + nickname?: string | null + avatar?: string | null + gender?: string | null + status?: string | null + createdAt?: number | null } export interface SendMessageParams { + messageId?: string toId: string chatType: ChatType msgType: MsgType content: string - extra?: string + mentionedUserIds?: string } export interface AppVersionInfo { diff --git a/xuqm-sdk/src/main/ets/im/ImClient.ets b/xuqm-sdk/src/main/ets/im/ImClient.ets index 76899f9..532481a 100644 --- a/xuqm-sdk/src/main/ets/im/ImClient.ets +++ b/xuqm-sdk/src/main/ets/im/ImClient.ets @@ -1,6 +1,7 @@ import webSocket from '@ohos.net.webSocket' -import type { ImMessage, SendMessageParams } from '../core/Types' +import { HttpClient } from '../core/HttpClient' import { SDKContext } from '../core/SDKContext' +import type { ChatType, ConversationData, HistoryQuery, ImMessage, MsgType, PageResult, SendMessageParams, UserProfile } from '../core/Types' export interface ImEventDelegate { onConnected?(): void @@ -41,9 +42,9 @@ export class ImClient { 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: object } + const frame = JSON.parse(text) as { type: string; payload: unknown } if (frame.type === 'MESSAGE') { - this.delegate?.onMessage?.(frame.payload as ImMessage) + this.delegate?.onMessage?.(this.normalizeMessage(frame.payload as ImMessage)) } else if (frame.type === 'REVOKE') { this.delegate?.onRevoke?.(frame.payload as RevokeData) } @@ -64,19 +65,133 @@ export class ImClient { this.ws.connect(url, {}) } - send(params: SendMessageParams): void { - if (!this.ws) throw new Error('WebSocket not connected') - const frame = JSON.stringify({ destination: '/app/chat.send', payload: params }) - this.ws.send(frame, (_err: Error) => { - if (_err) console.error('[ImClient] send error', _err.message) - }) + 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 } revoke(msgId: string): void { - if (!this.ws) throw new Error('WebSocket not connected') - const frame = JSON.stringify({ destination: '/app/chat.revoke', payload: { msgId } }) - this.ws.send(frame, (_err: Error) => { - if (_err) console.error('[ImClient] revoke error', _err.message) + 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> { + return HttpClient.get>(`/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> { + return HttpClient.get>(`/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 { + return HttpClient.get('/api/im/conversations', { + appId: SDKContext.getConfig().appKey, + page: 0, + size, + }) + } + + async markRead(targetId: string, chatType: ChatType = 'SINGLE'): Promise { + await HttpClient.put(`/api/im/conversations/${encodeURIComponent(targetId)}/read`, undefined, { + appId: SDKContext.getConfig().appKey, + chatType, + }) + } + + async setDraft(targetId: string, chatType: ChatType, draft: string): Promise { + await HttpClient.put(`/api/im/conversations/${encodeURIComponent(targetId)}/draft`, undefined, { + appId: SDKContext.getConfig().appKey, + chatType, + draft, + }) + } + + async deleteConversation(targetId: string, chatType: ChatType): Promise { + await HttpClient.delete(`/api/im/conversations/${encodeURIComponent(targetId)}`, { + appId: SDKContext.getConfig().appKey, + chatType, + }) + } + + async getProfile(userId: string): Promise { + return HttpClient.get(`/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 { + return HttpClient.put(`/api/im/accounts/${encodeURIComponent(userId)}`, undefined, { + appId: SDKContext.getConfig().appKey, + ...(nickname !== null ? { nickname } : {}), + ...(avatar !== null ? { avatar } : {}), + ...(gender !== null ? { gender } : {}), }) } @@ -99,4 +214,57 @@ export class ImClient { 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('') + } }