wechaty-puppet-padplus
Version:
Puppet Padplus for Wechaty
1,214 lines (1,094 loc) • 42.4 kB
text/typescript
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