wechaty-puppet-padplus
Version:
Puppet Padplus for Wechaty
467 lines • 21.5 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const grpc_1 = __importDefault(require("grpc"));
const util_1 = __importDefault(require("util"));
const uuid_1 = require("uuid");
const config_1 = require("../config");
const PadPlusServer_grpc_pb_1 = require("./proto-ts/PadPlusServer_grpc_pb"); // add proto file from Gao
const callbackHelper_1 = require("../utils/callbackHelper");
const PadPlusServer_pb_1 = require("./proto-ts/PadPlusServer_pb");
const events_1 = require("events");
const grpc_event_emitter_1 = require("./grpc-event-emitter");
const rx_queue_1 = require("rx-queue");
const PRE = 'GRPC_GATEWAY';
const NEED_CALLBACK_API_LIST = [
PadPlusServer_pb_1.ApiType.SEND_MESSAGE,
PadPlusServer_pb_1.ApiType.SEND_FILE,
PadPlusServer_pb_1.ApiType.GET_MESSAGE_MEDIA,
PadPlusServer_pb_1.ApiType.SEARCH_CONTACT,
PadPlusServer_pb_1.ApiType.ADD_CONTACT,
PadPlusServer_pb_1.ApiType.CREATE_ROOM,
PadPlusServer_pb_1.ApiType.GET_ROOM_ANNOUNCEMENT,
PadPlusServer_pb_1.ApiType.SET_ROOM_ANNOUNCEMENT,
PadPlusServer_pb_1.ApiType.HEARTBEAT,
PadPlusServer_pb_1.ApiType.GET_ROOM_QRCODE,
PadPlusServer_pb_1.ApiType.GET_CONTACT_SELF_QRCODE,
PadPlusServer_pb_1.ApiType.SET_CONTACT_SELF_INFO,
PadPlusServer_pb_1.ApiType.GET_CONTACT_SELF_INFO,
PadPlusServer_pb_1.ApiType.LOGOUT,
];
class GrpcGateway extends events_1.EventEmitter {
constructor(token, endpoint) {
super();
this.token = token;
this.endpoint = endpoint;
this.eventEmitterMap = {};
this.stopping = false;
this.client = new PadPlusServer_grpc_pb_1.PadPlusServerClient(this.endpoint, grpc_1.default.credentials.createInsecure());
this.isAlive = false;
this.reconnectStatus = true;
this.timeoutNumber = 0;
this.startTime = Date.now();
}
static get Instance() {
return this._instance;
}
static init(token, endpoint, name) {
return __awaiter(this, void 0, void 0, function* () {
if (!this._instance) {
const instance = new GrpcGateway(token, endpoint);
yield instance.initSelf();
this._instance = instance;
}
else if (!this._instance.isAlive) {
yield this._instance.stop();
const instance = new GrpcGateway(token, endpoint);
yield instance.initSelf();
this._instance = instance;
}
return this._instance.addNewInstance(name);
});
}
keepHeartbeat() {
return __awaiter(this, void 0, void 0, function* () {
config_1.log.silly(PRE, `keepHeartbeat()`);
try {
const res = yield this.request(PadPlusServer_pb_1.ApiType.HEARTBEAT, '');
if (!res) {
throw new Error(`no heartbeat response from grpc server`);
}
}
catch (error) {
config_1.log.error(`can not get heartbeat from grpc server`, error);
Object.values(this.eventEmitterMap).map(emitter => {
emitter.emit('reconnect');
});
}
});
}
static release() {
return __awaiter(this, void 0, void 0, function* () {
if (this._instance) {
yield this._instance.stop();
this._instance = undefined;
}
});
}
initSelf() {
return __awaiter(this, void 0, void 0, function* () {
this.debounceQueue = new rx_queue_1.DebounceQueue(30 * 1000);
this.debounceQueueSubscription = this.debounceQueue.subscribe(() => __awaiter(this, void 0, void 0, function* () {
try {
yield this.keepHeartbeat();
}
catch (e) {
config_1.log.silly(PRE, `debounce error : ${util_1.default.inspect(e)}`);
}
}));
this.throttleQueue = new rx_queue_1.ThrottleQueue(15 * 1000);
this.throttleQueueSubscription = this.throttleQueue.subscribe((data) => {
config_1.log.silly(PRE, `throttleQueue emit heartbeat.`);
Object.values(this.eventEmitterMap).map(emitter => {
emitter.emit('heartbeat', data.getRequestid());
});
});
yield this.initGrpcGateway();
this.isAlive = true;
});
}
addNewInstance(name) {
const eventEmitter = new grpc_event_emitter_1.GrpcEventEmitter(name);
this.eventEmitterMap[name] = eventEmitter;
return eventEmitter;
}
emit(event, data) {
return super.emit(event, data);
}
on(event, listener) {
config_1.log.verbose(PRE, `on(${event}, ${typeof listener}) registered`);
super.on(event, (data) => {
try {
listener.call(this, data);
}
catch (e) {
config_1.log.error(PRE, `onFunction(${event}) listener exception: ${e}`);
}
});
return this;
}
checkTimeout(uin) {
return __awaiter(this, void 0, void 0, function* () {
if (this.timeoutNumber > 10 && Date.now() - this.startTime <= 3 * 60 * 1000) {
yield this.request(PadPlusServer_pb_1.ApiType.RECONNECT, uin);
this.startTime = Date.now();
this.timeoutNumber = 0;
}
else if (this.timeoutNumber === 0) {
this.startTime = Date.now();
}
else if (Date.now() - this.startTime > 3 * 60 * 1000) {
this.startTime = Date.now();
this.timeoutNumber = 0;
}
this.timeoutNumber++;
});
}
request(apiType, uin, data) {
return __awaiter(this, void 0, void 0, function* () {
const request = new PadPlusServer_pb_1.RequestObject();
const requestId = uuid_1.v4();
config_1.log.silly(PRE, `GRPC Request ApiType: ${apiType}`);
request.setToken(this.token);
if (uin !== '') {
request.setUin(uin);
}
request.setApitype(apiType);
request.setParams(JSON.stringify(data));
request.setRequestid(requestId);
const traceId = uuid_1.v4();
request.setTraceid(traceId);
try {
const result = yield this._request(request);
if (result && NEED_CALLBACK_API_LIST.includes(apiType)) {
return new Promise((resolve, reject) => {
let timeoutMs = 5 * 1000;
switch (apiType) {
case PadPlusServer_pb_1.ApiType.SEND_MESSAGE:
case PadPlusServer_pb_1.ApiType.SEND_FILE:
timeoutMs = 3 * 60 * 1000;
break;
case PadPlusServer_pb_1.ApiType.GET_MESSAGE_MEDIA:
timeoutMs = 5 * 60 * 1000;
break;
case PadPlusServer_pb_1.ApiType.SEARCH_CONTACT:
case PadPlusServer_pb_1.ApiType.ADD_CONTACT:
case PadPlusServer_pb_1.ApiType.CREATE_ROOM:
case PadPlusServer_pb_1.ApiType.GET_ROOM_QRCODE:
timeoutMs = 1 * 60 * 1000;
break;
default:
timeoutMs = 5 * 1000;
break;
}
const timeout = setTimeout(() => __awaiter(this, void 0, void 0, function* () {
if (apiType !== PadPlusServer_pb_1.ApiType.HEARTBEAT) {
yield this.checkTimeout(uin);
}
reject(new Error(`ApiType: ${apiType} request timeout, traceId: ${traceId}`));
}), timeoutMs);
callbackHelper_1.CallbackPool.Instance.pushCallbackToPool(traceId, (data) => {
const _traceId = data.getTraceid();
if (!_traceId) {
config_1.log.error(PRE, `Can not get trace id by type: ${apiType}`);
}
if (traceId === _traceId) {
clearTimeout(timeout);
if (apiType !== PadPlusServer_pb_1.ApiType.HEARTBEAT) {
this.timeoutNumber = 0;
}
resolve(data);
}
});
});
}
}
catch (err) {
yield new Promise(resolve => setTimeout(resolve, 5000));
this.isAlive = false;
this.client.close();
Object.values(this.eventEmitterMap).map(emitter => {
emitter.emit('reconnect');
});
if (err.details === 'INVALID_TOKEN') {
config_1.padplusToken();
}
}
return null;
});
}
_request(request) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve, reject) => {
try {
this.client.request(request, (err, response) => {
if (err !== null) {
reject(err);
}
else {
const flag = response.getResult();
if (flag) {
resolve(true);
}
else {
reject(err);
}
}
});
}
catch (error) {
throw new Error(`can not get response data of grpc request`);
}
});
});
}
stop() {
return __awaiter(this, void 0, void 0, function* () {
config_1.log.silly(PRE, `stop()`);
if (this.stream) {
this.stream.destroy();
this.stream.removeAllListeners();
}
this.client.close();
this.stopping = true;
try {
yield this.request(PadPlusServer_pb_1.ApiType.CLOSE, '');
}
catch (error) {
config_1.log.error(PRE, `error : ${util_1.default.inspect(error)}`);
}
if (!this.throttleQueueSubscription || !this.debounceQueueSubscription) {
config_1.log.verbose(PRE, `releaseQueue() subscriptions have been released.`);
}
else {
this.throttleQueueSubscription.unsubscribe();
this.debounceQueueSubscription.unsubscribe();
this.throttleQueueSubscription = undefined;
this.debounceQueueSubscription = undefined;
}
if (!this.debounceQueue || !this.throttleQueue) {
config_1.log.verbose(PRE, `releaseQueue() queues have been released.`);
}
else {
this.debounceQueue.unsubscribe();
this.throttleQueue.unsubscribe();
this.debounceQueue = undefined;
this.throttleQueue = undefined;
}
});
}
initGrpcGateway() {
return __awaiter(this, void 0, void 0, function* () {
config_1.log.silly(PRE, `initGrpcGateway()`);
const initConfig = new PadPlusServer_pb_1.InitConfig();
initConfig.setToken(this.token);
const channel = this.client.getChannel();
if (channel) {
yield new Promise((resolve, reject) => {
channel.getConnectivityState(true);
const beforeState = channel.getConnectivityState(false);
channel.watchConnectivityState(beforeState, Date.now() + 5000, (err) => {
if (err) {
reject(new Error('Try to connect to server timeout.'));
}
else {
const state = channel.getConnectivityState(false);
if (state !== grpc_1.default.connectivityState.READY) {
reject(new Error(`Failed to connect to server, state changed to ${state}`));
}
else {
resolve();
}
}
});
});
}
else {
throw new Error('No channel for grpc client.');
}
const stream = this.client.init(initConfig);
stream.on('error', (err) => __awaiter(this, void 0, void 0, function* () {
config_1.log.error(PRE, `GRPC SERVER ERROR.
=====================================================================
try to reconnect grpc server, waiting...
=====================================================================
`);
if (err.code === 14 || err.code === 13 || err.code === 2) {
yield new Promise(resolve => setTimeout(resolve, 5000));
this.isAlive = false;
Object.values(this.eventEmitterMap).map(emitter => {
emitter.emit('reconnect');
});
}
else {
config_1.log.error(PRE, `stream error:`, util_1.default.inspect(err));
}
}));
stream.on('end', () => __awaiter(this, void 0, void 0, function* () {
if (this.reconnectStatus) {
config_1.log.error(PRE, `GRPC SERVER END.
=====================================================================
try to reconnect grpc server, waiting...
=====================================================================
`);
yield new Promise(resolve => setTimeout(resolve, 5000));
this.isAlive = false;
if (!this.stopping) {
Object.values(this.eventEmitterMap).map(emitter => {
emitter.emit('reconnect');
});
}
}
else {
config_1.log.info(PRE, `
=====================================================================
DUPLICATE CONNECTED, THIS THREAD WILL EXIT NOW
=====================================================================
`);
process.exit();
}
}));
stream.on('close', () => __awaiter(this, void 0, void 0, function* () {
config_1.log.error(PRE, 'GRPC SERVER CLOSE');
this.isAlive = false;
Object.values(this.eventEmitterMap).map(emitter => {
emitter.emit('reconnect');
});
}));
stream.on('data', (data) => __awaiter(this, void 0, void 0, function* () {
const traceId = data.getTraceid();
const responseType = data.getResponsetype();
if (responseType !== PadPlusServer_pb_1.ResponseType.LOGIN_QRCODE && responseType !== PadPlusServer_pb_1.ResponseType.ROOM_QRCODE && responseType !== PadPlusServer_pb_1.ResponseType.CONTACT_SELF_QRCODE_GET) {
config_1.log.silly(`==P==A==D==P==L==U==S==<GRPC DATA>==P==A==D==P==L==U==S==`);
config_1.log.silly(PRE, `responseType: ${responseType}, data : ${data.getData()}`);
config_1.log.silly(`==P==A==D==P==L==U==S==<GRPC DATA>==P==A==D==P==L==U==S==\n`);
}
if (this.debounceQueue && this.throttleQueue) {
this.debounceQueue.next(data);
this.throttleQueue.next(data);
}
let message = '';
const _data = data.getData();
if (_data) {
try {
message = JSON.parse(_data).message;
}
catch (error) {
config_1.log.error(`can not parse data`);
}
}
if (message && message === 'Another instance connected, disconnected the current one.') {
this.reconnectStatus = false;
}
else if (message && message === 'EXPIRED_TOKEN') {
Object.values(this.eventEmitterMap).map(emitter => {
this.reconnectStatus = false;
emitter.emit('EXPIRED_TOKEN');
});
}
else if (message && message === 'INVALID_TOKEN') {
Object.values(this.eventEmitterMap).map(emitter => {
this.reconnectStatus = false;
emitter.emit('INVALID_TOKEN');
});
}
if (traceId) {
let callback = callbackHelper_1.CallbackPool.Instance.getCallback(traceId);
if (callback) {
callback(data);
callbackHelper_1.CallbackPool.Instance.removeCallback(traceId);
}
else {
yield new Promise(resolve => {
setTimeout(resolve, 500);
});
try {
callback = callbackHelper_1.CallbackPool.Instance.getCallback(traceId);
callback(data);
callbackHelper_1.CallbackPool.Instance.removeCallback(traceId);
}
catch (error) {
throw new Error(`can not find callback by traceId : ${traceId}`);
}
}
}
else {
if (responseType === PadPlusServer_pb_1.ResponseType.LOGIN_QRCODE) {
const name = Object.keys(this.eventEmitterMap).find(name => {
const qrcodeId = this.eventEmitterMap[name].getQrcodeId();
const uin = this.eventEmitterMap[name].getUIN();
const userName = this.eventEmitterMap[name].getUserName();
config_1.log.silly(PRE, `uin : ${uin}, userName: ${userName}`);
return qrcodeId === '' && uin === '' && userName === '';
});
if (name) {
this.eventEmitterMap[name].emit('data', data);
}
}
else {
const uin = data.getUin();
try {
const user = JSON.parse(data.getData());
const userName = user.userName;
const emitter = Object.values(this.eventEmitterMap).find(em => em.getUIN() === uin || em.getQrcodeId() === userName);
if (!emitter) {
return;
}
if (responseType === PadPlusServer_pb_1.ResponseType.QRCODE_SCAN && user.status === 3 && user.qrcodeId !== emitter.getQrcodeId()) {
return;
}
else {
emitter.emit('data', data);
}
}
catch (error) {
throw new Error(`can not parse json data from grpc server.`);
}
}
}
}));
this.stream = stream;
});
}
}
exports.GrpcGateway = GrpcGateway;
GrpcGateway._instance = undefined;
//# sourceMappingURL=grpc-gateway.js.map