aijinkela-wechaty
Version:
Wechaty is a RPA SDK for Chatbot Makers.
399 lines • 21.3 kB
JavaScript
import * as PUPPET from 'wechaty-puppet';
import { log } from 'wechaty-puppet';
import { GError, timeoutPromise, TimeoutPromiseGError, } from 'gerror';
import { StateSwitch } from 'state-switch';
import { config } from '../config.js';
import { timestampToDate } from '../pure-functions/timestamp-to-date.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) => {
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;
__puppetMixinInited = false;
constructor(...args) {
log.verbose('WechatyPuppetMixin', 'construct()');
super(...args);
this.__readyState = new StateSwitch('WechatyReady', { log });
}
async start() {
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);
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);
}
}
async stop() {
log.verbose('WechatyPuppetMixin', 'stop()');
try {
log.verbose('WechatyPuppetMixin', 'stop() stopping puppet ...');
await timeoutPromise(this.puppet.stop(), 15 * 1000);
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() {
log.verbose('WechatyPuppetMixin', 'ready()');
await this.__readyState.stable('active');
log.silly('WechatyPuppetMixin', 'ready() this.readyState.stable(on) resolved');
}
async init() {
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('puppet', puppetInstance);
log.verbose('WechatyPuppetMixin', 'init() emitting "puppet" event ... done');
this.__puppet = puppetInstance;
}
__setupPuppetEvents(puppet) {
log.verbose('WechatyPuppetMixin', '__setupPuppetEvents(%s)', puppet);
const eventNameList = Object.keys(PUPPET.types.PUPPET_EVENT_DICT);
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);
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);
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 });
await contact?.ready(true);
break;
}
case PUPPET.types.Payload.Room: {
const room = await this.Room.find({ id: payloadId });
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;
};
export { puppetMixin, };
//# sourceMappingURL=puppet-mixin.js.map