@juzi/wechaty
Version:
Wechaty is a RPA SDK for Chatbot Makers.
471 lines • 18.5 kB
JavaScript
"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;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Io = exports.IO_EVENT_DICT = void 0;
/**
* Wechaty Chatbot SDK - https://github.com/wechaty/wechaty
*
* @copyright 2016 Huan LI (李卓桓) <https://github.com/huan>, and
* Wechaty Contributors <https://github.com/wechaty>.
*
* 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.
*
*/
const ws_1 = __importDefault(require("ws"));
const state_switch_1 = require("state-switch");
const jsonRpcPeer = __importStar(require("json-rpc-peer"));
const config_js_1 = require("./config.js");
const io_peer_js_1 = require("./io-peer/io-peer.js");
exports.IO_EVENT_DICT = {
botie: 'tbw',
error: 'tbw',
heartbeat: 'tbw',
jsonrpc: 'JSON RPC',
login: 'tbw',
logout: 'tbw',
message: 'tbw',
raw: 'tbw',
reset: 'tbw',
scan: 'tbw',
shutdown: 'tbw',
sys: 'tbw',
update: 'tbw',
};
/**
* https://github.com/Chatie/botie/issues/2
* https://github.com/actions/github-script/blob/f035cea4677903b153fa754aa8c2bba66f8dc3eb/src/async-function.ts#L6
*/
const AsyncFunction = Object.getPrototypeOf(async () => null).constructor;
// function callAsyncFunction<U extends {} = {}, V = unknown> (
// args: U,
// source: string
// ): Promise<V> {
// const fn = new AsyncFunction(...Object.keys(args), source)
// return fn(...Object.values(args))
// }
class Io {
options;
id;
protocol;
eventBuffer = [];
ws;
state = new state_switch_1.StateSwitch('Io', { log: config_js_1.log });
reconnectTimer;
reconnectTimeout;
lifeTimer;
onMessage;
scanPayload;
jsonRpc;
constructor(options) {
this.options = options;
options.apihost = options.apihost || config_js_1.config.apihost;
options.protocol = options.protocol || config_js_1.config.default.DEFAULT_PROTOCOL;
this.id = options.wechaty.id;
this.protocol = options.protocol + '|' + options.wechaty.id + '|' + config_js_1.config.serviceIp + '|' + options.servicePort;
config_js_1.log.verbose('Io', 'instantiated with apihost[%s], token[%s], protocol[%s], cuid[%s]', options.apihost, options.token, options.protocol, this.id);
if (options.servicePort) {
this.jsonRpc = (0, io_peer_js_1.getPeer)({
serviceGrpcPort: this.options.servicePort,
});
}
}
toString() {
return `Io<${this.options.token}>`;
}
connected() {
return this.ws && this.ws.readyState === ws_1.default.OPEN;
}
async start() {
config_js_1.log.verbose('Io', 'start()');
if (this.lifeTimer) {
throw new Error('lifeTimer exist');
}
this.state.active('pending');
try {
this.initEventHook();
this.ws = await this.initWebSocket();
this.options.wechaty.on('login', () => { this.scanPayload = undefined; });
this.options.wechaty.on('scan', (qrcode, status, data, type, date) => {
this.scanPayload = {
...this.scanPayload,
qrcode,
status,
type,
data,
timestamp: date.valueOf(),
};
});
this.lifeTimer = setInterval(() => {
if (this.ws && this.connected()) {
config_js_1.log.silly('Io', 'start() setInterval() ws.ping()');
// TODO: check 'pong' event on ws
this.ws.ping();
}
}, 1000 * 10);
this.state.active(true);
}
catch (e) {
config_js_1.log.warn('Io', 'start() exception: %s', e.message);
this.state.inactive(true);
throw e;
}
}
initEventHook() {
config_js_1.log.verbose('Io', 'initEventHook()');
const wechaty = this.options.wechaty;
wechaty.on('error', error => this.send({ name: 'error', payload: error }));
wechaty.on('heartbeat', data => this.send({ name: 'heartbeat', payload: { cuid: this.id, data } }));
wechaty.on('login', user => this.send({ name: 'login', payload: wechaty.puppet.contactPayload(user.id) }));
wechaty.on('logout', user => this.send({ name: 'logout', payload: wechaty.puppet.contactPayload(user.id) }));
wechaty.on('message', message => this.ioMessage(message));
// FIXME: payload schema need to be defined universal
// wechaty.on('scan', (url, code) => this.send({ name: 'scan', payload: { url, code } }))
wechaty.on('scan', (qrcode, status) => this.send({ name: 'scan', payload: { qrcode, status } }));
}
async initWebSocket() {
config_js_1.log.verbose('Io', 'initWebSocket()');
// this.state.current('on', false)
// const auth = 'Basic ' + new Buffer(this.setting.token + ':X').toString('base64')
const auth = 'Token ' + this.options.token;
const headers = { Authorization: auth };
if (!this.options.apihost) {
throw new Error('no apihost');
}
let endpoint = 'wss://' + this.options.apihost + '/v0/websocket';
// XXX quick and dirty: use no ssl for API_HOST other than official
// FIXME: use a configurable VARIABLE for the domain name at here:
if (!/api\.chatie\.io/.test(this.options.apihost)) {
endpoint = 'ws://' + this.options.apihost + '/v0/websocket';
}
const ws = this.ws = new ws_1.default(endpoint, this.protocol, { headers });
ws.on('open', () => this.wsOnOpen(ws));
ws.on('message', data => this.wsOnMessage(data));
ws.on('error', e => this.wsOnError(e));
ws.on('close', (code, reason) => this.wsOnClose(ws, code, reason.toString()));
await new Promise((resolve, reject) => {
ws.once('open', resolve);
ws.once('error', reject);
ws.once('close', reject);
});
return ws;
}
wsOnOpen(ws) {
if (this.protocol !== ws.protocol) {
config_js_1.log.error('Io', 'wsOnOpen() require protocol[%s] failed', this.protocol);
// XXX deal with error?
}
config_js_1.log.verbose('Io', 'wsOnOpen() connected with protocol [%s]', ws.protocol);
// this.currentState('connected')
// this.state.current('on')
// FIXME: how to keep alive???
// ws._socket.setKeepAlive(true, 100)
this.reconnectTimeout = undefined;
const name = 'sys';
const payload = 'Wechaty version ' + this.options.wechaty.version() + ` with CUID: ${this.id}`;
const initEvent = {
name,
payload,
};
this.send(initEvent).catch(console.error);
}
wsOnMessage(data) {
config_js_1.log.silly('Io', 'wsOnMessage() ws.on(message): %s', data);
this.wsOnMessageAsync(data).catch(console.error);
}
async wsOnMessageAsync(data) {
config_js_1.log.silly('Io', 'wsOnMessageAsync() ws.on(message): %s', data);
// flags.binary will be set if a binary data is received.
// flags.masked will be set if the data was masked.
let payload;
if (Array.isArray(data)) {
payload = data[0]?.toString() ?? 'NODATA';
}
else {
payload = data.toString();
}
const ioEvent = {
name: 'raw',
payload: data,
};
try {
const obj = JSON.parse(payload);
ioEvent.name = obj.name;
ioEvent.payload = obj.payload;
}
catch (e) {
config_js_1.log.verbose('Io', 'on(message) recv a non IoEvent data[%s]', data);
}
switch (ioEvent.name) {
case 'botie':
{
const payload = ioEvent.payload;
const args = payload.args;
const source = payload.source;
try {
if (args[0] === 'message' && args.length === 1) {
const fn = new AsyncFunction(...args, source);
this.onMessage = fn;
}
else {
config_js_1.log.warn('Io', 'server pushed function is invalid. args: %s', JSON.stringify(args));
}
}
catch (e) {
config_js_1.log.warn('Io', 'server pushed function exception: %s', e);
this.options.wechaty.emitError(e);
}
}
break;
case 'reset':
config_js_1.log.verbose('Io', 'on(reset): %s', ioEvent.payload);
this.options.wechaty.emitError(new Error('reset by server: '
+ JSON.stringify(ioEvent.payload)));
await this.options.wechaty.reset();
break;
case 'shutdown':
config_js_1.log.info('Io', 'on(shutdown): %s', ioEvent.payload);
process.exit(0);
// eslint-disable-next-line
break;
case 'update':
config_js_1.log.verbose('Io', 'on(update): %s', ioEvent.payload);
{
const wechaty = this.options.wechaty;
if (wechaty.isLoggedIn) {
const loginEvent = {
name: 'login',
payload: await wechaty.puppet.contactPayload(wechaty.puppet.currentUserId),
};
await this.send(loginEvent);
}
if (this.scanPayload) {
const scanEvent = {
name: 'scan',
payload: this.scanPayload,
};
await this.send(scanEvent);
}
}
break;
case 'sys':
// do nothing
break;
case 'logout':
config_js_1.log.info('Io', 'on(logout): %s', ioEvent.payload);
await this.options.wechaty.logout();
break;
case 'jsonrpc':
config_js_1.log.info('Io', 'on(jsonrpc): %s', ioEvent.payload);
try {
const request = ioEvent.payload;
if (!(0, io_peer_js_1.isJsonRpcRequest)(request)) {
config_js_1.log.warn('Io', 'on(jsonrpc) payload is not a jsonrpc request: %s', JSON.stringify(request));
return;
}
if (!this.jsonRpc) {
throw new Error('jsonRpc not initialized!');
}
const response = await this.jsonRpc.exec(request);
if (!response) {
config_js_1.log.warn('Io', 'on(jsonrpc) response is undefined.');
return;
}
const payload = jsonRpcPeer.parse(response);
const jsonrpcEvent = {
name: 'jsonrpc',
payload,
};
config_js_1.log.verbose('Io', 'on(jsonrpc) send(%s)', response);
await this.send(jsonrpcEvent);
}
catch (e) {
config_js_1.log.error('Io', 'on(jsonrpc): %s', e);
}
break;
default:
config_js_1.log.warn('Io', 'UNKNOWN on(%s): %s', ioEvent.name, ioEvent.payload);
break;
}
}
// FIXME: it seems the parameter `e` might be `undefined`.
// @types/ws might has bug for `ws.on('error', e => this.wsOnError(e))`
wsOnError(e) {
config_js_1.log.warn('Io', 'wsOnError() error event[%s]', e && e.message);
if (!e) {
return;
}
if (!this.ws) {
config_js_1.log.error('Io', 'wsOnError() ws.on(error) this.ws is `undefined`', e.message);
return;
}
if (this.ws.readyState === ws_1.default.CONNECTING) {
config_js_1.log.error('Io', 'wsOnError() ws.on(error) ws.readyState is CONNECTING: %s', e.message);
return;
}
if (this.ws.readyState === ws_1.default.CLOSING) {
config_js_1.log.error('Io', 'wsOnError() ws.on(error) ws.readyState is CLOSING: %s', e.message);
return;
}
this.options.wechaty.emitError(e);
// when `error`, there must have already a `close` event
// we should not call this.reconnect() again
//
// this.close()
// this.reconnect()
}
wsOnClose(ws, code, message) {
if (this.state.active()) {
config_js_1.log.warn('Io', 'wsOnClose() close event[%d: %s]', code, message);
ws.close();
this.reconnect();
}
}
reconnect() {
config_js_1.log.verbose('Io', 'reconnect()');
if (this.state.inactive()) {
config_js_1.log.warn('Io', 'reconnect() canceled because state.target() === offline');
return;
}
if (this.connected()) {
config_js_1.log.warn('Io', 'reconnect() on a already connected io');
return;
}
if (this.reconnectTimer) {
config_js_1.log.warn('Io', 'reconnect() on a already re-connecting io');
return;
}
if (!this.reconnectTimeout) {
this.reconnectTimeout = 1;
}
else if (this.reconnectTimeout < 10 * 1000) {
this.reconnectTimeout *= 3;
}
config_js_1.log.warn('Io', 'reconnect() will reconnect after %d s', Math.floor(this.reconnectTimeout / 1000));
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = undefined;
this.initWebSocket().catch(console.error);
}, this.reconnectTimeout); // as any as NodeJS.Timer
}
async send(ioEvent) {
if (!this.ws) {
throw new Error('no ws');
}
const ws = this.ws;
if (ioEvent) {
config_js_1.log.silly('Io', 'send(%s)', JSON.stringify(ioEvent));
this.eventBuffer.push(ioEvent);
}
else {
config_js_1.log.silly('Io', 'send()');
}
if (!this.connected()) {
config_js_1.log.verbose('Io', 'send() without a connected websocket, eventBuffer.length = %d', this.eventBuffer.length);
return;
}
const list = [];
while (this.eventBuffer.length) {
const data = JSON.stringify(this.eventBuffer.shift());
const p = new Promise((resolve, reject) => ws.send(data, (err) => {
if (err) {
reject(err);
}
else {
resolve();
}
}));
list.push(p);
}
try {
await Promise.all(list);
}
catch (e) {
config_js_1.log.error('Io', 'send() exception: %s', e.stack);
throw e;
}
}
async stop() {
config_js_1.log.verbose('Io', 'stop()');
if (!this.ws) {
throw new Error('no ws');
}
this.state.inactive('pending');
// try to send IoEvents in buffer
await this.send();
this.eventBuffer = [];
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = undefined;
}
if (this.lifeTimer) {
clearInterval(this.lifeTimer);
this.lifeTimer = undefined;
}
this.ws.close();
await new Promise(resolve => {
if (this.ws) {
this.ws.once('close', resolve);
}
else {
resolve();
}
});
this.ws = undefined;
this.state.inactive(true);
}
/**
*
* Prepare to be overwritten by server setting
*
*/
async ioMessage(m) {
config_js_1.log.silly('Io', 'ioMessage() is a nop function before be overwritten from cloud');
if (typeof this.onMessage === 'function') {
await this.onMessage(m);
}
}
async syncMessage(m) {
config_js_1.log.silly('Io', 'syncMessage(%s)', m);
const messageEvent = {
name: 'message',
payload: await m.wechaty.puppet.messagePayload(m.id),
};
await this.send(messageEvent);
}
}
exports.Io = Io;
//# sourceMappingURL=io.js.map