UNPKG

wechaty-puppet-padplus

Version:
1,214 lines (1,094 loc) 42.4 kB
import util from 'util' import { DelayQueueExecutor, ThrottleQueue, } from 'rx-queue' import { StateSwitch } from 'state-switch' import { log, GRPC_ENDPOINT, MESSAGE_CACHE_MAX, MESSAGE_CACHE_AGE, WAIT_FOR_READY_TIME, INVALID_TOKEN_MESSAGE, EXPIRED_TOKEN_MESSAGE } from '../config' import { MemoryCard } from 'memory-card' import FileBox from 'file-box' import LRU from 'lru-cache' import fileBoxToQrcode from '../utils/file-box-to-qrcode' import { GrpcGateway } from '../server-manager/grpc-gateway' import { StreamResponse, ResponseType } from '../server-manager/proto-ts/PadPlusServer_pb' import { ScanStatus, ContactGender, FriendshipPayload as PuppetFriendshipPayload } from 'wechaty-puppet' import { RequestClient } from './api-request/request' import { PadplusUser } from './api-request/user' import { PadplusContact } from './api-request/contact' import { PadplusMessage } from './api-request/message' import { GrpcEventEmitter } from '../server-manager/grpc-event-emitter' import { GrpcContactPayload, GrpcRoomPayload, PadplusContactPayload, PadplusError, PadplusErrorType, PadplusMessagePayload, PadplusMessageType, PadplusRoomPayload, ScanData, FriendshipPayload, QrcodeStatus, PadplusRichMediaData, GrpcRoomMemberPayload, GrpcRoomMemberList, PadplusMediaData, PadplusRoomMemberPayload, GrpcSearchContact, GrpcDeleteContact, LogoutGrpcResponse, PadplusRoomMemberMap, GetContactSelfInfoGrpcResponse, } from '../schemas' import { convertMessageFromGrpcToPadplus } from '../convert-manager/message-convertor' import { GrpcMessagePayload, GrpcQrCodeLogin } from '../schemas/grpc-schemas' import { CacheManager } from '../server-manager/cache-manager' import { convertFromGrpcContact, convertFromGrpcContactSelf } from '../convert-manager/contact-convertor' import { PadplusRoom } from './api-request/room' import { convertRoomFromGrpc } from '../convert-manager/room-convertor' import { CallbackPool } from '../utils/callbackHelper' import { PadplusFriendship } from './api-request/friendship' import { briefRoomMemberParser, roomMemberParser } from '../pure-function-helpers/room-member-parser' import { isRoomId, isContactId } from '../pure-function-helpers' import { EventEmitter } from 'events' const MEMORY_SLOT_NAME = 'WECHATY_PUPPET_PADPLUS' export interface PadplusMemorySlot { qrcodeId: string, uin : string, userName : string, } export interface ManagerOptions { token: string, name: unknown, endpoint?: string, } const PRE = 'PadplusManager' export type PadplusManagerEvent = 'error' | 'scan' | 'login' | 'logout' | 'contact-list' | 'contact-modify' | 'contact-delete' | 'message' | 'room-member-list' | 'room-member-modify' | 'status-notify' | 'ready' | 'reset' | 'heartbeat' | 'EXPIRED_TOKEN' | 'INVALID_TOKEN' export class PadplusManager extends EventEmitter { private grpcGatewayEmitter?: GrpcEventEmitter private readonly state : StateSwitch private requestClient? : RequestClient private padplusUser? : PadplusUser private padplusMesasge? : PadplusMessage private padplusContact? : PadplusContact private padplusRoom? : PadplusRoom private padplusFriendship? : PadplusFriendship public cacheManager? : CacheManager private memory? : MemoryCard private memorySlot : PadplusMemorySlot private qrcodeStatus? : ScanStatus private loginStatus? : boolean public readonly cachePadplusMessagePayload: LRU<string, PadplusMessagePayload> private contactAndRoomData? : { contactTotal: number, friendTotal: number, roomTotal: number, updatedTime: number, readyEmitted: boolean, } private resetThrottleQueue : ThrottleQueue private getContactQueue : DelayQueueExecutor private getRoomMemberQueue : DelayQueueExecutor constructor ( public options: ManagerOptions, ) { super() log.verbose(PRE, 'constructor()') const lruOptions: LRU.Options<string, PadplusMessagePayload> = { dispose (key: string, val: any) { log.silly(PRE, `constructor() lruOptions.dispose(${key}, ${JSON.stringify(val)})`) }, max: MESSAGE_CACHE_MAX, maxAge: MESSAGE_CACHE_AGE, } this.loginStatus = false this.cachePadplusMessagePayload = new LRU<string, PadplusMessagePayload>(lruOptions) this.state = new StateSwitch('PadplusManager') this.state.off() this.memorySlot = { qrcodeId: '', uin: '', userName: '', } this.getContactQueue = new DelayQueueExecutor(200) this.getRoomMemberQueue = new DelayQueueExecutor(500) this.resetThrottleQueue = new ThrottleQueue<string>(5000) this.resetThrottleQueue.subscribe(async reason => { log.silly('Puppet', 'constructor() resetThrottleQueue.subscribe() reason: %s', reason) if (this.grpcGatewayEmitter) { this.grpcGatewayEmitter.removeAllListeners() } delete this.padplusUser delete this.padplusContact delete this.padplusFriendship delete this.padplusRoom delete this.padplusMesasge delete this.requestClient await this.start() }) log.silly(PRE, ` : ${util.inspect(this.state)}`) } public emit (event: 'scan', qrcode: string, status: number, data?: string): boolean public emit (event: 'login', data: GrpcQrCodeLogin): boolean public emit (event: 'logout'): boolean public emit (event: 'contact-list', data: string): boolean public emit (event: 'contact-modify', data: string): boolean public emit (event: 'contact-delete', data: string): boolean public emit (event: 'message', msg: PadplusMessagePayload): boolean public emit (event: 'room-member-list', data: string): boolean public emit (event: 'room-member-modify', data: string): boolean public emit (event: 'status-notify', data: string): boolean public emit (event: 'ready'): boolean public emit (event: 'reset', reason: string): boolean public emit (event: 'heartbeat', data: string): boolean public emit (event: 'error', error: Error): boolean public emit (event: never, listener: never): never public emit ( event: PadplusManagerEvent, ...args: any[] ): boolean { return super.emit(event, ...args) } public on (event: 'scan', listener: ((this: PadplusManager, qrcode: string, status: number, data?: string) => void)): this public on (event: 'login', listener: ((this: PadplusManager, data: GrpcQrCodeLogin) => void)): this public on (event: 'logout', listener: ((this: PadplusManager) => void)): this public on (event: 'message', listener: ((this: PadplusManager, msg: PadplusMessagePayload) => void)): this public on (event: 'status-notify', listener: ((this: PadplusManager, data: string) => void)): this public on (event: 'ready', listener: ((this: PadplusManager) => void)): this public on (event: 'reset', listener: ((this: PadplusManager, reason: string) => void)): this public on (event: 'heartbeat', listener: ((this: PadplusManager, data: string) => void)): this public on (event: 'error', listener: ((this: PadplusManager, error: Error) => void)): this public on (event: never, listener: never): never public on (event: PadplusManagerEvent, listener: ((...args: any[]) => any)): this { log.verbose(PRE, `on(${event}, ${typeof listener}) registered`) super.on(event, (...args: any[]) => { try { listener.apply(this, args) } catch (e) { log.error(PRE, 'onFunction(%s) listener exception: %s', event, e) } }) return this } public async start (): Promise<void> { log.silly(PRE, `start()`) this.state.on('pending') let emitter: GrpcEventEmitter | undefined try { emitter = await GrpcGateway.init(this.options.token, this.options.endpoint || GRPC_ENDPOINT, String(this.options.name)) } catch (e) { log.info(`start grpc gateway failed for reason: ${e}, retry start in 5 seconds.`) await new Promise(resolve => setTimeout(resolve, 5000)) await this.start() return } if (!GrpcGateway.Instance) { throw new Error(`The grpc gateway has no instance.`) } this.requestClient = new RequestClient(GrpcGateway.Instance, emitter) this.padplusUser = new PadplusUser(this.requestClient) this.padplusMesasge = new PadplusMessage(this.requestClient) this.padplusContact = new PadplusContact(this.requestClient) this.padplusRoom = new PadplusRoom(this.requestClient) this.padplusFriendship = new PadplusFriendship(this.requestClient) await this.initGrpcGatewayListener(emitter) this.grpcGatewayEmitter = emitter if (this.memory) { const slot = await this.memory.get(MEMORY_SLOT_NAME) if (slot && slot.uin) { log.silly(PRE, `uin : ${slot.uin}`) emitter.setUIN(slot.uin) await new Promise((resolve) => setTimeout(resolve, 500)) await this.padplusUser.initInstance() } else { await this.padplusUser.getWeChatQRCode() } this.memorySlot = { ...this.memorySlot, ...await this.memory.get<PadplusMemorySlot>(MEMORY_SLOT_NAME), } } } public async stop (): Promise<void> { log.verbose(PRE, `stop()`) this.state.off('pending') if (this.grpcGatewayEmitter) { this.grpcGatewayEmitter.removeAllListeners() } await GrpcGateway.release() await CacheManager.release() this.cacheManager = undefined this.loginStatus = false this.state.off(true) log.verbose(PRE, `stop() finished`) } public async logout (selfId: string): Promise<void> { log.verbose(PRE, `logout()`) this.state.off('pending') if (this.padplusUser) { const logoutResult: boolean = await this.padplusUser.logout(selfId) if (!logoutResult) { log.error(`Logout WeChat failed!`) } else { log.silly(PRE, `Logout WeChat success!`) } } else { throw new Error(`no padplus user.`) } if (this.grpcGatewayEmitter) { this.grpcGatewayEmitter.removeAllListeners() } if (GrpcGateway.Instance) { await GrpcGateway.Instance.stop() } await CacheManager.release() this.cacheManager = undefined this.loginStatus = false this.state.off(true) log.verbose(PRE, `logout() finished`) } public setMemory (memory: MemoryCard) { this.memory = memory } public async setContactAndRoomData () { log.silly(PRE, `setContactAndRoomData()`) if (!this.cacheManager) { log.verbose(PRE, `setContactAndRoomData() can not proceed due to no cache.`) return } const contactTotal = await this.cacheManager.getContactCount() const roomTotal = await this.cacheManager.getRoomCount() const friendTotal = (await this.cacheManager.getAllContacts()).filter(contact => { return contact.contactFlag !== 0 }).length const now = new Date().getTime() if (this.contactAndRoomData) { if (this.contactAndRoomData.contactTotal === contactTotal && this.contactAndRoomData.roomTotal === roomTotal && this.contactAndRoomData.friendTotal === friendTotal) { if (now - this.contactAndRoomData.updatedTime > WAIT_FOR_READY_TIME && !this.contactAndRoomData.readyEmitted) { log.verbose(PRE, `setContactAndRoomData() more than ${WAIT_FOR_READY_TIME / 1000 / 60} minutes no change on data, emit ready event.`) this.contactAndRoomData.readyEmitted = true this.emit('ready') } log.silly(PRE, `setContactAndRoomData() found contact, room, friend data no change.`) } else { log.silly(PRE, `setContactAndRoomData() found contact or room or friend change. Record changes...`) this.contactAndRoomData.contactTotal = contactTotal this.contactAndRoomData.roomTotal = roomTotal this.contactAndRoomData.friendTotal = friendTotal this.contactAndRoomData.updatedTime = now } } else { log.silly(PRE, `setContactAndRoomData() initialize contact and room data.`) this.contactAndRoomData = { contactTotal, friendTotal, readyEmitted: false, roomTotal, updatedTime: now, } } } public async initGrpcGatewayListener (grpcGatewayEmitter: GrpcEventEmitter) { grpcGatewayEmitter.on('reconnect', async () => { this.resetThrottleQueue.next('reconnect') }) grpcGatewayEmitter.on('grpc-end', async () => { this.emit('reset', 'grpc server end.') }) grpcGatewayEmitter.on('heartbeat', async (data: any) => { this.emit('heartbeat', data) // TODO: 数据同步后,需要停止该函数的执行 if (!this.contactAndRoomData || !this.contactAndRoomData.readyEmitted) { await this.setContactAndRoomData() } }) grpcGatewayEmitter.on('EXPIRED_TOKEN', async () => { log.info(EXPIRED_TOKEN_MESSAGE) setTimeout(() => process.exit(), 5000) }) grpcGatewayEmitter.on('INVALID_TOKEN', async () => { log.info(INVALID_TOKEN_MESSAGE) setTimeout(() => process.exit(), 5000) }) grpcGatewayEmitter.on('data', async (data: StreamResponse) => { const type = data.getResponsetype() switch (type) { case ResponseType.LOGIN_QRCODE : const qrcodeRawData = data.getData() if (qrcodeRawData) { const qrcodeData = JSON.parse(qrcodeRawData) grpcGatewayEmitter.setQrcodeId(qrcodeData.qrcodeId) const fileBox = FileBox.fromBase64(qrcodeData.qrcode, `qrcode${(Math.random() * 10000).toFixed()}.png`) const qrcodeUrl = await fileBoxToQrcode(fileBox) this.emit('scan', qrcodeUrl, ScanStatus.Cancel) this.qrcodeStatus = ScanStatus.Cancel } break case ResponseType.QRCODE_SCAN : const scanRawData = data.getData() if (scanRawData) { log.silly(PRE, `QRCODE_SCAN : ${util.inspect(scanRawData)}`) const scanData: ScanData = JSON.parse(scanRawData) log.info(PRE, ` ================================================= QRCODE_SCAN MSG : ${scanData.msg || '已确认'} ================================================= `) grpcGatewayEmitter.setQrcodeId(scanData.user_name) switch (scanData.status as QrcodeStatus) { case QrcodeStatus.Scanned: if (this.qrcodeStatus !== ScanStatus.Waiting) { this.qrcodeStatus = ScanStatus.Waiting this.emit('scan', '', this.qrcodeStatus) } break case QrcodeStatus.Confirmed: if (this.qrcodeStatus !== ScanStatus.Scanned) { this.qrcodeStatus = ScanStatus.Scanned this.emit('scan', '', this.qrcodeStatus) } break case QrcodeStatus.Canceled: case QrcodeStatus.Expired: const uin = await grpcGatewayEmitter.getUIN() const wxid = await grpcGatewayEmitter.getUserName() const data = { uin, wxid, } this.emit('scan', '', scanData.status) if (this.padplusUser) { await this.padplusUser.getWeChatQRCode(data) } break default: break } } break case ResponseType.QRCODE_LOGIN : const grpcLoginData = data.getData() if (grpcLoginData) { log.silly(PRE, `QRCODE_LOGIN : ${util.inspect(grpcLoginData)}`) const loginData: GrpcQrCodeLogin = JSON.parse(grpcLoginData) this.loginStatus = true grpcGatewayEmitter.setQrcodeId('') grpcGatewayEmitter.setUserName(loginData.userName) grpcGatewayEmitter.setUIN(loginData.uin) if (this.memory) { this.memorySlot = { qrcodeId: '', uin: loginData.uin, userName: loginData.userName, } log.silly(PRE, `name: ${this.options.name}, memory slot : ${util.inspect(this.memorySlot)}`) await this.memory.set(MEMORY_SLOT_NAME, this.memorySlot) await this.memory.save() } log.verbose(PRE, `init cache manager`) await CacheManager.init(loginData.userName) this.cacheManager = CacheManager.Instance const contactSelf: PadplusContactPayload = { alias: loginData.alias, bigHeadUrl: loginData.headImgUrl, city: '', contactFlag: 3, contactType: 0, country: '', labelLists: '', nickName: loginData.nickName, province: '', remark: '', sex: ContactGender.Unknown, signature: '', smallHeadUrl: '', stranger: '', ticket: '', userName: loginData.userName, verifyFlag: 0, } await this.cacheManager.setContact(contactSelf.userName, contactSelf) this.emit('login', loginData) const selfOnline = await this.getContact(loginData.userName) if (selfOnline) { await this.cacheManager.setContact(selfOnline.userName, selfOnline) } } break case ResponseType.AUTO_LOGIN : const grpcAutoLoginData = data.getData() if (grpcAutoLoginData) { const autoLoginData = JSON.parse(grpcAutoLoginData) log.silly(PRE, `user name : ${util.inspect(autoLoginData)}`) if (autoLoginData && autoLoginData.online) { if (!this.loginStatus) { const wechatUser = autoLoginData.wechatUser log.verbose(PRE, `init cache manager`) await CacheManager.init(wechatUser.userName) this.cacheManager = CacheManager.Instance /* if (this.padplusUser) { await this.padplusUser.reconnect() } else { throw new Error(`no padplus user.`) } */ const contactSelfInfo = await this.contactSelfInfo() const contactSelfPayload = convertFromGrpcContactSelf(contactSelfInfo) await this.cacheManager.setContact(contactSelfPayload.userName, contactSelfPayload) this.emit('login', wechatUser) this.loginStatus = true } } else { const uin = await grpcGatewayEmitter.getUIN() const wxid = await grpcGatewayEmitter.getUserName() const data = { uin, wxid, } await grpcGatewayEmitter.setUIN('') await grpcGatewayEmitter.setUserName('') if (this.padplusUser) { await this.padplusUser.getWeChatQRCode(data) } } } break case ResponseType.ACCOUNT_LOGOUT : const logoutRawData = data.getData() if (logoutRawData) { const logoutData: LogoutGrpcResponse = JSON.parse(logoutRawData) const uin = data.getUin() const _uin = grpcGatewayEmitter.getUIN() if (uin === _uin) { this.loginStatus = false if (logoutData.mqType === 1100) { this.emit('error', new PadplusError(PadplusErrorType.EXIT, logoutData.message)) await new Promise((resolve) => setTimeout(resolve, 20 * 1000)) this.emit('logout') } } else { const userName = grpcGatewayEmitter.getUserName() throw new Error(`can not get userName for this uin : ${uin}, userName: ${userName}`) } } else { log.info(`can not get data from Event LOGOUT, ready to restart...`) if (!this.padplusUser) { throw new Error(`no padplusUser`) } await this.padplusUser.reconnect() await new Promise((resolve) => setTimeout(resolve, 30 * 1000)) this.emit('reset', 'logout with some unknow reasons') } break case ResponseType.CONTACT_LIST : case ResponseType.CONTACT_MODIFY : const roomRawData = data.getData() if (roomRawData) { const _data = JSON.parse(roomRawData) if (!isRoomId(_data.UserName)) { const contactData: GrpcContactPayload = _data if (contactData.Type7) { log.warn(`This id: ${contactData.UserName} is not wxid, using weixin instead of wxid will potentially cause system failure. To make sure everything works as excepted, please use the wxid to load Contact.`) } const contact = convertFromGrpcContact(contactData, true) if (this.cacheManager) { await this.cacheManager.setContact(contact.userName, contact) } CallbackPool.Instance.resolveContactCallBack(contact.userName, contact) } else { const roomData: GrpcRoomPayload = _data const roomPayload: PadplusRoomPayload = convertRoomFromGrpc(roomData) if (this.cacheManager) { const roomMembers = briefRoomMemberParser(roomPayload.members) const _roomMembers = await this.cacheManager.getRoomMember(roomPayload.chatroomId) if (!_roomMembers) { await this.cacheManager.setRoomMember(roomPayload.chatroomId, roomMembers) } await this.cacheManager.setRoom(roomPayload.chatroomId, roomPayload) } else { throw new PadplusError(PadplusErrorType.NO_CACHE, `CONTACT_MODIFY`) } CallbackPool.Instance.resolveRoomCallBack(roomPayload.chatroomId, roomPayload) } } break case ResponseType.CONTACT_DELETE : const contactDataStr = data.getData() if (contactDataStr) { const contactData: GrpcDeleteContact = JSON.parse(contactDataStr) log.silly(PRE, `delete contact data : ${util.inspect(contactData)}`) const deleteUserName = contactData.field if (this.cacheManager) { if (isRoomId(deleteUserName)) { const room = await this.cacheManager.getRoom(deleteUserName) if (!room) { const roomPayload: PadplusRoomPayload = { alias : '', bigHeadUrl : '', chatRoomOwner : '', chatroomId : deleteUserName, chatroomVersion: 0, contactType : 0, isDelete : true, labelLists : '', memberCount : 0, members : [], nickName : '', smallHeadUrl : '', stranger : '', ticket : '', } await this.cacheManager.setRoom(deleteUserName, roomPayload) } else { room.isDelete = true await this.cacheManager.setRoom(deleteUserName, room) } await this.cacheManager.setRoomMember(deleteUserName, {} as { [contactId: string]: PadplusRoomMemberPayload }) } else if (isContactId(deleteUserName)) { await this.cacheManager.deleteContact(deleteUserName) } else { throw new Error(`the filed is not right.`) } } } break case ResponseType.MESSAGE_RECEIVE : const rawMessageStr = data.getData() if (rawMessageStr) { const rawMessage: GrpcMessagePayload = JSON.parse(rawMessageStr) const message: PadplusMessagePayload = await this.onProcessMessage(rawMessage) this.emit('message', message) } break case ResponseType.CONTACT_ADD : const addContactCallback = CallbackPool.Instance.getCallback(data.getRequestid()!) addContactCallback && addContactCallback(data) CallbackPool.Instance.removeCallback(data.getRequestid()!) break case ResponseType.CONTACT_SEARCH : const searchContactTraceId = data.getTraceid() if (searchContactTraceId) { const searchContactCallback = CallbackPool.Instance.getCallback(searchContactTraceId) searchContactCallback && searchContactCallback(data) CallbackPool.Instance.removeCallback(searchContactTraceId) } break case ResponseType.ROOM_QRCODE: const roomQrcodeTraceId = data.getTraceid() if (roomQrcodeTraceId) { const callback = CallbackPool.Instance.getCallback(roomQrcodeTraceId) callback && callback(data) CallbackPool.Instance.removeCallback(roomQrcodeTraceId) } break case ResponseType.ROOM_MEMBER_LIST : const roomMembersStr = data.getData() if (roomMembersStr) { if (this.cacheManager) { const roomMemberList: GrpcRoomMemberList = JSON.parse(roomMembersStr) const roomId = roomMemberList.roomId const membersStr = roomMemberList.membersJson const membersList: GrpcRoomMemberPayload[] = JSON.parse(membersStr) const members = roomMemberParser(membersList) await this.cacheManager.setRoomMember(roomId, members) await Promise.all(membersList.map(async member => { if (!this.cacheManager) { throw new PadplusError(PadplusErrorType.NO_CACHE, 'roomMemberList') } const contact = await this.cacheManager.getContact(member.UserName) if (!contact) { const newContact: PadplusContactPayload = { alias: '', bigHeadUrl: member.HeadImgUrl, city: '', contactFlag: 0, contactType: 0, country: '', labelLists: '', nickName: member.NickName, province: '', remark: '', sex: ContactGender.Unknown, signature: '', smallHeadUrl: member.HeadImgUrl, stranger: '', ticket: '', userName: member.UserName, verifyFlag: 0, } await this.cacheManager.setContact(newContact.userName, newContact) } else { const newContact: PadplusContactPayload = Object.assign({}, contact, { bigHeadUrl: member.HeadImgUrl, nickName: member.NickName, smallHeadUrl: member.HeadImgUrl, userName: member.UserName, }) await this.cacheManager.setContact(newContact.userName, newContact) } CallbackPool.Instance.resolveRoomMemberCallback(roomId, members) })) } else { throw new PadplusError(PadplusErrorType.NO_CACHE, `CONTACT_MODIFY`) } } else { throw new Error(`can not get receive room member data from server`) } break case ResponseType.ROOM_MEMBER_MODIFY : // TODO: not support now break case ResponseType.STATUS_NOTIFY : // TODO: not support now break case ResponseType.MESSAGE_MEDIA_SRC : const traceId = data.getTraceid() if (traceId) { const callback = CallbackPool.Instance.getCallback(traceId) callback && callback(data) CallbackPool.Instance.removeCallback(traceId) } else { log.silly(PRE, `can not get trace id`) } break case ResponseType.REQUEST_RESPONSE : const requestId = data.getRequestid() const responseData = data.getData() if (responseData) { const callback = CallbackPool.Instance.getCallback(requestId!) callback && callback(data) } break } }) } /** * Contact Self Section */ public async contactSelfQrcode (): Promise<string> { log.silly(PRE, `contactSelfQrcode()`) if (this.padplusContact) { return this.padplusContact.contactSelfQrcode() } else { throw new Error(`no padplus contact`) } } public async contactSelfName (name: string): Promise<void> { log.silly(PRE, `contactSelfName(${name})`) if (this.padplusContact) { const data = { nickName: name, } await this.padplusContact.setContactSelfInfo(data) } else { throw new Error(`no padplus contact`) } } public async contactSelfSignature (signature: string): Promise<void> { log.silly(PRE, `contactSelfSignature(${signature})`) if (this.padplusContact) { const data = { signature, } await this.padplusContact.setContactSelfInfo(data) } else { throw new Error(`no padplus contact`) } } public async contactSelfInfo (): Promise<GetContactSelfInfoGrpcResponse> { log.silly(PRE, `contactSelfInfo()`) if (this.padplusContact) { return this.padplusContact.getContactSelfInfo() } else { throw new Error(`no padplus contact`) } } /** * Message Section */ public async loadRichMediaData (mediaData: PadplusRichMediaData): Promise<PadplusMediaData> { log.silly(PRE, `loadRichMediaData()`) if (!this.padplusMesasge) { throw new Error(`no padplus message`) } const data = await this.padplusMesasge.loadRichMeidaData(mediaData) const mediaStr = data.getData() if (mediaStr) { const mediaData = JSON.parse(mediaStr) return mediaData } else { throw new Error(`can not load media data on manager`) } } public async sendMessage (selfId: string, receiver: string, text: string, type: PadplusMessageType, mention?: string) { log.silly(PRE, `selfId : ${selfId}, receiver : ${receiver}, text : ${text}, type : ${type}`) if (!this.padplusMesasge) { throw new Error(`no padplus message`) } return this.padplusMesasge.sendMessage(selfId, receiver, text, type, mention) } public async sendContact (selfId: string, receiver: string, contentStr: string) { log.silly(PRE, `selfId : ${selfId},receiver : ${receiver}`) if (!this.cacheManager) { throw new PadplusError(PadplusErrorType.NO_CACHE, `sendContact()`) } if (!this.padplusMesasge) { throw new Error(`no padplus message`) } return this.padplusMesasge.sendContact(selfId, receiver, contentStr) } public async addFriend ( contactId: string, hello: string | undefined, isPhoneNumber: number, strangerV1: string, strangerV2: string, ) { log.verbose(PRE, `addFriend(), isPhoneNumber: ${isPhoneNumber}`) if (!this.padplusFriendship) { throw new Error(`no padplusFriendship`) } return this.padplusFriendship.addFriend(strangerV1, strangerV2, isPhoneNumber, contactId, hello) } public async generatorFileUrl (file: FileBox): Promise<string> { log.verbose(PRE, 'generatorFileUrl(%s)', file) if (this.requestClient) { const url = await this.requestClient.uploadFile(file.name, await file.toStream()) return url } else { throw new Error(`no requestClient`) } } public async sendFile (selfId: string, receiverId: string, url: string, fileName: string, subType: string, fileSize?: number) { log.verbose(PRE, 'sendFile()') if (!this.padplusMesasge) { throw new Error(`no padplus message`) } return this.padplusMesasge.sendFile(selfId, receiverId, url, fileName, subType, fileSize) } public async sendUrlLink (selfId: string, receiver: string, content: string) { if (!this.padplusMesasge) { throw new Error(`no padplus message`) } return this.padplusMesasge.sendUrlLink(selfId, receiver, content) } private async onProcessMessage (rawMessage: any): Promise<PadplusMessagePayload> { const payload: PadplusMessagePayload = await convertMessageFromGrpcToPadplus(rawMessage) this.cachePadplusMessagePayload.set(payload.msgId, payload) return payload } /** * Contact Section */ public async setContactAlias ( contactId: string, alias: string, ): Promise<void> { log.silly(PRE, `setContactAlias(), contactId : ${contactId}, alias: ${alias}`) if (!this.padplusContact) { throw new Error(`no padplusContact`) } await this.padplusContact.setAlias(contactId, alias) return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('set alias failed since timeout')) }, 5000) CallbackPool.Instance.pushContactAliasCallback(contactId, alias, () => { clearTimeout(timeout) resolve() }) }) } public async getContactIdList ( selfId: string, ): Promise<string[]> { log.silly(PRE, `selfId : ${util.inspect(selfId)}`) if (!this.cacheManager) { throw new PadplusError(PadplusErrorType.NO_CACHE, 'contactList()') } return this.cacheManager.getContactIds() } public async getContact ( contactId: string ): Promise<PadplusContactPayload | null | undefined> { if (!this.cacheManager) { throw new Error() } const contact = await this.cacheManager.getContact(contactId) if (contact) { return contact } await this.getContactQueue.execute(async () => { if (!this.padplusContact) { throw new Error(`no padplusContact`) } await this.padplusContact.getContactInfo(contactId) }) return new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('get contact timeout')), 5000) CallbackPool.Instance.pushContactCallback(contactId, (data) => { clearTimeout(timeout) resolve(data as PadplusContactPayload) }) }) } public async getContactPayload ( contactId: string, ): Promise<PadplusContactPayload> { const payload = await this.getContact(contactId) if (!payload) { throw new Error('Can not find payload for contactId ' + contactId) } return payload } public async searchContact ( contactId: string, ): Promise<GrpcSearchContact> { if (!this.padplusContact) { throw new Error(`no padplusContact`) } const payload = await this.padplusContact.searchContact(contactId) if (!payload) { throw new Error('Can not find payload for contactId ' + contactId) } return payload } public async syncContacts (): Promise<void> { log.silly(PRE, `sync all contacts`) if (!this.padplusContact) { throw new Error(`no padplusContact`) } await this.padplusContact.syncContacts() } /** * Room Section */ public async setRoomTopic ( roomId: string, topic: string, ) { if (!this.padplusRoom) { throw new Error(`no padplus Room.`) } await this.padplusRoom.setTopic(roomId, topic) if (this.cacheManager) { await this.cacheManager.deleteRoom(roomId) } else { throw new Error(`no cache manager.`) } return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('set alias failed since timeout')) }, 5000) CallbackPool.Instance.pushRoomTopicCallback(roomId, topic, () => { clearTimeout(timeout) resolve() }) }) } public async getRoomQrcode (roomId: string): Promise<string> { if (!this.padplusRoom) { throw new Error(`no padplusRoom`) } const qrcodeBuf = this.padplusRoom.getRoomQrcode(roomId) return qrcodeBuf } public async getRoomIdList ():Promise<string[]> { if (!this.cacheManager) { throw new Error(`no cache.`) } return this.cacheManager.getRoomIds() } public async getRoomMemberIdList ( roomId: string, ) { const memberMap = await this.getRoomMembers(roomId) if (memberMap) { return Object.keys(memberMap) } else { return [] } } public async getRoomInfo (roomId: string) { const room = await this.getRoom(roomId) if (room) { if (!this.cacheManager) { throw new PadplusError(PadplusErrorType.NO_CACHE, `get room info`) } await this.cacheManager.setRoom(room.chatroomId, room) return room } else { throw new Error(`can not get room info by api`) } } public async getRoom (roomId: string): Promise<PadplusRoomPayload | null | undefined> { if (!this.cacheManager) { throw new Error() } const room = await this.cacheManager.getRoom(roomId) if (room) { return room } await this.getContactQueue.execute(async () => { if (!this.padplusContact) { throw new Error(`no padplusContact`) } await this.padplusContact.getContactInfo(roomId) }) return new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('get room timeout')), 5000) CallbackPool.Instance.pushContactCallback(roomId, (data) => { clearTimeout(timeout) resolve(data as PadplusRoomPayload) }) }) } public async getRoomMembers ( roomId: string, ): Promise<PadplusRoomMemberMap> { if (!this.cacheManager) { throw new Error(`no cache.`) } const memberMap = await this.cacheManager.getRoomMember(roomId) if (!memberMap) { if (!this.grpcGatewayEmitter) { throw new Error(`no grpcGatewayEmitter.`) } const uin = this.grpcGatewayEmitter.getUIN() await this.getRoomMemberQueue.execute(async () => { if (!this.padplusRoom) { throw new Error(`no padplus Room.`) } await this.padplusRoom.getRoomMembers(uin, roomId) }) return new Promise<PadplusRoomMemberMap>((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('get room member failed since timeout')) }, 5000) CallbackPool.Instance.pushRoomMemberCallback(roomId, (data: PadplusRoomMemberMap) => { clearTimeout(timeout) resolve(data) }) }) } else { return memberMap } } public async deleteRoomMember (roomId: string, contactId: string): Promise<void> { log.silly(PRE, `deleteRoomMember(%s, %s)`, roomId, contactId) if (!this.padplusRoom) { throw new Error(`no padplus Room.`) } await this.padplusRoom.deleteRoomMember(roomId, contactId) } public async setAnnouncement ( roomId: string, announcement: string, ) { if (!this.padplusRoom) { throw new Error(`no padplus Room.`) } return this.padplusRoom.setAnnouncement(roomId, announcement) } public async getAnnouncement ( roomId: string, ): Promise<string> { if (!this.padplusRoom) { throw new Error(`no padplus Room.`) } return this.padplusRoom.getAnnouncement(roomId) } public async roomAddMember ( roomId: string, memberId: string, ) { log.silly(PRE, `roomAddMember: ${roomId}, ${memberId}`) if (!this.padplusRoom) { throw new Error(`no padplus Room.`) } await this.padplusRoom.addMember(roomId, memberId) } public async createRoom ( topic: string, memberIdList: string[], ) { log.silly(PRE, `careteRoom : ${topic}, ${memberIdList.join(',')}`) if (!this.padplusRoom) { throw new Error(`no padplus Room.`) } const result = await this.padplusRoom.createRoom(topic, memberIdList) return result } public async quitRoom ( roomId: string, ) { log.silly(PRE, `quitRoom : ${roomId}`) if (!this.padplusRoom) { throw new Error(`no padplus Room.`) } await this.padplusRoom.quitRoom(roomId) } /** * * room event * */ public async roomInvitationRawPayload ( roomInvitationId: string, ) { log.verbose(PRE, `roomInvitationRawPayload(${roomInvitationId})`) if (!this.cacheManager) { throw new Error(`no cache manager.`) } const payload = await this.cacheManager.getRoomInvitation(roomInvitationId) if (payload) { return payload } else { throw new Error(`can not find invitation with id:${roomInvitationId}`) } } /** * Friendship Section */ public async getFriendship ( friendshipId: string, ) { log.silly(PRE, `getFriendship(${friendshipId})`) if (!this.cacheManager) { throw new Error(`no cache manager.`) } const friendship = await this.cacheManager.getFriendshipRawPayload(friendshipId) return friendship } public async confirmFriendship ( contactId: string, encryptUserName: string, ticket: string, ) { if (!this.padplusFriendship) { throw new Error(`no padplusFriendship`) } await this.padplusFriendship.confirmFriendship(encryptUserName, ticket) await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('accept friend request timeout.')) }, 60 * 1000) CallbackPool.Instance.pushAcceptFriendCallback(contactId, () => { clearTimeout(timeout) resolve() }) }) } public async saveFriendship ( friendshipId: string, friendship: FriendshipPayload, ): Promise<void> { log.silly(PRE, `saveFriendship : ${util.inspect(friendship)}`) if (!this.cacheManager) { throw new Error(`no cache.`) } await this.cacheManager.setFriendshipRawPayload(friendshipId, friendship as PuppetFriendshipPayload) } } export default PadplusManager