@juzi/wechaty
Version:
Wechaty is a RPA SDK for Chatbot Makers.
652 lines • 37.8 kB
JavaScript
;
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