UNPKG

wechaty-lab

Version:

Wechaty is a RPA SDK for Chatbot Makers.

502 lines (427 loc) 17.9 kB
import * as PUPPET from 'wechaty-puppet' import { log } from 'wechaty-puppet' import { GError, timeoutPromise, TimeoutPromiseGError, } from 'gerror' import { StateSwitch } from 'state-switch' import type { StateSwitchInterface, } from 'state-switch' import { config } from '../config.js' import { timestampToDate } from '../pure-functions/timestamp-to-date.js' import type { ContactImpl, ContactInterface, RoomImpl, } from '../user-modules/mod.js' import type { WechatifyUserModuleMixin, } from './wechatify-user-module-mixin.js' import type { GErrorMixin } from './gerror-mixin.js' import type { IoMixin } from './io-mixin.js' const PUPPET_MEMORY_NAME = 'puppet' /** * Huan(202111): `puppetMixin` must extend `pluginMixin` * because the `wechaty-redux` plugin need to be installed before * the puppet started * * Huan(20211128): `puppetMixin` must extend `IoMixin` * because the Io need the puppet instance to be ready when it starts */ const puppetMixin = <MixinBase extends WechatifyUserModuleMixin & GErrorMixin & IoMixin> (mixinBase: MixinBase) => { log.verbose('WechatyPuppetMixin', 'puppetMixin(%s)', mixinBase.name) abstract class PuppetMixin extends mixinBase { __puppet?: PUPPET.impls.PuppetInterface get puppet (): PUPPET.impls.PuppetInterface { if (!this.__puppet) { throw new Error('NOPUPPET') } return this.__puppet } readonly __readyState : StateSwitchInterface __puppetMixinInited = false constructor (...args: any[]) { log.verbose('WechatyPuppetMixin', 'construct()') super(...args) this.__readyState = new StateSwitch('WechatyReady', { log }) } override async start (): Promise<void> { log.verbose('WechatyPuppetMixin', 'start()') log.verbose('WechatyPuppetMixin', 'start() super.start() ...') await super.start() log.verbose('WechatyPuppetMixin', 'start() super.start() ... done') try { /** * reset the `wechaty.ready()` state * if it was previous set to `active` */ if (this.__readyState.active()) { this.__readyState.inactive(true) } try { log.verbose('WechatyPuppetMixin', 'start() starting puppet ...') await timeoutPromise( this.puppet.start(), 15 * 1000, // 15 seconds timeout ) log.verbose('WechatyPuppetMixin', 'start() starting puppet ... done') } catch (e) { if (e instanceof TimeoutPromiseGError) { /** * Huan(202111): * * We should throw the Timeout error when the puppet.start() can not be finished in time. * However, we need to compatible with some buggy puppet implementations which will not resolve the promise. * * TODO: throw the Timeout error when the puppet.start() can not be finished in time. * * e.g. after resolve @issue https://github.com/padlocal/wechaty-puppet-padlocal/issues/116 */ log.warn('WechatyPuppetMixin', 'start() starting puppet ... timeout') log.warn('WechatyPuppetMixin', 'start() puppet info: %s', this.puppet) } else { throw e } } } catch (e) { this.emitError(e) } } override async stop (): Promise<void> { log.verbose('WechatyPuppetMixin', 'stop()') try { log.verbose('WechatyPuppetMixin', 'stop() stopping puppet ...') await timeoutPromise( this.puppet.stop(), 15 * 1000, // 15 seconds timeout ) log.verbose('WechatyPuppetMixin', 'stop() stopping puppet ... done') } catch (e) { if (e instanceof TimeoutPromiseGError) { log.warn('WechatyPuppetMixin', 'stop() stopping puppet ... timeout') log.warn('WechatyPuppetMixin', 'stop() puppet info: %s', this.puppet) } this.emitError(e) } log.verbose('WechatyPuppetMixin', 'stop() super.stop() ...') await super.stop() log.verbose('WechatyPuppetMixin', 'stop() super.stop() ... done') } async ready (): Promise<void> { log.verbose('WechatyPuppetMixin', 'ready()') await this.__readyState.stable('active') log.silly('WechatyPuppetMixin', 'ready() this.readyState.stable(on) resolved') } override async init (): Promise<void> { log.verbose('WechatyPuppetMixin', 'init()') await super.init() if (this.__puppetMixinInited) { log.verbose('WechatyPuppetMixin', 'init() skipped because this puppet has already been inited before.') return } this.__puppetMixinInited = true log.verbose('WechatyPuppetMixin', 'init() instanciating puppet instance ...') const puppetInstance = await PUPPET.helpers.resolvePuppet({ puppet: this.__options.puppet || config.systemPuppetName(), puppetOptions: 'puppetOptions' in this.__options ? this.__options.puppetOptions : undefined, }) log.verbose('WechatyPuppetMixin', 'init() instanciating puppet instance ... done') /** * Plug the Memory Card to Puppet */ log.verbose('WechatyPuppetMixin', 'init() setting memory ...') const puppetMemory = this.memory.multiplex(PUPPET_MEMORY_NAME) puppetInstance.setMemory(puppetMemory) log.verbose('WechatyPuppetMixin', 'init() setting memory ... done') /** * Propagate Puppet Events to Wechaty */ log.verbose('WechatyPuppetMixin', 'init() setting up events ...') this.__setupPuppetEvents(puppetInstance) log.verbose('WechatyPuppetMixin', 'init() setting up events ... done') /** * Private Event * - Huan(202005): emit puppet when set * - Huan(202110): @see https://github.com/wechaty/redux/blob/16af0ae01f72e37f0ee286b49fa5ccf69850323d/src/wechaty-redux.ts#L82-L98 */ log.verbose('WechatyPuppetMixin', 'init() emitting "puppet" event ...') ;(this.emit as any)('puppet', puppetInstance) log.verbose('WechatyPuppetMixin', 'init() emitting "puppet" event ... done') this.__puppet = puppetInstance } __setupPuppetEvents (puppet: PUPPET.impls.PuppetInterface): void { log.verbose('WechatyPuppetMixin', '__setupPuppetEvents(%s)', puppet) const eventNameList: PUPPET.types.PuppetEventName[] = Object.keys(PUPPET.types.PUPPET_EVENT_DICT) as PUPPET.types.PuppetEventName[] for (const eventName of eventNameList) { log.verbose('PuppetMixin', '__setupPuppetEvents() puppet.on(%s) (listenerCount:%s) registering...', eventName, puppet.listenerCount(eventName), ) switch (eventName) { case 'dong': puppet.on('dong', payload => { this.emit('dong', payload.data) }) break case 'error': puppet.on('error', payload => { /** * Huan(202112): * 1. remove `payload.data` after it has been sunset (after Dec 31, 2022) * 2. throw error if `payload.gerror` is not exists (for enforce puppet strict follow the error event schema) */ this.emit('error', GError.from(payload.gerror || payload.data || payload)) }) break case 'heartbeat': puppet.on('heartbeat', payload => { /** * Use `watchdog` event from Puppet to `heartbeat` Wechaty. */ // TODO: use a throttle queue to prevent beat too fast. this.emit('heartbeat', payload.data) }) break case 'friendship': puppet.on('friendship', async payload => { const friendship = this.Friendship.load(payload.friendshipId) try { await friendship.ready() this.emit('friendship', friendship) friendship.contact().emit('friendship', friendship) } catch (e) { this.emit('error', GError.from(e)) } }) break case 'login': puppet.on('login', async payload => { try { const contact = await this.ContactSelf.find({ id: payload.contactId }) if (!contact) { throw new Error('no contact found for id: ' + payload.contactId) } this.emit('login', contact) } catch (e) { this.emit('error', GError.from(e)) } }) break case 'logout': puppet.on('logout', async payload => { try { this.__readyState.inactive(true) const contact = await this.ContactSelf.find({ id: payload.contactId }) if (contact) { this.emit('logout', contact, payload.data) } else { log.verbose('PuppetMixin', '__setupPuppetEvents() logout event contact self not found for id: %s', payload.contactId, ) } } catch (e) { this.emit('error', GError.from(e)) } }) break case 'message': puppet.on('message', async payload => { try { const msg = await this.Message.find({ id: payload.messageId }) if (!msg) { this.emit('error', GError.from('message not found for id: ' + payload.messageId)) return } this.emit('message', msg) const room = msg.room() const listener = msg.listener() if (room) { room.emit('message', msg) } else if (listener) { listener.emit('message', msg) } else { this.emit('error', GError.from('message without room and listener')) } } catch (e) { this.emit('error', GError.from(e)) } }) break case 'post': puppet.on('post', async payload => { try { const post = await this.Post.find({ id: payload.postId }) if (!post) { this.emit('error', GError.from('post not found for id: ' + payload.postId)) return } this.emit('post', post) } catch (e) { this.emit('error', GError.from(e)) } }) break case 'ready': puppet.on('ready', () => { log.silly('WechatyPuppetMixin', '__setupPuppetEvents() puppet.on(ready)') this.emit('ready') this.__readyState.active(true) }) break case 'room-invite': puppet.on('room-invite', async payload => { const roomInvitation = this.RoomInvitation.load(payload.roomInvitationId) this.emit('room-invite', roomInvitation) }) break case 'room-join': puppet.on('room-join', async payload => { try { const room = await this.Room.find({ id: payload.roomId }) if (!room) { throw new Error('no room found for id: ' + payload.roomId) } await room.sync() const inviteeListAll = await Promise.all( payload.inviteeIdList.map(id => this.Contact.find({ id })), ) const inviteeList = inviteeListAll.filter(c => !!c) as ContactInterface[] const inviter = await this.Contact.find({ id: payload.inviterId }) if (!inviter) { throw new Error('no inviter found for id: ' + payload.inviterId) } const date = timestampToDate(payload.timestamp) this.emit('room-join', room, inviteeList, inviter, date) room.emit('join', inviteeList, inviter, date) } catch (e) { this.emit('error', GError.from(e)) } }) break case 'room-leave': puppet.on('room-leave', async payload => { try { const room = await this.Room.find({ id: payload.roomId }) if (!room) { throw new Error('no room found for id: ' + payload.roomId) } /** * See: https://github.com/wechaty/wechaty/pull/1833 */ await room.sync() const leaverListAll = await Promise.all( payload.removeeIdList.map(id => this.Contact.find({ id })), ) const leaverList = leaverListAll.filter(c => !!c) as ContactInterface[] const remover = await this.Contact.find({ id: payload.removerId }) if (!remover) { throw new Error('no remover found for id: ' + payload.removerId) } const date = timestampToDate(payload.timestamp) this.emit('room-leave', room, leaverList, remover, date) room.emit('leave', leaverList, remover, date) // issue #254 if (payload.removeeIdList.includes(puppet.currentUserId)) { await puppet.roomPayloadDirty(payload.roomId) await puppet.roomMemberPayloadDirty(payload.roomId) } } catch (e) { this.emit('error', GError.from(e)) } }) break case 'room-topic': puppet.on('room-topic', async payload => { try { const room = await this.Room.find({ id: payload.roomId }) if (!room) { throw new Error('no room found for id: ' + payload.roomId) } await room.sync() const changer = await this.Contact.find({ id: payload.changerId }) if (!changer) { throw new Error('no changer found for id: ' + payload.changerId) } const date = timestampToDate(payload.timestamp) this.emit('room-topic', room, payload.newTopic, payload.oldTopic, changer, date) room.emit('topic', payload.newTopic, payload.oldTopic, changer, date) } catch (e) { this.emit('error', GError.from(e)) } }) break case 'scan': puppet.on('scan', async payload => { this.__readyState.inactive(true) this.emit('scan', payload.qrcode || '', payload.status, payload.data) }) break case 'reset': // Do not propagation `reset` event from puppet break case 'dirty': /** * https://github.com/wechaty/wechaty-puppet-service/issues/43 */ puppet.on('dirty', async ({ payloadType, payloadId }) => { try { switch (payloadType) { case PUPPET.types.Payload.RoomMember: case PUPPET.types.Payload.Contact: { const contact = await this.Contact.find({ id: payloadId }) as unknown as undefined | ContactImpl await contact?.ready(true) break } case PUPPET.types.Payload.Room: { const room = await this.Room.find({ id: payloadId }) as unknown as undefined | RoomImpl await room?.ready(true) break } /** * Huan(202008): noop for the following */ case PUPPET.types.Payload.Friendship: // Friendship has no payload break case PUPPET.types.Payload.Message: // Message does not need to dirty (?) break case PUPPET.types.Payload.Unspecified: default: throw new Error('unknown payload type: ' + payloadType) } } catch (e) { this.emit('error', GError.from(e)) } }) break default: /** * Check: The eventName here should have the type `never` */ throw new Error('eventName ' + eventName + ' unsupported!') } } log.verbose('WechatyPuppetMixin', '__setupPuppetEvents() ... done') } } return PuppetMixin } type PuppetMixin = ReturnType<typeof puppetMixin> type ProtectedPropertyPuppetMixin = | '__puppet' | '__readyState' | '__setupPuppetEvents' export type { PuppetMixin, ProtectedPropertyPuppetMixin, } export { puppetMixin, }