UNPKG

wechaty-puppet-wechat4u

Version:
1,173 lines (996 loc) 36.6 kB
/** * Wechaty - https://github.com/chatie/wechaty * * @copyright 2016-2018 Huan LI <zixia@zixia.net> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ import Wechat4u from 'wechat4u' import QuickLru from '@alloc/quick-lru' import * as PUPPET from 'wechaty-puppet' import { log } from 'wechaty-puppet' import type { FileBoxInterface } from 'file-box' import { FileBox } from 'file-box' import { GError } from 'gerror' import { qrCodeForChatie, retry, VERSION, NAME, } from './config.js' import { WebContactRawPayload, WebMessageRawPayload, WebMessageType, WebRoomRawMember, WebRoomRawPayload, } from './web-schemas.js' import { parseEvent, EventType } from './wechat4u/events/mod.js' import { webMessageToWechaty } from './wechat4u/schema-mapper/message.js' // 解析小程序数据格式 import { parseMiniProgramMessagePayload } from './wechat4u/messages/message-miniprogram.js' // 解析appmsg 数据格式 import { parseAppmsgMessagePayload } from './wechat4u/messages/message-appmsg.js' // 解析表情数据格式 import { parseEmotionMessagePayload } from './wechat4u/messages/message-emotion.js' import { wechat4uContactToWechaty } from './wechat4u/schema-mapper/contact.js' import { wechat4uRoomMemberToWechaty, wechat4uRoomToWechaty } from './wechat4u/schema-mapper/room.js' import { isRoomId } from './wechat4u/utils/is-type.js' const MEMORY_SLOT_NAME = 'PUPPET-WECHAT4U' export class PuppetWechat4u extends PUPPET.Puppet { static override readonly VERSION = VERSION /** * Wecaht4u * * Code from: * https://github.com/nodeWechat/wechat4u/blob/46931e78bcb56899b8d2a42a37b919e7feaebbef/run-core.js * */ private wechat4u?: any private scanQrCode?: string // 启动时间 为了处理历史消息 private startTime: number = 0 private unknownContactId: string[][] = [] private getContactInterval: undefined | NodeJS.Timeout private _heartBeatTimer?: ReturnType<typeof setTimeout> private readonly cacheMessageRawPayload: QuickLru<string, WebMessageRawPayload> constructor ( override options: PUPPET.PuppetOptions = {}, ) { super(options) const lruOptions: QuickLru.Options<string, any> = { maxAge: 1000 * 60 * 60, maxSize: 10000, onEviction (key: string, val: object) { log.silly('PuppetWechat4u', 'constructor() lruOptions.dispose(%s, %s)', key, JSON.stringify(val)) }, } this.cacheMessageRawPayload = new QuickLru<string, WebMessageRawPayload>(lruOptions) } override version () { return `${VERSION}<${super.version()}>` } override name () { return `${NAME}<${super.name()}>` } override async onStart (): Promise<void> { log.verbose('PuppetWechat4u', 'onStart() with %s', this.memory.name || 'NONAME') if (this.wechat4u) { log.warn('PuppetWechat4u', 'onStart() wechat4u exist, will be overwrited') } this.startTime = parseInt(String(new Date().getTime() / 1000)) /** * Huan(202110): rename `onStart()` to `tryStart()` * then we will be able to use `MemoryMixin` * to init MemoryCard for the child puppet */ try { await this.memory.load() } catch (_) {} // console.info('faint 1') const syncData = await this.memory.get(MEMORY_SLOT_NAME) // console.info('faint 2') if (syncData) { this.wechat4u = new Wechat4u(syncData) } else { this.wechat4u = new Wechat4u() } this.monkeyPatch(this.wechat4u) this.initHookEvents(this.wechat4u) /** * Should not `await` onStart/restart for wechat4u * because it will blocks... */ if (this.wechat4u.PROP.uin) { // 存在登录数据时,可以随时调用restart进行重启 this.wechat4u.restart() } else { this.wechat4u.start() } } /** * At present, if a user information that does not exist is found, it will be called once. * If it is a group message, it will send a lot of information requests, and finally most of the interface requests will fail. * At present, the method of timer is used to regularly obtain user information * 1、A timer is started when the search request is triggered for the first time * 2、All requested unknown user ids will be stored in unknownContactId * 3、The timer will be executed once every 500ms, each time fetching 50 pieces of data in unknownContactId * 4、If the data of unknownContactId is empty, the timer will be cleared and wait for the next establishment * @private */ private getContactsInfo () { const tempArray: string[][] = this.unknownContactId.splice(0, 40) if (tempArray.length === 0 && this.getContactInterval) { clearInterval(this.getContactInterval) this.getContactInterval = undefined } if (tempArray.length) { const userDataList = tempArray.map(contact => { return { EncryChatRoomId : contact[1], UserName : contact[0], } }) this.wechat4u.batchGetContact(userDataList).then((result: any[]) => { result.forEach((item) => { if (isRoomId(item.UserName)) { const membersList = item.MemberList.map((mItem: any) => { return { ...mItem, EncryChatRoomId: item.UserName, } }) this.wechat4u.updateContacts(membersList) } }) this.wechat4u.updateContacts(result) return null }).catch((e: any) => { log.warn('PuppetWechat4u', 'contactRawPayload(%s) wechat4u.batchGetContact() exception: %s', e) }) } } private monkeyPatch (wechat4u: any) { log.silly('PuppetWechat4u', 'monkeyPatch()') // fake wechat4u to think as we had logined.) this.monkeyPatchOffState(wechat4u, 'checkLogin', Promise.resolve({ code: 200 })) this.monkeyPatchOffState(wechat4u, 'login', Promise.resolve()) this.monkeyPatchOffState(wechat4u, '_init', Promise.resolve()) this._startPuppetHeart(true, wechat4u) /** * Disable Wechat4u for Sending Message to Filehelper when Heartbeat. */ // tslint:disable-next-line wechat4u.setPollingTargetGetter(() => { return '' }) wechat4u.setPollingMessageGetter(() => { return '' }) // 自定义心跳间隔(以毫秒为单位) // 25 days: https://stackoverflow.com/a/12633556/1123955 // this.wechat4u.setPollingIntervalGetter(() => Math.pow(2,31) - 1) } // 开始监听心跳 private _startPuppetHeart (firstTime: boolean = true, wechat4u: any) { if (firstTime && this._heartBeatTimer) { return } let status: string|undefined = wechat4u.state if (status === wechat4u.CONF.STATE.login) { status = 'normal' } else if (status === wechat4u.CONF.STATE.logout) { status = 'logout' } else if (status === wechat4u.CONF.STATE.init) { status = 'init' } else if (status === wechat4u.CONF.STATE.uuid) { status = 'uuid' } this.emit('heartbeat', { data: `heartbeat@puppet-wechat4u:${status}` }) // eslint-disable-next-line @typescript-eslint/no-misused-promises this._heartBeatTimer = setTimeout(async (): Promise<void> => { await this._startPuppetHeart(false, wechat4u) return undefined }, 15 * 1000) // 15s } /** * Monkey Patch for Wechat4u * - https://www.audero.it/blog/2016/12/05/monkey-patching-javascript/#what-is-monkey-patching * * What is Monkey patching? * Monkey patching is a technique to add, modify, or suppress * the default behavior of a piece of code at runtime * without changing its original source code. */ private monkeyPatchOffState (wechat4u: any, func: string, valueWhenLogouted: any): void { log.verbose('PuppetWechat4u', 'monkeyPatchOffState(wechat4u, %s)', func) const puppetThis = this const funcOrig = wechat4u[func] function funcNew (this: any) { log.verbose('PuppetWechat4u', 'monkeyPatchOffState(%s) funcNew()', func) if (puppetThis.state.inactive()) { log.verbose('PuppetWechat4u', 'monkeyPatchOffState(%s) funcNew() state.off() is true, return', func) return valueWhenLogouted } return funcOrig.call(this) } wechat4u[func] = funcNew } /** * @private * For issue https://github.com/wechaty/puppet-wechat/issues/107 */ private async waitStable (): Promise<void> { log.verbose('PuppetWeChat', 'waitStable()') let maxNum = 0 let curNum = 0 let unchangedNum = 0 const SLEEP_SECOND = 4 const STABLE_CHECK_NUM = 5 while (unchangedNum < STABLE_CHECK_NUM) { // wait 1 second await new Promise(resolve => setTimeout(resolve, SLEEP_SECOND * 1000)) const contactList = await this.contactList() curNum = contactList.length if (curNum > 0 && curNum === maxNum) { unchangedNum++ } else /* curNum < maxNum */ { unchangedNum = 0 } if (curNum > maxNum) { maxNum = curNum } log.silly('PuppetWeChat', 'readyStable() while() curNum=%s, maxNum=%s, unchangedNum=%s', curNum, maxNum, unchangedNum, ) } log.verbose('PuppetWeChat', 'readyStable() emit(ready)') this.emit('ready', { data: 'stable' }) } private initHookEvents (wechat4u: any) { log.verbose('PuppetWechat4u', 'initHookEvents()') /** * uuid事件,参数为uuid,根据uuid生成二维码 */ this.wechat4u.on('uuid', (uuid: string) => { log.silly('PuppetWechat4u', 'initHookEvents() wechat4u.on(uuid)') this.scanQrCode = 'https://login.weixin.qq.com/l/' + uuid this.emit('scan', { qrcode: this.scanQrCode, status: PUPPET.types.ScanStatus.Waiting }) }) /** * 登录用户头像事件,手机扫描后可以得到登录用户头像的Data URL */ wechat4u.on('user-avatar', (avatarDataUrl: string) => { this.emit('scan', { data: avatarDataUrl, qrcode: this.scanQrCode || '', status: PUPPET.types.ScanStatus.Scanned, }) }) /** * 登录成功事件 */ wechat4u.on('login', async () => { // 由于初始好友列表没有存储当前bot的基础信息,所以在登录后直接更新当前机器人的信息 this.wechat4u.updateContacts([ this.wechat4u.user ]) this.startTime = parseInt(String(new Date().getTime() / 1000)) // FIXME: where's the logined user id? const userId = this.wechat4u.user.UserName if (!userId) { this.emit('error', { data: GError.stringify( new Error('login event can not found selfId'), ), }) return } // we do not wait `ready` before emit `login` this.waitStable().catch(e => { log.error('PuppetWeChatEvent', 'onLogin() this.waitStable() rejection: %s', e && (e as Error).message) }) await this.login(userId) // 保存数据,将数据序列化之后保存到任意位置 await this.memory.set(MEMORY_SLOT_NAME, wechat4u.botData) await this.memory.save() }) /** * 登出成功事件 */ wechat4u.on('logout', async () => { log.silly('PuppetWechat4u', 'initHookEvents() wechat4u.on(logout)') if (this.isLoggedIn) { await this.logout() } // 清除数据 await this.memory.delete(MEMORY_SLOT_NAME) await this.memory.save() this.wechat4u.start() }) /** * 联系人更新事件,参数为被更新的联系人列表 */ wechat4u.on('contacts-updated', (contacts: WebContactRawPayload[]) => { log.silly('PuppetWechat4u', 'initHookEvents() wechat4u.on(contacts-updated) new/total contacts.length=%d/%d', contacts.length, Object.keys(wechat4u.contacts).length, ) contacts.forEach((item: any) => { if (isRoomId(item.UserName)) { const membersList = item.MemberList.map((mItem: any) => { this.unknownContactId.push([ mItem.UserName, item.UserName ]) return { ...mItem, EncryChatRoomId: item.UserName, } }) this.wechat4u.updateContacts(membersList) } }) if (!this.getContactInterval) { this.getContactsInfo() this.getContactInterval = setInterval(() => { this.getContactsInfo() }, 2000) } }) /** * 错误事件,参数一般为Error对象 */ wechat4u.on('error', (err: Error) => { this.emit('error', { data: GError.stringify(err), }) }) /** * 如何处理会话消息 */ wechat4u.on('message', async (msg: WebMessageRawPayload) => { if (!msg.MsgId) { log.warn('PuppetWechat4u', 'initHookEvents() wechat4u.on(message) no message id: %s', JSON.stringify(msg)) throw new Error('no id') } // 如果是消息的创建时间小于机器人启动的时间 直接丢弃 if (msg.CreateTime < this.startTime) { // log.warn('PuppetWechat4u', 'initHookEvents() wechat4u.on(message) is history message: %s', JSON.stringify(msg)) return } this.cacheMessageRawPayload.set(msg.MsgId, msg) const event = await parseEvent(this, msg) switch (event.type) { case EventType.Message: this.emit('message', { messageId: msg.MsgId }) break case EventType.Friendship: { this.emit('friendship', { friendshipId: msg.MsgId, }) break } case EventType.RoomInvite: { this.emit('room-invite', { roomInvitationId: msg.MsgId, }) break } case EventType.RoomJoin: { const roomJoin: PUPPET.payloads.EventRoomJoin = event.payload this.emit('room-join', roomJoin) break } case EventType.RoomLeave: { const roomLeave: PUPPET.payloads.EventRoomLeave = event.payload this.emit('room-leave', roomLeave) break } case EventType.RoomTopic: { const roomTopic: PUPPET.payloads.EventRoomTopic = event.payload this.emit('room-topic', roomTopic) break } } }) } override async onStop (): Promise<void> { log.verbose('PuppetWechat4u', 'onStop()') this.wechat4u.stop() this.wechat4u = undefined if (this._heartBeatTimer) { clearTimeout(this._heartBeatTimer) this._heartBeatTimer = undefined } } override async ding (data: string): Promise<void> { log.silly('PuppetWechat4u', 'ding(%s)', data || '') this.emit('dong', { data }) } /** * * ContactSelf * * */ override async contactSelfQRCode (): Promise<string> { return PUPPET.throwUnsupportedError() } override async contactSelfName (name: string): Promise<void> { return PUPPET.throwUnsupportedError(name) } override async contactSelfSignature (signature: string): Promise<void> { return PUPPET.throwUnsupportedError(signature) } /** * * Contact * */ override contactAlias (contactId: string) : Promise<string> override contactAlias (contactId: string, alias: null | string): Promise<void> override async contactAlias (contactId: string, alias?: null | string): Promise<void | string> { log.verbose('PuppetWechat4u', 'contactAlias(%s, %s)', contactId, alias) if (typeof alias === 'undefined') { const payload = await this.contactPayload(contactId) return payload.alias } await this.wechat4u.updateRemarkName(contactId, alias) } override async contactList (): Promise<string[]> { log.verbose('PuppetWechat4u', 'contactList()') const idList = Object.keys(this.wechat4u.contacts) .filter((contact: any) => !this.wechat4u.Contact.isRoomContact(this.wechat4u.contacts[contact])) return idList } // override async contactQrCode (contactId: string): Promise<string> { // return PUPPET.throwUnsupportedError(contactId) // } override async contactAvatar (contactId: string) : Promise<FileBoxInterface> override async contactAvatar (contactId: string, file: FileBoxInterface) : Promise<void> override async contactAvatar (contactId: string, file?: FileBoxInterface): Promise<void | FileBoxInterface> { log.verbose('PuppetWechat4u', 'contactAvatar(%s)', contactId) if (file) { return PUPPET.throwUnsupportedError() } const rawPayload = await this.contactRawPayload(contactId) const payload = await this.contactPayload(contactId) const name = payload.name // add '&type=big' to get big image if (rawPayload.HeadImgUrl) { const res = await this.wechat4u.getHeadImg(rawPayload.HeadImgUrl + '&type=big') /** * 如何获取联系人头像 */ return FileBox.fromBuffer( res.data, `wechaty-contact-avatar-${name}.jpg`, // FIXME ) } } override async contactRawPayload (contactId: string): Promise<WebContactRawPayload> { log.verbose('PuppetWechat4u', 'contactRawPayload(%s) with contacts.length=%d', contactId, Object.keys(this.wechat4u.contacts).length, ) if (!(contactId in this.wechat4u.contacts)) { this.unknownContactId.push([ contactId, '' ]) if (!this.getContactInterval) { this.getContactsInfo() this.getContactInterval = setInterval(() => { this.getContactsInfo() }, 2000) } } const rawPayload: WebContactRawPayload = await retry<WebContactRawPayload>(async (retryException, attempt) => { log.verbose('PuppetWechat4u', 'contactRawPayload(%s) retry() attempt=%d', contactId, attempt) if (contactId in this.wechat4u.contacts) { return this.wechat4u.contacts[contactId] } retryException(new Error('no this.wechat4u.contacts[' + contactId + ']')) }) return rawPayload } override async contactRawPayloadParser (rawPayload: WebContactRawPayload): Promise<PUPPET.payloads.Contact> { return wechat4uContactToWechaty(rawPayload) } /** * * Message * */ override async messageContact ( messageId: string, ): Promise<string> { log.verbose('PuppetWechat4u', 'messageContact(%s)', messageId) return PUPPET.throwUnsupportedError() } // web支持撤回消息 https://github.com/nodeWechat/wechat4u/blob/8e20b34507dbe783ada8c769b72ef1792f33c94a/src/core.js#L1219 override async messageRecall ( messageId: string, ): Promise<boolean> { log.verbose('PuppetWechat4u', 'messageRecall(%s)', messageId) const rawPayload = await this.messageRawPayload(messageId) this.wechat4u.revokeMsg(messageId, rawPayload.ToUserName) return true } override async messageImage ( messageId: string, imageType: PUPPET.types.Image, ) : Promise<FileBoxInterface> { log.verbose('PuppetWechat4u', 'messageImage(%s, %s[%s])', messageId, imageType, PUPPET.types.Image[imageType], ) /** * 图片消息 */ // console.log('图片消息,保存到本地') const filename = `${messageId}.jpg` const msg = await this.wechat4u.getMsgImg(messageId) const file = FileBox.fromStream( msg.data, filename, ) return file } override async messageFile (id: string): Promise<FileBoxInterface> { log.verbose('PuppetWechat4u', 'messageFile(%s)', id) const payload = await this.messagePayload(id) const rawPayload = await this.messageRawPayload(id) let filename = payload.filename || 'unknown.txt' /** * 判断消息类型 */ switch (payload.type) { case PUPPET.types.Message.Text: /** * 文本消息 */ throw new Error('msg type is text') case PUPPET.types.Message.Emoticon: { /** * 表情消息 */ const emotionPayload = await parseEmotionMessagePayload(rawPayload) const emoticonBox = FileBox.fromUrl(emotionPayload.cdnurl, { name: `message-${id}-emoticon.jpg` }) emoticonBox.metadata = { payload: emotionPayload, type: 'emoticon', } return emoticonBox } // eslint-disable-next-lint no-fallthrough case PUPPET.types.Message.Image:{ /** * 图片消息 */ // console.log('图片消息,保存到本地') filename = `${rawPayload.MsgId}.jpg` const msg = await this.wechat4u.getMsgImg(rawPayload.MsgId) const file = FileBox.fromBuffer( msg.data, filename, ) return file } case PUPPET.types.Message.Audio: { /** * 语音消息 */ const audioFileBox = FileBox.fromBuffer( (await this.wechat4u.getVoice(rawPayload.MsgId)).data, `message-${id}-audio.sil`, ) const voiceLength = rawPayload.VoiceLength audioFileBox.metadata = { voiceLength, } // console.log('语音消息,保存到本地') return audioFileBox } case PUPPET.types.Message.Video: /** * 视频消息 */ // console.log('视频消息,保存到本地') return FileBox.fromBuffer( (await this.wechat4u.getVideo(rawPayload.MsgId)).data, `message-${id}-video.mp4`, ) case PUPPET.types.Message.Attachment: if (rawPayload.AppMsgType === 6) { /** * 文件消息 */ // console.log('文件消息,保存到本地') filename = rawPayload.FileName return FileBox.fromBuffer( (await this.wechat4u.getDoc(rawPayload.FromUserName, rawPayload.MediaId, rawPayload.FileName)).data, filename, ) } break default: break } throw new Error('unsupported message. id: ' + id) } override async messageUrl (messageId: string) : Promise<PUPPET.payloads.UrlLink> { log.verbose('PuppetWechat4u', 'messageUrl(%s)', messageId) const rawPayload: WebMessageRawPayload | undefined = this.cacheMessageRawPayload.get(messageId) if (!rawPayload) { throw new Error('id not found') } const message = await this.messageRawPayloadParser(rawPayload) if (message.type !== PUPPET.types.Message.Url) { throw new Error('Can not get url from non url payload') } const appPayload = await parseAppmsgMessagePayload(rawPayload.Content) return { description: appPayload.des, thumbnailUrl: appPayload.thumburl, title: appPayload.title, url: appPayload.url, } } override async messageMiniProgram (messageId: string): Promise<PUPPET.payloads.MiniProgram> { log.verbose('PuppetWechat4u', 'messageMiniProgram(%s)', messageId) const rawPayload: WebMessageRawPayload | undefined = this.cacheMessageRawPayload.get(messageId) if (!rawPayload) { throw new Error('id not found') } const message = await this.messageRawPayloadParser(rawPayload) if (message.type !== PUPPET.types.Message.MiniProgram) { throw new Error('message is not mini program, can not get MiniProgramPayload') } return parseMiniProgramMessagePayload(rawPayload) } override async messageRawPayload (id: string): Promise<WebMessageRawPayload> { log.verbose('PuppetWechat4u', 'messageRawPayload(%s)', id) const rawPayload = this.cacheMessageRawPayload.get(id) if (!rawPayload) { throw new Error('id not found') } return rawPayload } override async messageRawPayloadParser ( rawPayload: WebMessageRawPayload, ): Promise<PUPPET.payloads.Message> { log.verbose('PuppetWechat4u', 'messageRawPayloadParser(%s) @ %s', rawPayload, this) // console.log(rawPayload) const payload = webMessageToWechaty(this, rawPayload) return payload } override async messageSendText ( conversationId : string, text : string, ): Promise<void> { log.verbose('PuppetWechat4u', 'messageSend(%s, %s)', conversationId, text) /** * 发送文本消息,可以包含emoji(😒)和QQ表情([坏笑]) */ await this.wechat4u.sendMsg(text, conversationId) /** * { BaseResponse: { Ret: 0, ErrMsg: '' }, * MsgID: '830582407297708303', * LocalID: '15279119663740094' } */ } override async messageSendFile ( conversationId : string, file : FileBox, ): Promise<void> { log.verbose('PuppetWechat4u', 'messageSend(%s, %s)', conversationId, file) /** * 通过表情MD5发送表情 */ // wechat4u.sendMsg({ // emoticonMd5: '00c801cdf69127550d93ca52c3f853ff' // }, ToUserName) // .catch(err => { // bot.emit('error', err) // }) /** * 以下通过上传文件发送图片,视频,附件等 * 通用方法为入下 * file为多种类型 * filename必填,主要为了判断文件类型 */ await this.wechat4u.sendMsg({ file : await file.toStream(), filename : file.name, }, conversationId) } override async messageSendContact ( conversationId : string, contactId : string, ): Promise<void> { log.verbose('PuppetWechat4u', 'messageSend("%s", %s)', conversationId, contactId) PUPPET.throwUnsupportedError() } override async messageSendUrl (conversationId: string, urlLinkPayload: PUPPET.payloads.UrlLink) : Promise<void> { PUPPET.throwUnsupportedError(conversationId, urlLinkPayload) } override async messageSendMiniProgram (conversationId: string, miniProgramPayload: PUPPET.payloads.MiniProgram): Promise<void> { log.verbose('PuppetWechat4u', 'messageSendMiniProgram("%s", %s)', JSON.stringify(conversationId), JSON.stringify(miniProgramPayload), ) PUPPET.throwUnsupportedError(conversationId, miniProgramPayload) } override async messageForward ( conversationid : string, messageId : string, ): Promise<void> { log.verbose('PuppetWechat4u', 'messageForward(%s, %s)', conversationid, messageId, ) const rawPayload = await this.messageRawPayload(messageId) /** * 如何直接转发消息 */ await this.wechat4u.forwardMsg(rawPayload, conversationid) } override async conversationReadMark ( conversationId: string, hasRead?: boolean, ) : Promise<void | boolean> { return PUPPET.throwUnsupportedError(conversationId, hasRead) } /** * * Room Invitation * */ override async roomInvitationAccept (roomInvitationId: string): Promise<void> { return PUPPET.throwUnsupportedError(roomInvitationId) } override async roomInvitationRawPayload (roomInvitationId: string): Promise<any> { return PUPPET.throwUnsupportedError(roomInvitationId) } override async roomInvitationRawPayloadParser (rawPayload: any): Promise<PUPPET.payloads.RoomInvitation> { return PUPPET.throwUnsupportedError(rawPayload) } /** * * Room * */ override async roomRawPayload ( id: string, ): Promise<WebRoomRawPayload> { log.verbose('PuppetWechat4u', 'roomRawPayload(%s)', id) const rawPayload: WebRoomRawPayload = await retry<WebRoomRawPayload>((retryException, attempt) => { log.verbose('PuppetWechat4u', 'contactRawPayload(%s) retry() attempt=%d', id, attempt) if (!this.wechat4u.contacts[id]) { retryException(new Error('no this.wechat4u.contacts[' + id + ']')) } return this.wechat4u.contacts[id] }) return rawPayload } override async roomRawPayloadParser ( rawPayload: WebRoomRawPayload, ): Promise<PUPPET.payloads.Room> { return wechat4uRoomToWechaty(rawPayload) } override async roomList (): Promise<string[]> { log.verbose('PuppetWechat4u', 'roomList()') const idList = Object.keys(this.wechat4u.contacts) .filter((contact: any) => this.wechat4u.Contact.isRoomContact(this.wechat4u.contacts[contact])) return idList } override async roomDel ( roomId : string, contactId : string, ): Promise<void> { log.verbose('PuppetWechat4u', 'roomDel(%s, %s)', roomId, contactId) const type = 'delmember' // XXX: [contactId] or [{ UserName: id }, ...] ? await this.wechat4u.updateChatroom(roomId, [ contactId ], type) } override async roomAvatar (roomId: string): Promise<FileBoxInterface> { log.verbose('PuppetWechat4u', 'roomAvatar(%s)', roomId) const payload = await this.roomPayload(roomId) if (payload.avatar) { // FIXME: set http headers with cookies return FileBox.fromUrl(payload.avatar) } log.warn('PuppetWechat4u', 'roomAvatar() avatar not found, use the chatie default.') return qrCodeForChatie() } override async roomAdd ( roomId : string, contactId : string, ): Promise<void> { log.verbose('PuppetWechat4u', 'roomAdd(%s, %s)', roomId, contactId) const roomPayload = await this.roomPayload(roomId) // TODO: if the room owner enabled "invite only?" let type = 'addmember' // invitemember ??? if (roomPayload.memberIdList.length > 40) { type = 'invitemember' } // https://github.com/nodeWechat/wechat4u/tree/46931e78bcb56899b8d2a42a37b919e7feaebbef#botupdatechatroomchatroomusername-memberlist-fun const ret = await this.wechat4u.updateChatroom(roomId, [ contactId ], type) log.verbose('PuppetWechat4u', 'roomAdd(%s, %s) ret: %s', roomId, contactId, JSON.stringify(ret)) } override async roomTopic (roomId: string) : Promise<string> override async roomTopic (roomId: string, topic: string) : Promise<void> override async roomTopic ( roomId: string, topic?: string, ): Promise<void | string> { log.verbose('PuppetWechat4u', 'roomTopic(%s, %s)', roomId, topic) const roomPayload = await this.roomPayload(roomId) if (typeof topic === 'undefined') { return roomPayload.topic } await this.wechat4u.updateChatRoomName(roomId, topic) } override async roomCreate ( contactIdList : string[], topic : string, ): Promise<string> { log.verbose('PuppetWechat4u', 'roomCreate(%s, %s)', contactIdList, topic) const memberList = contactIdList.map(id => ({ UserName: id })) const roomId = await this.wechat4u.createChatroom(topic, memberList) return roomId } override async roomAnnounce (roomId: string) : Promise<string> override async roomAnnounce (roomId: string, text: string) : Promise<void> override async roomAnnounce (roomId: string, text?: string) : Promise<void | string> { return PUPPET.throwUnsupportedError(roomId, text) } override async roomQuit (roomId: string): Promise<void> { return PUPPET.throwUnsupportedError(roomId) } override async roomQRCode (roomId: string): Promise<string> { return PUPPET.throwUnsupportedError(roomId) } override async roomMemberList (roomId: string) : Promise<string[]> { log.verbose('PuppetWechat4u', 'roommemberList(%s)', roomId) const rawPayload = await this.roomRawPayload(roomId) const memberIdList = (rawPayload.MemberList || []) .map(member => member.UserName) return memberIdList } override async roomMemberRawPayload (roomId: string, contactId: string): Promise<WebRoomRawMember> { log.verbose('PuppetWechat4u', 'roomMemberRawPayload(%s, %s)', roomId, contactId) const rawPayload = await this.roomRawPayload(roomId) const memberPayloadList = rawPayload.MemberList || [] const memberPayloadResult = memberPayloadList.filter(payload => payload.UserName === contactId) if (memberPayloadResult.length > 0) { return memberPayloadResult[0]! } else { throw new Error('not found') } } override async roomMemberRawPayloadParser (rawPayload: WebRoomRawMember): Promise<PUPPET.payloads.RoomMember> { return wechat4uRoomMemberToWechaty(rawPayload) } /** * * Friendship * */ override async friendshipSearchPhone ( phone: string, ): Promise<null | string> { log.verbose('PuppetWechat4u', 'friendshipSearchPhone(%s)', phone) return PUPPET.throwUnsupportedError() } override async friendshipSearchWeixin ( weixin: string, ): Promise<null | string> { log.verbose('PuppetWechat4u', 'friendshipSearchWeixin(%s)', weixin) return PUPPET.throwUnsupportedError() } override async friendshipAdd ( contactId : string, hello : string, ): Promise<void> { log.verbose('PuppetWechat4u', 'friendshipAdd(%s, %s)', contactId, hello) return PUPPET.throwUnsupportedError() // await this.wechat4u.addFriend(contactId, hello) } override async friendshipAccept ( friendshipId : string, ): Promise<void> { log.verbose('PuppetWechat4u', 'friendshipAccept(%s)', friendshipId) const payload = await this.friendshipPayload(friendshipId) as any as PUPPET.payloads.FriendshipReceive await this.wechat4u.verifyUser(payload.contactId, payload.ticket) } override async friendshipRawPayload (id: string): Promise<any> { log.verbose('PuppetWechat4u', 'friendshipRawPayload(%s)', id) const rawPayload = this.cacheMessageRawPayload.get(id) if (!rawPayload) { throw new Error('no rawPayload') } return rawPayload } override async friendshipRawPayloadParser (rawPayload: any) : Promise<PUPPET.payloads.Friendship> { log.verbose('PuppetWechat4u', 'friendshipRawPayloadParser(%s)', rawPayload) const timestamp = Math.floor(Date.now() / 1000) // in seconds switch (rawPayload.MsgType) { case WebMessageType.VERIFYMSG: { const recommendInfo = rawPayload.RecommendInfo if (!recommendInfo) { throw new Error('no recommendInfo') } const payloadReceive: PUPPET.payloads.FriendshipReceive = { contactId : recommendInfo.UserName, hello : recommendInfo.Content, id : rawPayload.MsgId, ticket : recommendInfo.Ticket, timestamp, type : PUPPET.types.Friendship.Receive, } return payloadReceive } case WebMessageType.SYS: { const payloadConfirm: PUPPET.payloads.FriendshipConfirm = { contactId : rawPayload.FromUserName, id : rawPayload.MsgId, timestamp, type : PUPPET.types.Friendship.Confirm, } return payloadConfirm } default: throw new Error('not supported friend request message raw payload') } } /** * * Tag * */ override async tagContactAdd ( tagId: string, contactId: string, ): Promise<void> { log.verbose('PuppetWechat4u', 'tagContactAdd(%s)', tagId, contactId) } override async tagContactRemove ( tagId: string, contactId: string, ): Promise<void> { log.verbose('PuppetWechat4u', 'tagContactRemove(%s)', tagId, contactId) } override async tagContactDelete ( tagId: string, ): Promise<void> { log.verbose('PuppetWechat4u', 'tagContactDelete(%s)', tagId) } override async tagContactList ( contactId?: string, ): Promise<string[]> { log.verbose('PuppetWechat4u', 'tagContactList(%s)', contactId) return [] } override contactCorporationRemark (..._: any[]) { return PUPPET.throwUnsupportedError() } override contactDescription (..._: any[]) { return PUPPET.throwUnsupportedError() } override contactPhone (..._: any[]) { return PUPPET.throwUnsupportedError() } override async messageLocation (messageId: string): Promise<PUPPET.payloads.Location> { return PUPPET.throwUnsupportedError(messageId) } override async messageSendLocation ( conversationId: string, locationPayload: PUPPET.payloads.Location, ): Promise<void | string> { return PUPPET.throwUnsupportedError(conversationId, locationPayload) } } export default PuppetWechat4u