UNPKG

@juzi/wechaty

Version:

Wechaty is a RPA SDK for Chatbot Makers.

652 lines 37.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.puppetMixin = void 0; const PUPPET = __importStar(require("@juzi/wechaty-puppet")); const wechaty_puppet_1 = require("@juzi/wechaty-puppet"); const gerror_1 = require("gerror"); const state_switch_1 = require("state-switch"); const config_js_1 = require("../config.js"); const timestamp_to_date_js_1 = require("../pure-functions/timestamp-to-date.js"); const retry_policy_js_1 = require("../pure-functions/retry-policy.js"); const types_1 = require("@juzi/wechaty-puppet/types"); 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) => { wechaty_puppet_1.log.verbose('WechatyPuppetMixin', 'puppetMixin(%s)', mixinBase.name); class PuppetMixin extends mixinBase { __puppet; get puppet() { if (!this.__puppet) { throw new Error('NOPUPPET'); } return this.__puppet; } __readyState; __loginIndicator; __puppetMixinInited = false; constructor(...args) { wechaty_puppet_1.log.verbose('WechatyPuppetMixin', 'construct()'); super(...args); this.__readyState = new state_switch_1.StateSwitch('WechatyReady', { log: wechaty_puppet_1.log }); this.__loginIndicator = new state_switch_1.BooleanIndicator(); this.on('login', () => { this.__loginIndicator.value(true); }); this.on('logout', () => { this.__loginIndicator.value(false); }); } async start() { wechaty_puppet_1.log.verbose('WechatyPuppetMixin', 'start()'); wechaty_puppet_1.log.verbose('WechatyPuppetMixin', 'start() super.start() ...'); await super.start(); wechaty_puppet_1.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 { wechaty_puppet_1.log.verbose('WechatyPuppetMixin', 'start() starting puppet ...'); await (0, gerror_1.timeoutPromise)(this.puppet.start(), 15 * 1000); wechaty_puppet_1.log.verbose('WechatyPuppetMixin', 'start() starting puppet ... done'); } catch (e) { if (e instanceof gerror_1.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 */ wechaty_puppet_1.log.warn('WechatyPuppetMixin', 'start() starting puppet ... timeout'); wechaty_puppet_1.log.warn('WechatyPuppetMixin', 'start() puppet info: %s', this.puppet); } else { throw e; } } } catch (e) { this.emitError(e); } } async stop() { wechaty_puppet_1.log.verbose('WechatyPuppetMixin', 'stop()'); try { wechaty_puppet_1.log.verbose('WechatyPuppetMixin', 'stop() stopping puppet ...'); await (0, gerror_1.timeoutPromise)(this.puppet.stop(), 15 * 1000); wechaty_puppet_1.log.verbose('WechatyPuppetMixin', 'stop() stopping puppet ... done'); } catch (e) { if (e instanceof gerror_1.TimeoutPromiseGError) { wechaty_puppet_1.log.warn('WechatyPuppetMixin', 'stop() stopping puppet ... timeout'); wechaty_puppet_1.log.warn('WechatyPuppetMixin', 'stop() puppet info: %s', this.puppet); } this.emitError(e); } wechaty_puppet_1.log.verbose('WechatyPuppetMixin', 'stop() super.stop() ...'); await super.stop(); wechaty_puppet_1.log.verbose('WechatyPuppetMixin', 'stop() super.stop() ... done'); } async ready() { wechaty_puppet_1.log.verbose('WechatyPuppetMixin', 'ready()'); await this.__readyState.stable('active'); wechaty_puppet_1.log.silly('WechatyPuppetMixin', 'ready() this.readyState.stable(on) resolved'); } async init() { wechaty_puppet_1.log.verbose('WechatyPuppetMixin', 'init()'); await super.init(); if (this.__puppetMixinInited) { wechaty_puppet_1.log.verbose('WechatyPuppetMixin', 'init() skipped because this puppet has already been inited before.'); return; } this.__puppetMixinInited = true; wechaty_puppet_1.log.verbose('WechatyPuppetMixin', 'init() instanciating puppet instance ...'); const puppetInstance = await PUPPET.helpers.resolvePuppet({ puppet: this.__options.puppet || config_js_1.config.systemPuppetName(), puppetOptions: 'puppetOptions' in this.__options ? this.__options.puppetOptions : undefined, }); wechaty_puppet_1.log.verbose('WechatyPuppetMixin', 'init() instanciating puppet instance ... done'); /** * Plug the Memory Card to Puppet */ wechaty_puppet_1.log.verbose('WechatyPuppetMixin', 'init() setting memory ...'); const puppetMemory = this.memory.multiplex(PUPPET_MEMORY_NAME); puppetInstance.setMemory(puppetMemory); wechaty_puppet_1.log.verbose('WechatyPuppetMixin', 'init() setting memory ... done'); /** * Propagate Puppet Events to Wechaty */ wechaty_puppet_1.log.verbose('WechatyPuppetMixin', 'init() setting up events ...'); this.__setupPuppetEvents(puppetInstance); wechaty_puppet_1.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 */ wechaty_puppet_1.log.verbose('WechatyPuppetMixin', 'init() emitting "puppet" event ...'); this.emit('puppet', puppetInstance); wechaty_puppet_1.log.verbose('WechatyPuppetMixin', 'init() emitting "puppet" event ... done'); this.__puppet = puppetInstance; } __setupPuppetEvents(puppet) { wechaty_puppet_1.log.verbose('WechatyPuppetMixin', '__setupPuppetEvents(%s)', puppet); const eventNameList = Object.keys(PUPPET.types.PUPPET_EVENT_DICT); for (const eventName of eventNameList) { wechaty_puppet_1.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_1.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(); await friendship.contact().sync(); this.emit('friendship', friendship); friendship.contact().emit('friendship', friendship); } catch (e) { this.emit('error', gerror_1.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); const readyTimeout = setTimeout(() => { if (this.puppet.readyIndicator.value()) { this.emit('ready'); } }, 15 * 1000); puppet.once('ready', () => { // if we got ready from puppet, we don't have to fire it here. // it will be fired by ready listener clearTimeout(readyTimeout); }); } catch (e) { this.emit('error', gerror_1.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 { wechaty_puppet_1.log.verbose('PuppetMixin', '__setupPuppetEvents() logout event contact self not found for id: %s', payload.contactId); } } catch (e) { this.emit('error', gerror_1.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_1.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_1.GError.from('message without room and listener')); } } catch (e) { this.emit('error', gerror_1.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_1.GError.from('post not found for id: ' + payload.postId)); return; } this.emit('post', post); } catch (e) { this.emit('error', gerror_1.GError.from(e)); } }); break; case 'post-comment': puppet.on('post-comment', async (payload) => { try { const comment = await this.Post.find({ id: payload.commentId }); const post = await this.Post.find({ id: payload.postId }); if (!post) { this.emit('error', gerror_1.GError.from('post not found for id: ' + payload.postId)); return; } if (!comment) { this.emit('error', gerror_1.GError.from('comment not found for id: ' + payload.commentId)); return; } this.emit('post-comment', comment, post); } catch (e) { this.emit('error', gerror_1.GError.from(e)); } }); break; case 'post-tap': puppet.on('post-tap', async (payload) => { try { const post = await this.Post.find({ id: payload.postId }); const contact = await this.Contact.find({ id: payload.contactId }); const date = (0, timestamp_to_date_js_1.timestampToDate)(payload.timestamp); if (!post) { this.emit('error', gerror_1.GError.from('post not found for id: ' + payload.postId)); return; } if (!contact) { this.emit('error', gerror_1.GError.from('contact not found for id: ' + payload.contactId)); return; } this.emit('post-tap', post, contact, payload.tapType, payload.tap, date); } catch (e) { this.emit('error', gerror_1.GError.from(e)); } }); break; case 'ready': puppet.on('ready', async () => { wechaty_puppet_1.log.silly('WechatyPuppetMixin', '__setupPuppetEvents() puppet.on(ready)'); // ready event should be emitted 15s after login let onceLogout; let timeout; // 'NodeJS' is not defined. const future = new Promise((resolve, reject) => { onceLogout = () => { reject(new Error('puppet logout!')); }; puppet.once('logout', onceLogout); timeout = setTimeout(() => { reject(new Error('waiting for login timeout')); }, 60 * 1000); void this.__loginIndicator.ready(true).then(resolve); }).finally(() => { puppet.off('logout', onceLogout); clearTimeout(timeout); }); try { await future; await new Promise(resolve => { setTimeout(resolve, 15 * 1000); }); if (this.__loginIndicator.value()) { this.emit('ready'); this.__readyState.active(true); } } catch (e) { wechaty_puppet_1.log.error(`ready error: ${e.message}, will emit event anyway if it's logged in now`); if (this.puppet.isLoggedIn) { this.emit('ready'); this.__loginIndicator.value(true); 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); const inviter = await this.Contact.find({ id: payload.inviterId }); if (!inviter) { throw new Error('no inviter found for id: ' + payload.inviterId); } const date = (0, timestamp_to_date_js_1.timestampToDate)(payload.timestamp); this.emit('room-join', room, inviteeList, inviter, date); room.emit('join', inviteeList, inviter, date); } catch (e) { this.emit('error', gerror_1.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); const remover = await this.Contact.find({ id: payload.removerId }); if (!remover) { throw new Error('no remover found for id: ' + payload.removerId); } const date = (0, timestamp_to_date_js_1.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_1.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 = (0, timestamp_to_date_js_1.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_1.GError.from(e)); } }); break; case 'room-announce': puppet.on('room-announce', 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(); let changer; try { if (payload.changerId) { changer = await this.Contact.find({ id: payload.changerId }); } } catch (e) { wechaty_puppet_1.log.warn('room-announce', 'room-announce event error: %s', e.message); } const date = (0, timestamp_to_date_js_1.timestampToDate)(payload.timestamp); this.emit('room-announce', room, payload.newAnnounce, changer, payload.oldAnnounce, date); room.emit('announce', payload.newAnnounce, changer, payload.oldAnnounce, date); } catch (e) { this.emit('error', gerror_1.GError.from(e)); } }); break; case 'scan': puppet.on('scan', async (payload) => { this.__readyState.inactive(true); const date = (0, timestamp_to_date_js_1.timestampToDate)(payload.createTimestamp || payload.timestamp || 0); const expireDate = payload.expireTimestamp ? (0, timestamp_to_date_js_1.timestampToDate)(payload.expireTimestamp) : undefined; this.emit('scan', payload.qrcode || '', payload.status, payload.data || '', payload.type || types_1.ScanType.Unknown, date, expireDate); }); break; case 'tag': puppet.on('tag', async (payload) => { const date = (0, timestamp_to_date_js_1.timestampToDate)(payload.timestamp); switch (payload.type) { case PUPPET.types.TagEvent.TagCreate: { const newTagPromises = payload.idList.map(id => this.Tag.find({ id })); const newTags = await Promise.all(newTagPromises); this.emit('tag', payload.type, newTags, date); break; } case PUPPET.types.TagEvent.TagDelete: { const deletedTagPromises = payload.idList.map(id => this.Tag.find({ id })); const deletedTags = await Promise.all(deletedTagPromises); this.emit('tag', payload.type, deletedTags, date); // TODO: bind tag-delete to tag instance break; } case PUPPET.types.TagEvent.TagRename: { const renamedTagPromises = payload.idList.map(id => this.Tag.find({ id })); const renamedTags = (await Promise.all(renamedTagPromises)).filter(tag => !!tag); await Promise.all(renamedTags.map(async (tag) => { const oldName = tag.name(); const result = await (0, retry_policy_js_1.checkUntilChanged)(config_js_1.PUPPET_PAYLOAD_SYNC_GAP, config_js_1.PUPPET_PAYLOAD_SYNC_MAX_RETRY, async () => { await tag.sync(); return tag.name() === oldName; }); if (!result) { wechaty_puppet_1.log.warn('WechatyPuppetMixin', 'tagRenameEvent still get old name after %s retries for tag %s', config_js_1.PUPPET_PAYLOAD_SYNC_MAX_RETRY, tag.id); } })); this.emit('tag', payload.type, renamedTags, date); // TODO: bind tag-rename to tag instance break; } default: throw new Error('tagEventType ' + payload.type + ' unsupported!'); } }); break; case 'tag-group': puppet.on('tag-group', async (payload) => { const date = (0, timestamp_to_date_js_1.timestampToDate)(payload.timestamp); switch (payload.type) { case PUPPET.types.TagGroupEvent.TagGroupCreate: { const newTagGroupPromises = payload.idList.map(id => this.TagGroup.find({ id })); const newTagGroups = await Promise.all(newTagGroupPromises); this.emit('tag-group', payload.type, newTagGroups, date); break; } case PUPPET.types.TagGroupEvent.TagGroupDelete: { const deletedTagGroupPromises = payload.idList.map(id => this.TagGroup.find({ id })); const deletedTagGroups = await Promise.all(deletedTagGroupPromises); this.emit('tag-group', payload.type, deletedTagGroups, date); break; // TODO: bind tagGroup-delete to tagGroup instance } case PUPPET.types.TagGroupEvent.TagGroupRename: { const renamedTagGroupPromises = payload.idList.map(id => this.TagGroup.find({ id })); const renamedTagGroups = (await Promise.all(renamedTagGroupPromises)); await Promise.all(renamedTagGroups.map(async (tagGroup) => { const oldName = tagGroup.name(); const result = await (0, retry_policy_js_1.checkUntilChanged)(config_js_1.PUPPET_PAYLOAD_SYNC_GAP, config_js_1.PUPPET_PAYLOAD_SYNC_MAX_RETRY, async () => { await tagGroup.sync(); return tagGroup.name() === oldName; }); if (!result) { wechaty_puppet_1.log.warn('WechatyPuppetMixin', 'tagGroupRenameEvent still get old name after %s retries for tagGroup %s', config_js_1.PUPPET_PAYLOAD_SYNC_MAX_RETRY, tagGroup.id); } })); this.emit('tag-group', payload.type, renamedTagGroups, date); // TODO: bind tagGroup-rename to tagGroup instance break; } default: throw new Error('tagGroupEventType ' + payload.type + ' unsupported!'); } }); break; case 'verify-code': puppet.on('verify-code', (payload) => { this.emit('verify-code', payload.id, payload.message || '', payload.scene || PUPPET.types.VerifyCodeScene.UNKNOWN, payload.status || PUPPET.types.VerifyCodeStatus.UNKNOWN); }); 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.Contact: { const contact = await this.Contact.find({ id: payloadId }); await contact?.ready(true); break; } case PUPPET.types.Payload.Room: { const room = await this.Room.find({ id: payloadId }); await room?.ready(true); break; } case PUPPET.types.Payload.RoomMember: { if (payloadId.includes(PUPPET.STRING_SPLITTER)) { break; } const room = await this.Room.find({ id: payloadId }); await room?.ready(); 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 (?) const message = await this.Message.find({ id: payloadId }); await message?.ready(true); break; } case PUPPET.types.Payload.Tag: break; case PUPPET.types.Payload.TagGroup: break; case PUPPET.types.Payload.Post: break; case PUPPET.types.Payload.Unspecified: default: wechaty_puppet_1.log.warn('unknown payload type: ' + payloadType); } this.emit('dirty', payloadId, payloadType); } catch (e) { this.emit('error', gerror_1.GError.from(e)); } }); break; case 'login-url': puppet.on('login-url', (payload) => { this.emit('login-url', payload.url); }); break; default: /** * Check: The eventName here should have the type `never` */ throw new Error('eventName ' + eventName + ' unsupported!'); } } wechaty_puppet_1.log.verbose('WechatyPuppetMixin', '__setupPuppetEvents() ... done'); } } return PuppetMixin; }; exports.puppetMixin = puppetMixin; //# sourceMappingURL=puppet-mixin.js.map