@tsuk1ko/cq-websocket
Version:
A Node SDK for developing QQ chatbots based on WebSocket, which is depending on CoolQ and CQHTTP API plugin.
399 lines (352 loc) • 9.82 kB
JavaScript
const $get = require('lodash.get');
const $traverse = require('./util/traverse');
const CQTag = require('./message/CQTag');
const CQText = require('./message/models/CQText');
class CQEventBus {
constructor(cqbot) {
// eventType-to-handlers mapping
// blank keys refer to default keys
this._EventMap = {
message: {
'': [],
private: [],
group: {
'': [],
'@': {
'': [],
me: [],
},
},
discuss: {
'': [],
'@': {
'': [],
me: [],
},
},
guild: {
'': [],
'@': {
'': [],
me: [],
},
},
},
event: [],
notice: {
'': [],
group_upload: [],
group_admin: {
'': [],
set: [],
unset: [],
},
group_decrease: {
'': [],
leave: [],
kick: [],
kick_me: [],
},
group_increase: {
'': [],
approve: [],
invite: [],
},
group_ban: {
'': [],
ban: [],
lift_ban: [],
},
friend_add: [],
group_recall: [],
friend_recall: [],
notify: {
'': [],
poke: [],
lucky_king: [],
honor: [],
},
group_card: [],
offline_file: [],
client_status: [],
essence: {
'': [],
add: [],
delete: [],
},
},
request: {
'': [],
friend: [],
group: {
'': [],
add: [],
invite: [],
},
},
ready: [],
error: [],
socket: {
connecting: [],
connect: [],
failed: [],
reconnecting: [],
reconnect: [],
reconnect_failed: [],
max_reconnect: [],
error: [],
closing: [],
close: [],
},
api: {
response: [],
send: {
pre: [],
post: [],
},
},
meta_event: {
'': [],
lifecycle: [],
heartbeat: [],
},
};
/**
* A function-to-function mapping
* from the original listener received via #once(event, listener)
* to the once listener wrapper function
* which wraps the original listener
* and is the listener that is actually registered via #on(event, listener)
* @type {WeakMap<Function, Function>}
*/
this._onceListeners = new WeakMap();
this._isSocketErrorHandled = false;
this._bot = cqbot;
// has a default handler; automatically removed when developers register their own ones
this._installDefaultErrorHandler();
}
_getHandlerQueue(eventType) {
let queue = $get(this._EventMap, eventType);
if (Array.isArray(queue)) {
return queue;
}
queue = $get(this._EventMap, `${eventType}.`);
return Array.isArray(queue) ? queue : undefined;
}
count(eventType) {
const queue = this._getHandlerQueue(eventType);
return queue ? queue.length : undefined;
}
has(eventType) {
return this._getHandlerQueue(eventType) !== undefined;
}
emit(eventType, ...args) {
return this._emitThroughHierarchy(eventType, ...args);
}
async _emitThroughHierarchy(eventType, ...args) {
const queue = [];
const isResponsable = eventType.startsWith('message');
for (let hierarchy = eventType.split('.'); hierarchy.length > 0; hierarchy.pop()) {
const currentQueue = this._getHandlerQueue(hierarchy.join('.'));
if (currentQueue && currentQueue.length > 0) {
queue.push(...currentQueue);
}
}
if (queue && queue.length > 0) {
const cqevent = isResponsable ? new CQEvent() : undefined;
if (isResponsable && Array.isArray(args)) {
args.unshift(cqevent);
}
for (const handler of queue) {
if (isResponsable) {
// reset
cqevent._errorHandler = cqevent._responseHandler = cqevent._responseOptions = null;
}
const returned = await Promise.resolve(handler(...args));
if (isResponsable && (typeof returned === 'string' || Array.isArray(returned))) {
cqevent.stopPropagation();
cqevent.setMessage(returned);
}
if (isResponsable && cqevent._isCanceled) {
break;
}
}
if (isResponsable && cqevent.hasMessage()) {
let message = cqevent.getMessage();
message = !Array.isArray(message)
? message
: message
.filter(cqmsg => typeof cqmsg === 'object')
.map(cqmsg => (cqmsg instanceof CQTag ? cqmsg.toJSON() : cqmsg));
this._bot('send_msg', { ...args[1], message }, cqevent._responseOptions)
.then(ctxt => {
if (typeof cqevent._responseHandler === 'function') {
cqevent._responseHandler(ctxt);
}
})
.catch(err => {
if (typeof cqevent._errorHandler === 'function') {
cqevent._errorHandler(err);
} else {
this.emit('error', err);
}
});
}
}
}
once(eventType, handler) {
const onceWrapper = (...args) => {
const returned = handler(...args);
// if the returned value is `false` which indicates the handler have not yet finish its task,
// keep the handler for next event handling
if (returned === false) return;
this.off(eventType, onceWrapper);
return returned;
};
this._onceListeners.set(handler, onceWrapper);
return this.on(eventType, onceWrapper);
}
off(eventType, handler) {
if (typeof eventType !== 'string') {
this._onceListeners = new WeakMap();
$traverse(this._EventMap, value => {
// clean all handler queues
if (Array.isArray(value)) {
value.splice(0, value.length);
return false;
}
});
this._installDefaultErrorHandler();
return this;
}
const queue = this._getHandlerQueue(eventType);
if (queue === undefined) {
return this;
}
if (typeof handler !== 'function') {
// clean all handlers of the event queue specified by eventType
queue.splice(0, queue.length);
if (eventType === 'socket.error') {
this._installDefaultErrorHandler();
}
return this;
}
const idx = queue.indexOf(handler);
const wrapperIdx = this._onceListeners.has(handler) ? queue.indexOf(this._onceListeners.get(handler)) : -1;
// no matter the listener is a once listener wrapper or not,
// the first occurence of the "handler" (2nd arg passed in) or its wrapper will be removed from the queue
const victimIdx =
idx >= 0 && wrapperIdx >= 0 ? Math.min(idx, wrapperIdx) : idx >= 0 ? idx : wrapperIdx >= 0 ? wrapperIdx : -1;
if (victimIdx >= 0) {
queue.splice(victimIdx, 1);
if (victimIdx === wrapperIdx) {
this._onceListeners.delete(handler);
}
if (eventType === 'socket.error' && queue.length === 0) {
this._installDefaultErrorHandler();
}
return this;
}
return this;
}
_installDefaultErrorHandler() {
if (this._EventMap.socket.error.length === 0) {
this._EventMap.socket.error.push(onSocketError);
this._isSocketErrorHandled = false;
}
}
_removeDefaultErrorHandler() {
if (!this._isSocketErrorHandled) {
this._EventMap.socket.error.splice(0, this._EventMap.socket.error.length);
this._isSocketErrorHandled = true;
}
}
/**
* @param {string} eventType
* @param {function} handler
*/
on(eventType, handler) {
// @deprecated
// keep the compatibility for versions lower than v1.5.0
if (eventType.endsWith('@me')) {
eventType = eventType.replace(/\.$/, '.@.me');
}
if (!this.has(eventType)) {
return this;
}
if (eventType === 'socket.error') {
this._removeDefaultErrorHandler();
}
const queue = this._getHandlerQueue(eventType);
if (queue) {
queue.push(handler);
}
return this;
}
}
function onSocketError(which, err) {
err.which = which;
console.error('\nYou should listen on "socket.error" yourself to avoid those unhandled promise warnings.\n');
throw err;
}
class CQEvent {
constructor() {
this._isCanceled = false;
/**
* @type {CQTag[] | string}
*/
this._message = '';
this._responseHandler = null;
this._responseOptions = null;
this._errorHandler = null;
}
get messageFormat() {
return typeof this._message === 'string' ? 'string' : 'array';
}
stopPropagation() {
this._isCanceled = true;
}
getMessage() {
return this._message;
}
hasMessage() {
return typeof this._message === 'string' ? Boolean(this._message) : this._message.length > 0;
}
setMessage(msgIn) {
if (Array.isArray(msgIn)) {
msgIn = msgIn.map(m => (typeof m === 'string' ? new CQText(m) : m));
}
this._message = msgIn;
}
appendMessage(msgIn) {
if (typeof this._message === 'string') {
this._message += String(msgIn);
} else {
if (typeof msgIn === 'string') {
msgIn = new CQText(msgIn);
}
this._message.push(msgIn);
}
}
/**
* @param {(res: object)=>void} handler
*/
onResponse(handler, options) {
if (typeof handler !== 'function') {
options = handler;
handler = null;
}
this._responseHandler = handler;
this._responseOptions = options;
}
/**
* @param {(error: Error)=>void} handler
*/
onError(handler) {
this._errorHandler = handler;
}
}
module.exports = {
CQEventBus,
CQEvent,
};