UNPKG

wechaty-puppet-padplus

Version:
1,191 lines (1,048 loc) 41.4 kB
import util from 'util' import path from 'path' import FileBox from 'file-box' import flatten from 'array-flatten' import { ContactPayload, MessagePayload, Receiver, FriendshipPayload, RoomMemberPayload, RoomPayload, UrlLinkPayload, MiniProgramPayload, PuppetOptions, ScanStatus, Puppet, RoomInvitationPayload, FriendshipType, FriendshipPayloadReceive, MessageType, } from 'wechaty-puppet' import { log, padplusToken, retry, GRPC_ENDPOINT, PADPLUS_REPLAY_MESSAGE, } from './config' import { RequestQueue } from './padplus-manager/api-request/request-queue' import PadplusManager from './padplus-manager/padplus-manager' import { PadplusMessageType, PadplusContactPayload, PadplusRoomPayload, GrpcQrCodeLogin, PadplusRoomMemberPayload, PadplusRoomInvitationPayload, FriendshipPayload as PadplusFriendshipPayload, SearchContactTypeStatus, GrpcSearchContact, PadplusMessageStatus, GetContactSelfInfoGrpcResponse } from './schemas' import { PadplusMessagePayload, PadplusRichMediaData, GrpcResponseMessageData } from './schemas/model-message' import { convertToPuppetRoomMember } from './convert-manager/room-convertor' import { roomJoinEventMessageParser } from './pure-function-helpers/room-event-join-message-parser' import { roomLeaveEventMessageParser } from './pure-function-helpers/room-event-leave-message-parser' import { roomTopicEventMessageParser } from './pure-function-helpers/room-event-topic-message-parser' import { friendshipConfirmEventMessageParser, friendshipReceiveEventMessageParser, friendshipVerifyEventMessageParser } from './pure-function-helpers/friendship-event-message-parser' import { messageRawPayloadParser, roomRawPayloadParser, friendshipRawPayloadParser, appMessageParser, isStrangerV2, isStrangerV1, isRoomId } from './pure-function-helpers' import { contactRawPayloadParser } from './pure-function-helpers/contact-raw-payload-parser' import { xmlToJson } from './pure-function-helpers/xml-to-json' const PRE = 'PUPPET_PADPLUS' export class PuppetPadplus extends Puppet { private manager: PadplusManager constructor ( public options: PuppetOptions = {}, ) { super(options) const token = this.options.token || padplusToken() const name = this.options.name if (token) { this.manager = new PadplusManager({ endpoint: this.options.endpoint || GRPC_ENDPOINT, name, token, }) } else { log.error(PRE, `can not get token info from options for start grpc gateway.`) throw new Error(`can not get token info.`) } } public async start (): Promise<void> { log.silly(PRE, `start()`) this.state.on('pending') await this.startManager(this.manager) this.state.on(true) } private async startManager (manager: PadplusManager) { manager.on('scan', async (url: string, status: ScanStatus) => { log.silly(PRE, `scan : ${url}, status: ${status}`) this.emit('scan', url, status) }) manager.on('login', async (loginData: GrpcQrCodeLogin) => { log.silly(PRE, `login success : ${util.inspect(loginData)}`) await super.login(loginData.userName) await this.manager.syncContacts() }) manager.setMemory(this.memory) manager.on('message', msg => this.onMessage(msg)) manager.on('ready', () => this.emit('ready')) manager.on('reset', (reason: string) => { this.emit('reset', reason) }) manager.on('heartbeat', (data: string) => { this.emit('watchdog', { data, }) }) manager.on('logout', () => this.logout()) manager.on('error', (err: Error) => { this.emit('error', err) }) await manager.start() } public async stop (): Promise<void> { log.verbose(PRE, 'stop()') if (!this.manager) { throw new Error('no padplus manager') } if (this.state.off()) { log.verbose(PRE, 'stop() is called on a OFF puppet. await ready(off) and return.') await this.state.ready('off') return } this.state.off('pending') await this.manager.stop() await this.manager.removeAllListeners() this.state.off(true) log.verbose(PRE, `stop() finished`) } public async logout (): Promise<void> { log.verbose(PRE, 'logout()') await this.manager.logout(this.selfId()) this.emit('logout', this.selfId()) this.id = undefined this.emit('reset', 'padplus reset') } async onMessage (message: PadplusMessagePayload) { const messageType = message.msgType switch (messageType) { case PadplusMessageType.Sys: await Promise.all([ this.onRoomJoinEvent(message), this.onRoomLeaveEvent(message), this.onRoomTopicEvent(message), this.onFriendshipEvent(message), ]) break case PadplusMessageType.VerifyMsg: await this.onFriendshipEvent(message) break case PadplusMessageType.Recalled: this.emit('message', message.msgId) await this.onRoomJoinEvent(message) break case PadplusMessageType.Text: await this.onFriendshipEvent(message) this.emit('message', message.msgId) break case PadplusMessageType.Contact: case PadplusMessageType.Image: case PadplusMessageType.Deleted: case PadplusMessageType.Voice: case PadplusMessageType.SelfAvatar: case PadplusMessageType.PossibleFriendMsg: case PadplusMessageType.ShareCard: case PadplusMessageType.Video: case PadplusMessageType.Emoticon: case PadplusMessageType.Location: case PadplusMessageType.App: case PadplusMessageType.VoipMsg: case PadplusMessageType.StatusNotify: case PadplusMessageType.VoipNotify: case PadplusMessageType.VoipInvite: case PadplusMessageType.MicroVideo: case PadplusMessageType.SelfInfo: case PadplusMessageType.SysNotice: case PadplusMessageType.N11_2048: case PadplusMessageType.N15_32768: default: this.emit('message', message.msgId) break } } /** * ======================== * CONTACT SECTION * ======================== */ public async contactSelfQrcode (): Promise<string> { log.silly(PRE, `contactSelfQrcode()`) return this.manager.contactSelfQrcode() } public async contactSelfName (name: string): Promise<void> { log.silly(PRE, `contactSelfName(${name})`) await this.manager.contactSelfName(name) } public async contactSelfSignature (signature: string): Promise<void> { log.silly(PRE, `contactSelfSignature(${signature})`) await this.manager.contactSelfSignature(signature) } public async contactSelfInfo (): Promise<GetContactSelfInfoGrpcResponse> { log.silly(PRE, `contactSelfInfo()`) return this.manager.contactSelfInfo() } contactAlias (contactId: string): Promise<string> contactAlias (contactId: string, alias: string | null): Promise<void> public async contactAlias (contactId: string, alias?: string | null): Promise<string | void> { log.silly(PRE, `contactAlias(), contactId: ${contactId}, alias : ${alias}`) if (typeof alias === 'undefined') { const payload = await this.contactPayload(contactId) return payload.alias || '' } if (!this.manager) { throw new Error(`no padplus manage.`) } await this.manager.setContactAlias(contactId, alias || '') } contactAvatar (contactId: string): Promise<FileBox> contactAvatar (contactId: string, file: FileBox): Promise<void> public async contactAvatar (contactId: string, file?: FileBox): Promise<FileBox | void> { if (file) { if (contactId !== this.selfId()) { throw new Error(`can not set avatar for others.`) } if (!this.manager) { throw new Error(`no padplus manager.`) } return } const payload = await this.contactRawPayload(contactId) if (!payload || !payload.bigHeadUrl) { throw new Error(`can not find contact.`) } const fileBox = FileBox.fromUrl( payload.bigHeadUrl, `wechaty-contact-avatar-${payload.userName}.jpg` ) return fileBox } public async contactList (): Promise<string[]> { log.verbose(PRE, `contactList()`) if (!this.manager) { throw new Error(`no padplus manager.`) } const selfId = this.selfId() const contactIds = await this.manager.getContactIdList(selfId) return contactIds } protected async contactRawPayload (contactId: string): Promise<PadplusContactPayload> { if (!this.manager) { throw new Error(`no manager.`) } const payload = await this.manager.getContactPayload(contactId) return payload } protected async contactRawPayloadParser (rawPayload: PadplusContactPayload): Promise<ContactPayload> { log.verbose(PRE, `contactRawPayloadParser()`) const payload = contactRawPayloadParser(rawPayload) return payload } /** * ========================= * FRIENDSHIP SECTION * ========================= */ async onFriendshipEvent (message: PadplusMessagePayload): Promise<void> { log.verbose(PRE, 'onPadplusMessageFriendshipEvent({id=%s})', message.msgId) /** * 1. Look for friendship confirm event */ const friendshipConfirmContactId = friendshipConfirmEventMessageParser(message) /** * 2. Look for friendship receive event */ const friendshipReceiveContactId = await friendshipReceiveEventMessageParser(message) /** * 3. Look for friendship verify event */ const friendshipVerifyContactId = friendshipVerifyEventMessageParser(message) if (friendshipConfirmContactId || friendshipReceiveContactId || friendshipVerifyContactId ) { // Maybe load contact here since we know a new friend is added if (!this.manager) { throw new Error(`no manager.`) } const friendship = await friendshipRawPayloadParser(message) if (!friendship) { log.silly(PRE, `not friendship : ${message.msgId}`) return } const { msgId } = message await this.manager.saveFriendship(msgId, friendship) this.emit('friendship', msgId) } } public async friendshipAdd (contactId: string, hello?: string): Promise<void> { log.verbose(PRE, `friendshipAdd(${contactId}, ${hello})`) if (!this.manager) { throw new Error('no padplus manager') } const searchContact: GrpcSearchContact = await this.manager.searchContact(contactId) /** * If the contact is not stranger, than using WXSearchContact can get userName */ if (searchContact.wxid !== '' && !isStrangerV1(searchContact.v1) && !isStrangerV2(searchContact.v2)) { log.verbose(PRE, `friendshipAdd ${contactId} has been friend with bot, no need to send friend request!`) return } let strangerV1 let strangerV2 if (isStrangerV1(searchContact.v1)) { strangerV1 = searchContact.v1 strangerV2 = searchContact.v2 } else if (isStrangerV2(searchContact.v2)) { strangerV2 = searchContact.v2 strangerV1 = searchContact.v1 } else { throw new Error('stranger neither v1 nor v2!') } const isPhoneNumber = contactId.match(/^[1]([3-9])[0-9]{9}$/) const res = await this.manager.addFriend( contactId, hello, isPhoneNumber ? SearchContactTypeStatus.MOBILE : SearchContactTypeStatus.WXID, // default to wxid strangerV1 || '', strangerV2 || '', ) if (res && res.status !== '0') { throw new Error(`add friend failed.`) } else if (res && res.status === '0') { log.silly(PRE, `add friend request success.`) } } public async friendshipAccept (friendshipId: string): Promise<void> { log.verbose(PRE, `friendshipAccept(${friendshipId})`) if (!this.manager) { throw new Error(`no manager.`) } const payload = await this.manager.getFriendship(friendshipId) if (!payload || payload.type !== FriendshipType.Receive) { throw new Error(`friendship type error.`) } const { contactId, stranger, ticket } = payload as FriendshipPayloadReceive if (!stranger || !ticket) { throw new Error(`friendship data error, stranger or ticket is null.`) } await this.manager.confirmFriendship(contactId, stranger, ticket) } protected async friendshipRawPayload (friendshipId: string): Promise<PadplusFriendshipPayload> { log.silly(PRE, `friendshipId : ${util.inspect(friendshipId)}`) if (!this.manager) { throw new Error(`no manager.`) } const payload = await this.manager.getFriendship(friendshipId) if (payload) { return payload } throw new Error(`can not find friendship.`) } protected async friendshipRawPayloadParser (rawPayload: PadplusFriendshipPayload): Promise<FriendshipPayload> { log.silly(PRE, `friendship rawPayload : ${util.inspect(rawPayload)}`) return rawPayload as FriendshipPayload } /** * ======================== * MESSAGE SECTION * ======================== */ public async messageFile (messageId: string): Promise<FileBox> { log.silly(PRE, `messageFile() messageId : ${util.inspect(messageId)}`) const rawPayload = await this.messageRawPayload(messageId) const payload = await this.messagePayload(messageId) let filename = payload.filename || payload.id const type = payload.type === MessageType.Image ? 'img' : payload.type === MessageType.Video ? 'video' : 'file' switch (payload.type) { case MessageType.Image: case MessageType.Attachment: case MessageType.Video: let content = rawPayload.content const mediaData: PadplusRichMediaData = { appMsgType: type === 'file' ? 6 : 0, content, contentType: type, createTime: rawPayload.createTime, fileName: rawPayload.fileName || '', fromUserName: rawPayload.fromUserName, msgId: rawPayload.msgId, msgType: rawPayload.msgType, src: rawPayload.url, toUserName: rawPayload.toUserName, } const data = await RequestQueue.exec(() => this.manager.loadRichMediaData(mediaData)) if (data && data.src) { const name = path.parse(data.src).base return FileBox.fromUrl(encodeURI(data.src), name) } else { throw new Error(`Can not get media data url by this message id: ${messageId}`) } case MessageType.Emoticon: if (rawPayload && rawPayload.url) { return FileBox.fromUrl(rawPayload.url) } else { throw new Error(`can not get image/audio url fot message id: ${messageId}`) } case MessageType.Audio: if (rawPayload && rawPayload.url) { const fileBox = FileBox.fromUrl(rawPayload.url) let contentXML if (isRoomId(rawPayload.fromUserName)) { contentXML = rawPayload.content.split(':\n')[1] } else { contentXML = rawPayload.content } const content = await xmlToJson(contentXML) fileBox.metadata = { voiceLength: content.msg.voicemsg.$.voicelength / 1000, } return fileBox } else { throw new Error(`can not get image/audio url fot message id: ${messageId}`) } default: const base64 = 'Tm90IFN1cHBvcnRlZCBBdHRhY2htZW50IEZpbGUgVHlwZSBpbiBNZXNzYWdlLgpTZWU6IGh0dHBzOi8vZ2l0aHViLmNvbS9DaGF0aWUvd2VjaGF0eS9pc3N1ZXMvMTI0OQo=' filename = 'wechaty-puppet-padplus-message-attachment-' + messageId + '.txt' return FileBox.fromBase64( base64, filename, ) } } public async messageUrl (messageId: string): Promise<UrlLinkPayload> { log.silly(PRE, `messageUrl() messageId : ${util.inspect(messageId)}`) const rawPayload = await this.messageRawPayload(messageId) const payload = await this.messagePayload(messageId) if (payload.type !== MessageType.Url) { throw new Error('Can not get url from non url payload') } else { const appPayload = await appMessageParser(rawPayload) if (appPayload) { return { description: appPayload.des, thumbnailUrl: appPayload.thumburl, title: appPayload.title, url: appPayload.url, } } else { throw new Error('Can not parse url message payload') } } } public async messageContact (messageId: string): Promise<string> { throw new Error(`not implement`) } messageMiniProgram (messageId: string): Promise<MiniProgramPayload> { log.silly(PRE, `messageMiniProgram() messageId : ${util.inspect(messageId)}`) throw new Error('Method not implemented.') } public async messageForward (receiver: Receiver, messageId: string): Promise<void> { log.silly(PRE, `messageForward() receiver: ${receiver}, messageId : ${util.inspect(messageId)}`) const payload = await this.messagePayload(messageId) if (payload.type === MessageType.Text) { if (!payload.text) { throw new Error('no text') } await this.messageSendText( receiver, payload.text, ) } else if (payload.type === MessageType.Audio) { throw new Error('Message type Audio not supported.') } else if (payload.type === MessageType.Url) { await this.messageSendUrl( receiver, await this.messageUrl(messageId) ) } else if (payload.type === MessageType.MiniProgram) { await this.messageSendMiniProgram( receiver, await this.messageMiniProgram(messageId) ) } else if (payload.type === MessageType.ChatHistory) { throw new Error('Message type ChatHistory not supported.') } else { await this.messageSendFile( receiver, await this.messageFile(messageId), ) } } private generateBaseMsg (msgId: string, to: string): PadplusMessagePayload { const msg: PadplusMessagePayload = { content: '', createTime: new Date().getTime(), fromUserName: this.selfId(), imgStatus: 0, l1MsgType: 0, msgId, msgSource: '', msgSourceCd: 0, msgType: PadplusMessageType.Text, newMsgId: Number(msgId), pushContent: '', status: PadplusMessageStatus.One, toUserName: to, uin: '', wechatUserName: this.selfId(), } log.silly(PRE, 'generateBaseMsg(%s) %s', to, JSON.stringify(msg)) this.manager.cachePadplusMessagePayload.set(msgId, msg) return msg } public async messageSendText (receiver: Receiver, text: string, mentionIdList?: string[]): Promise<void | string> { log.silly(PRE, 'messageSend(%s, %s)', JSON.stringify(receiver), text) const contactIdOrRoomId = receiver.roomId || receiver.contactId let msgData: GrpcResponseMessageData if (mentionIdList && mentionIdList.length > 0) { msgData = await this.manager.sendMessage(this.selfId(), contactIdOrRoomId!, text, PadplusMessageType.Text, mentionIdList.toString()) if (PADPLUS_REPLAY_MESSAGE) { this.replayTextMsg(msgData.msgId, contactIdOrRoomId!, text, mentionIdList) } } else { msgData = await this.manager.sendMessage(this.selfId(), contactIdOrRoomId!, text, PadplusMessageType.Text) if (PADPLUS_REPLAY_MESSAGE) { this.replayTextMsg(msgData.msgId, contactIdOrRoomId!, text) } } if (msgData.success) { const msgPayload: PadplusMessagePayload = { content: text + ((mentionIdList && mentionIdList.length > 0) ? mentionIdList!.toString() : ''), createTime: msgData.timestamp, fromUserName: this.selfId(), imgStatus: 0, l1MsgType: 0, msgId: msgData.msgId, msgSource: 'self', msgSourceCd: 0, msgType: PadplusMessageType.Text, newMsgId: Number(msgData.msgId), pushContent: text, status: 1, toUserName: contactIdOrRoomId!, uin: '', wechatUserName: '', } this.manager.cachePadplusMessagePayload.set(msgData.msgId, msgPayload) } return msgData.msgId } private replayTextMsg (msgId: string, to: string, text: string, atUserList?: string[]): void { const payload = this.generateBaseMsg(msgId, to) payload.msgType = PadplusMessageType.Text payload.content = text if (atUserList) { payload.msgSource = `<msgsource>\n\t<atuserlist>${atUserList.join(',')}</atuserlist>\n</msgsource>\n` } log.silly(PRE, 'replayTextMsg replaying message: %s', JSON.stringify(payload)) this.emit('message', payload.msgId) } public async messageSendContact (receiver: Receiver, contactId: string): Promise<void | string> { log.verbose(PRE, `messageSend('%s', %s)`, receiver, contactId) const contactIdOrRoomId = receiver.roomId || receiver.contactId let contact = await this.manager.getContact(contactId) if (contact) { const content = { headImgUrl: contact.smallHeadUrl, nickName: contact.nickName, userName: contact.userName, } const contactData: GrpcResponseMessageData = await this.manager.sendContact(this.selfId(), contactIdOrRoomId!, JSON.stringify(content)) if (PADPLUS_REPLAY_MESSAGE) { this.replayContactMsg(contactData.msgId, contactIdOrRoomId!, JSON.stringify(content)) } if (contactData.success) { const msgPayload: PadplusMessagePayload = { content: JSON.stringify(content), createTime: contactData.timestamp, fromUserName: this.selfId(), imgStatus: 0, l1MsgType: 0, msgId: contactData.msgId, msgSource: 'self', msgSourceCd: 0, msgType: PadplusMessageType.Text, newMsgId: Number(contactData.msgId), pushContent: JSON.stringify(content), status: 1, toUserName: contactIdOrRoomId!, uin: '', wechatUserName: '', } this.manager.cachePadplusMessagePayload.set(contactData.msgId, msgPayload) } return contactData.msgId } else { throw new Error('not able to send contact') } } private replayContactMsg (msgId: string, to: string, content: string): void { const payload = this.generateBaseMsg(msgId, to) payload.msgType = PadplusMessageType.ShareCard payload.content = content log.silly(PRE, 'replayContactMsg replaying message: %s', JSON.stringify(payload)) this.emit('message', payload.msgId) } public async messageSendFile (receiver: Receiver, file: FileBox): Promise<void | string> { log.verbose(PRE, 'messageSendFile(%s, %s)', receiver, file) const contactIdOrRoomId = receiver.roomId || receiver.contactId const fileUrl = await this.manager.generatorFileUrl(file) const fileSize = (await file.toBuffer()).length log.silly(PRE, `file url : ${util.inspect(fileUrl)}`) // this needs to run before mimeType is available await file.ready() const type = (file.mimeType && file.mimeType !== 'application/octet-stream') ? file.mimeType : path.extname(file.name) log.silly(PRE, `fileType ${type}`) switch (type) { case '.slk': throw new Error('not support') case 'image/jpeg': case 'image/png': case '.jpg': case '.jpeg': case '.png': const picData = await this.manager.sendFile(this.selfId(), contactIdOrRoomId!, decodeURIComponent(fileUrl), file.name, 'pic') if (PADPLUS_REPLAY_MESSAGE) { this.replayImageMsg(picData.msgId, contactIdOrRoomId!, decodeURIComponent(fileUrl)) } if (picData.success) { const msgPayload: PadplusMessagePayload = { content: `<msg>${fileUrl}</msg>`, createTime: picData.timestamp, fromUserName: this.selfId(), imgStatus: 0, l1MsgType: 0, msgId: picData.msgId, msgSource: 'self', msgSourceCd: 0, msgType: PadplusMessageType.Text, newMsgId: Number(picData.msgId), pushContent: `<msg>${fileUrl}</msg>`, status: 1, toUserName: contactIdOrRoomId!, uin: '', wechatUserName: '', } this.manager.cachePadplusMessagePayload.set(picData.msgId, msgPayload) } return picData.msgId case 'video/mp4': case '.mp4': const videoData = await this.manager.sendFile(this.selfId(), contactIdOrRoomId!, fileUrl, file.name, 'video') if (PADPLUS_REPLAY_MESSAGE) { this.replayAppMsg(videoData.msgId, contactIdOrRoomId!, fileUrl) } if (videoData.success) { const msgPayload: PadplusMessagePayload = { content: `<msg>${fileUrl}</msg>`, createTime: videoData.timestamp, fromUserName: this.selfId(), imgStatus: 0, l1MsgType: 0, msgId: videoData.msgId, msgSource: 'self', msgSourceCd: 0, msgType: PadplusMessageType.Text, newMsgId: Number(videoData.msgId), pushContent: `<msg>${fileUrl}</msg>`, status: 1, toUserName: contactIdOrRoomId!, uin: '', wechatUserName: '', } this.manager.cachePadplusMessagePayload.set(videoData.msgId, msgPayload) } return videoData.msgId default: const docData = await this.manager.sendFile(this.selfId(), contactIdOrRoomId!, fileUrl, file.name, 'doc', fileSize) if (PADPLUS_REPLAY_MESSAGE) { this.replayAppMsg(docData.msgId, contactIdOrRoomId!, fileUrl) } if (docData.success) { const msgPayload: PadplusMessagePayload = { content: `<msg>${fileUrl}</msg>`, createTime: docData.timestamp, fromUserName: this.selfId(), imgStatus: 0, l1MsgType: 0, msgId: docData.msgId, msgSource: 'self', msgSourceCd: 0, msgType: PadplusMessageType.Text, newMsgId: Number(docData.msgId), pushContent: `<msg>${fileUrl}</msg>`, status: 1, toUserName: contactIdOrRoomId!, uin: '', wechatUserName: '', } this.manager.cachePadplusMessagePayload.set(docData.msgId, msgPayload) } return docData.msgId } } private replayImageMsg (msgId: string, to: string, url: string): void { const payload = this.generateBaseMsg(msgId, to) payload.msgType = PadplusMessageType.Image payload.content = `<msg>${url}</msg>` payload.url = url log.silly(PRE, 'replayImageMsg replaying message: %s', JSON.stringify(payload)) this.emit('message', payload.msgId) } private replayAppMsg (msgId: string, to: string, content: string): void { const payload = this.generateBaseMsg(msgId, to) payload.msgType = PadplusMessageType.App payload.content = `<msg>${content}</msg>` log.silly(PRE, 'replayAppMsg replaying message: %s', JSON.stringify(payload)) this.emit('message', payload.msgId) } public async messageSendUrl (receiver: Receiver, urlLinkPayload: UrlLinkPayload): Promise<void | string> { log.verbose(PRE, `messageSendUrl(${receiver})`, JSON.stringify(receiver), JSON.stringify(urlLinkPayload), ) const contactIdOrRoomId = receiver.roomId || receiver.contactId const { url, title, thumbnailUrl, description } = urlLinkPayload const payload = { description, thumburl: thumbnailUrl, title, type: 5, url, } const urlLinkData = await this.manager.sendUrlLink(this.selfId(), contactIdOrRoomId!, JSON.stringify(payload)) if (PADPLUS_REPLAY_MESSAGE) { this.replayUrlLinkMsg(urlLinkData.msgId, contactIdOrRoomId!, JSON.stringify(payload)) } if (urlLinkData.success) { const msgPayload: PadplusMessagePayload = { content: JSON.stringify(payload), createTime: urlLinkData.timestamp, fromUserName: this.selfId(), imgStatus: 0, l1MsgType: 0, msgId: urlLinkData.msgId, msgSource: 'self', msgSourceCd: 0, msgType: PadplusMessageType.Text, newMsgId: Number(urlLinkData.msgId), pushContent: JSON.stringify(payload), status: 1, toUserName: contactIdOrRoomId!, uin: '', wechatUserName: '', } this.manager.cachePadplusMessagePayload.set(urlLinkData.msgId, msgPayload) } log.silly(PRE, `urlLinkData : ${util.inspect(urlLinkData)}`) return urlLinkData.msgId } private replayUrlLinkMsg (msgId: string, to: string, content: string): void { const payload = this.generateBaseMsg(msgId, to) payload.msgType = PadplusMessageType.App payload.content = content log.silly(PRE, 'replayUrlLinkMsg replaying message: %s', JSON.stringify(payload)) this.emit('message', payload.msgId) } messageSendMiniProgram (receiver: Receiver, miniProgramPayload: MiniProgramPayload): Promise<void | string> { log.silly(PRE, `messageSendMiniProgram() receiver : ${util.inspect(receiver)}, miniProgramPayload: ${miniProgramPayload}`) throw new Error('Method not implemented.') } public async messageRawPayload (messageId: string): Promise<PadplusMessagePayload> { log.verbose(PRE, 'messageRawPayload(%s)', messageId) const rawPayload = await this.manager.cachePadplusMessagePayload.get(messageId) if (!rawPayload) { throw new Error('no message rawPayload') } return rawPayload } public async messageRawPayloadParser (rawPayload: PadplusMessagePayload): Promise<MessagePayload> { log.verbose(PRE, 'messageRawPayloadParser()') const payload = await messageRawPayloadParser(rawPayload) if (payload.mentionIdList && payload.mentionIdList.length === 1 && payload.mentionIdList[0] === 'announcement@all') { const memberIds = await this.roomMemberList(payload.roomId!) payload.mentionIdList = memberIds.filter(m => m !== payload.fromId) payload.text = `${payload.text || ''}` } return payload } /** * ======================== * ROOM SECTION * ======================== */ async onRoomJoinEvent (message: PadplusMessagePayload): Promise<void> { const joinEvent = await roomJoinEventMessageParser(message) if (joinEvent) { log.silly(PRE, `receive join event : ${util.inspect(joinEvent)}`) const inviteeNameList = joinEvent.inviteeNameList const inviterName = joinEvent.inviterName const roomId = joinEvent.roomId const timestamp = joinEvent.timestamp const inviteeIdList = await retry(async (retryException, attempt) => { log.verbose(PRE, 'onPadplusMessageRoomEventJoin({id=%s}) roomJoin retry(attempt=%d)', attempt) const tryIdList = flatten<string>( await Promise.all( inviteeNameList.map( inviteeName => this.roomMemberSearch(roomId, inviteeName), ), ), ) if (tryIdList.length) { return tryIdList } if (!this.manager) { throw new Error('no manager') } /** * Set Cache Dirty */ await this.roomMemberPayloadDirty(roomId) return retryException(new Error('roomMemberSearch() not found')) }).catch(e => { log.verbose(PRE, 'onPadplusMessageRoomEventJoin({id=%s}) roomJoin retry() fail: %s', e.message) return [] as string[] }) let inviterIdList = await this.roomMemberSearch(roomId, inviterName) if (inviterIdList.length < 1) { await this.roomMemberPayloadDirty(roomId) await this.manager.getRoomMembers(roomId) inviterIdList = await this.roomMemberSearch(roomId, inviterName) if (inviterIdList.length < 1) { throw new Error(`can not get room member`) } } else if (inviterIdList.length > 1) { log.verbose(PRE, 'onPadplusMessageRoomEventJoin() inviterId found more than 1, use the first one.') } const inviterId = inviterIdList[0] /** * Set Cache Dirty */ await this.roomMemberPayloadDirty(roomId) await this.roomPayloadDirty(roomId) this.emit('room-join', roomId, inviteeIdList, inviterId, timestamp) } } async onRoomLeaveEvent (message: PadplusMessagePayload): Promise<void> { log.verbose(PRE, 'onRoomLeaveEvent({id=%s})', message.msgId) const leaveEvent = roomLeaveEventMessageParser(message) if (leaveEvent) { log.silly(PRE, `receive remove event : ${util.inspect(leaveEvent)}`) const leaverNameList = leaveEvent.leaverNameList const removerName = leaveEvent.removerName const roomId = leaveEvent.roomId const timestamp = leaveEvent.timestamp const leaverIdList = flatten<string>( await Promise.all( leaverNameList.map( leaverName => this.roomMemberSearch(roomId, leaverName), ), ), ) const removerIdList = await this.roomMemberSearch(roomId, removerName) if (removerIdList.length < 1) { throw new Error('no removerId found') } else if (removerIdList.length > 1) { log.verbose(PRE, 'onPadplusMessageRoomEventLeave(): removerId found more than 1, use the first one.') } const removerId = removerIdList[0] if (!this.manager) { throw new Error('no padplusManager') } /** * Set Cache Dirty */ await this.roomMemberPayloadDirty(roomId) await this.roomPayloadDirty(roomId) this.emit('room-leave', roomId, leaverIdList, removerId, timestamp) } } async onRoomTopicEvent (message: PadplusMessagePayload): Promise<void> { log.verbose(PRE, 'onPadplusMessageRoomEventTopic({id=%s})', message.msgId) const topicEvent = roomTopicEventMessageParser(message) if (topicEvent) { log.silly(PRE, `receive topic event : ${util.inspect(topicEvent)}`) const changerName = topicEvent.changerName const newTopic = topicEvent.topic const roomId = topicEvent.roomId const timestamp = topicEvent.timestamp const roomOldPayload = await this.roomPayload(roomId) const oldTopic = roomOldPayload.topic const changerIdList = await this.roomMemberSearch(roomId, changerName) if (changerIdList.length < 1) { throw new Error('no changerId found') } else if (changerIdList.length > 1) { log.verbose(PRE, 'onPadplusMessageRoomEventTopic() changerId found more than 1, use the first one.') } const changerId = changerIdList[0] if (!this.manager) { throw new Error('no padplusManager') } /** * Set Cache Dirty */ await this.roomPayloadDirty(roomId) if (this.manager && this.manager.cacheManager) { await this.manager.cacheManager.deleteRoom(roomId) } this.emit('room-topic', roomId, newTopic, oldTopic, changerId, timestamp) } } roomInvitationAccept (roomInvitationId: string): Promise<void> { log.silly(PRE, `roomInvitationId : ${util.inspect(roomInvitationId)}`) throw new Error('Method not implemented.') } protected async roomInvitationRawPayload (roomInvitationId: string): Promise<PadplusRoomInvitationPayload> { log.silly(PRE, `roomInvitationId : ${util.inspect(roomInvitationId)}`) if (!this.manager) { throw new Error(`no manager.`) } const payload = await this.manager.roomInvitationRawPayload(roomInvitationId) return payload } protected async roomInvitationRawPayloadParser (rawPayload: PadplusRoomInvitationPayload): Promise<RoomInvitationPayload> { log.silly(PRE, `room invitation rawPayload : ${util.inspect(rawPayload)}`) const payload: RoomInvitationPayload = { id: rawPayload.id, inviterId: rawPayload.fromUser, roomMemberCount: 0, roomMemberIdList: [], roomTopic: rawPayload.roomName, timestamp: rawPayload.timestamp, } return payload } public async roomAdd (roomId: string, contactId: string): Promise<void> { log.silly(PRE, `roomId : ${util.inspect(roomId)}, contactId: ${contactId}`) if (!this.manager) { throw new Error(`no manager.`) } await this.manager.roomAddMember(roomId, contactId) } public async roomAvatar (roomId: string): Promise<FileBox> { log.silly(PRE, `roomAvatar(roomId : ${util.inspect(roomId)})`) if (!this.manager) { throw new Error(`no manager.`) } const room = await this.roomRawPayload(roomId) if (room) { const avatarUrl = room.bigHeadUrl || room.smallHeadUrl return FileBox.fromUrl(avatarUrl, `${roomId}_avatar_${Date.now()}.png`) } else { throw new Error(`Can not load room info by roomId : ${roomId}`) } } public async roomCreate (contactIdList: string[], topic?: string | undefined): Promise<string> { log.silly(PRE, `topic : ${topic}, contactIdList: ${contactIdList.join(',')}`) if (!this.manager) { throw new Error(`no manager.`) } const result = await this.manager.createRoom(topic || '', contactIdList) return result } public async roomDel (roomId: string, contactId: string): Promise<void> { log.verbose(PRE, `roomDel(${roomId}, ${contactId})`) const memberIdList = await this.roomMemberList(roomId) if (memberIdList.includes(contactId)) { await this.manager.deleteRoomMember(roomId, contactId) } else { log.verbose(PRE, `roomDel() room(${roomId}) has no member contact(${contactId})`) } } public async roomQuit (roomId: string): Promise<void> { log.silly(PRE, `roomId : ${util.inspect(roomId)}`) if (!this.manager) { throw new Error(`no manager.`) } await this.manager.quitRoom(roomId) } public async roomTopic (roomId: string): Promise<string> public async roomTopic (roomId: string, topic: string): Promise<void> public async roomTopic (roomId: string, topic?: string | undefined): Promise<string | void> public async roomTopic (roomId: string, topic?: any): Promise<string | void> { log.silly(PRE, `roomId : ${roomId}, topic: ${topic}`) if (typeof topic === 'undefined') { const room = await this.roomPayload(roomId) return room && (room.topic || '') } if (!this.manager) { throw new Error(`no manager.`) } await this.manager.setRoomTopic(roomId, topic as string) await this.roomPayloadDirty(roomId) await new Promise(resolve => setTimeout(resolve, 500)) await this.roomTopic(roomId) } async roomQrcode (roomId: string): Promise<string> { log.silly(PRE, `roomId : ${util.inspect(roomId)}`) if (!this.manager) { throw new Error(`no manager.`) } const roomQrcode = await this.manager.getRoomQrcode(roomId) return roomQrcode } async roomList (): Promise<string[]> { log.verbose(PRE, `roomList()`) if (!this.manager) { throw new Error(`no manager.`) } const roomIds = await this.manager.getRoomIdList() return roomIds } async roomMemberList (roomId: string): Promise<string[]> { log.silly(PRE, `roomMemberList(), roomId : ${util.inspect(roomId)}`) if (!this.manager) { throw new Error(`no manager.`) } const roomIds = await this.manager.getRoomMemberIdList(roomId) return roomIds } protected async roomRawPayload (roomId: string): Promise<PadplusRoomPayload> { log.silly(PRE, `roomRawPayload(), roomId : ${roomId}`) const rawRoom = await this.manager.getRoomInfo(roomId) return rawRoom } protected async roomRawPayloadParser (rawPayload: PadplusRoomPayload): Promise<RoomPayload> { log.silly(PRE, `roomRawPayloadParser()`) const room = roomRawPayloadParser(rawPayload) return room } protected async roomMemberRawPayload (roomId: string, contactId: string): Promise<PadplusRoomMemberPayload> { log.silly(PRE, `roomId : ${roomId}, contactId: ${contactId}`) if (!this.manager) { throw new Error(`no manager.`) } const memberMap = await this.manager.getRoomMembers(roomId) if (!memberMap) { throw new Error('can not find members. may be you are removed.') } const member = memberMap[contactId] return member } protected async roomMemberRawPayloadParser (rawPayload: PadplusRoomMemberPayload): Promise<RoomMemberPayload> { log.silly(PRE, `room member rawPayload : ${util.inspect(rawPayload)}`) const member = convertToPuppetRoomMember(rawPayload) return member } roomAnnounce (roomId: string): Promise<string> roomAnnounce (roomId: string, text: string): Promise<void> async roomAnnounce (roomId: any, text?: any): Promise<string | void> { log.silly(PRE, `roomId : ${util.inspect(roomId)}, text: ${text}`) if (!this.manager) { throw new Error(`no manager.`) } if (text) { return this.manager.setAnnouncement(roomId, text) } else { return this.manager.getAnnouncement(roomId) } } public ding (data?: string): void { log.silly(PRE, 'ding(%s)', data || '') this.emit('dong', data) } } export default PuppetPadplus