- 实现了聊天消息发送功能,支持文本、图片、视频、音频、文件等多种消息类型 - 集成了文件上传下载功能,支持多媒体文件的传输和管理 - 添加了群组管理功能,包括创建群组、成员管理、权限控制等操作 - 实现了好友系统,支持好友添加、删除、分组等功能 - 集成了黑名单管理,提供用户屏蔽和解除屏蔽功能 - 添加了会话管理功能,支持对话列表、未读消息统计等 - 实现了历史消息查询和搜索功能 - 添加了实时连接状态管理和自动重连机制
899 行
32 KiB
Plaintext
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)
|
|
}
|
|
}
|