324 lines
7.8 KiB
TypeScript
324 lines
7.8 KiB
TypeScript
import util from 'util';
|
|
|
|
import {
|
|
Plugin,
|
|
EventListener,
|
|
Configurable,
|
|
InjectService,
|
|
Auto
|
|
} from '@squeebot/core/lib/plugin';
|
|
|
|
import { EMessageType, Formatter, IMessage, IMessageTarget, Protocol } from '@squeebot/core/lib/types';
|
|
|
|
import { logger } from '@squeebot/core/lib/core';
|
|
import { IIRCMessage, IRC } from './irc';
|
|
|
|
import { IRCFormatter } from './format';
|
|
|
|
class IRCMessage implements IMessage {
|
|
public time: Date = new Date();
|
|
public resolved = false;
|
|
public direct = false;
|
|
|
|
constructor(
|
|
public type: EMessageType,
|
|
public data: any,
|
|
public source: Protocol,
|
|
public sender: IMessageTarget,
|
|
public target?: IMessageTarget,
|
|
public guest = true) {}
|
|
|
|
public get fullSenderID(): string {
|
|
return this.source.fullName + '/' + this.sender.id;
|
|
}
|
|
|
|
public get fullRoomID(): string {
|
|
if (!this.target) {
|
|
return this.source.fullName;
|
|
}
|
|
|
|
return this.source.fullName + '/' + this.target.id;
|
|
}
|
|
|
|
public get text(): string {
|
|
return this.data;
|
|
}
|
|
|
|
public resolve(...args: any[]): void {
|
|
this.resolved = true;
|
|
this.source.resolve(this, ...args);
|
|
}
|
|
|
|
public reject(error: Error): void {
|
|
this.resolved = true;
|
|
this.source.resolve(this, error.message);
|
|
}
|
|
|
|
public mention(user: IMessageTarget): string {
|
|
return user.name;
|
|
}
|
|
}
|
|
|
|
class IRCProtocol extends Protocol {
|
|
public format: Formatter = new IRCFormatter(true, true);
|
|
public type = 'IRCProtocol';
|
|
|
|
private irc: IRC = new IRC(this.config.irc);
|
|
private eventsAttached = false;
|
|
|
|
public start(...args: any[]): void {
|
|
this.runEvents();
|
|
|
|
this.me.id = this.me.name = this.irc.options.nick;
|
|
this.irc.connect();
|
|
|
|
this.running = true;
|
|
this.emit('running');
|
|
}
|
|
|
|
public stop(force = false): void {
|
|
if (!this.running) {
|
|
return;
|
|
}
|
|
|
|
if (this.irc.alive) {
|
|
this.irc.disconnect();
|
|
}
|
|
|
|
this.running = false;
|
|
this.stopped = true;
|
|
|
|
if (force) {
|
|
this.failed = true;
|
|
}
|
|
|
|
this.emit('stopped');
|
|
}
|
|
|
|
private validateNick(nickname: string, cb: Function): void {
|
|
if (!this.config.nickserv || !this.config.nickserv.enabled) {
|
|
return cb(true); // Assume the user is authentic
|
|
}
|
|
|
|
let stop = false;
|
|
const promiseTimeout = setTimeout(() => {
|
|
stop = true;
|
|
cb(false);
|
|
}, 4000);
|
|
|
|
this.irc.emit('testnick', {
|
|
nickname,
|
|
func: (result: boolean) => {
|
|
clearTimeout(promiseTimeout);
|
|
if (stop) {
|
|
return;
|
|
}
|
|
cb(result);
|
|
}
|
|
});
|
|
}
|
|
|
|
private runEvents(): void {
|
|
if (this.eventsAttached) {
|
|
return;
|
|
}
|
|
this.eventsAttached = true;
|
|
|
|
this.irc.on('authenticated', () => {
|
|
logger.log('[%s] Instance started successfully.', this.fullName);
|
|
this.me.id = this.me.name = this.irc.options.nick;
|
|
});
|
|
|
|
this.irc.on('error', (errdat) => {
|
|
if (errdat.fatal) {
|
|
this.stop();
|
|
}
|
|
logger.error('[%s] Instance error:', this.fullName, errdat.error.message);
|
|
});
|
|
|
|
this.irc.on('disconnect', (data) => {
|
|
logger.warn('[%s] Instance disconnected:', this.fullName, data.message);
|
|
this.stop();
|
|
});
|
|
|
|
// Pass events from IRC to the main channel, where it will then be routed
|
|
// to the list of handler plugins within a configured channel.
|
|
|
|
this.irc.on('leave', (data) => {
|
|
const left = data.channel ? { id: data.channel, name: data.channel } : undefined;
|
|
const newMessage = new IRCMessage(
|
|
EMessageType.roomLeave,
|
|
{},
|
|
this,
|
|
{ id: data.nickname, name: data.nickname },
|
|
left);
|
|
this.plugin.stream.emitTo('channel', 'event', newMessage);
|
|
});
|
|
|
|
this.irc.on('join', (data) => {
|
|
const newMessage = new IRCMessage(
|
|
EMessageType.roomJoin,
|
|
{},
|
|
this,
|
|
{ id: data.nickname, name: data.nickname },
|
|
{ id: data.channel, name: data.channel });
|
|
this.plugin.stream.emitTo('channel', 'event', newMessage);
|
|
});
|
|
|
|
this.irc.on('nick', (data) => {
|
|
const newMessage = new IRCMessage(
|
|
EMessageType.nameChange,
|
|
data.oldNick,
|
|
this,
|
|
{ id: data.newNick, name: data.newNick });
|
|
this.plugin.stream.emitTo('channel', 'event', newMessage);
|
|
});
|
|
|
|
this.irc.on('message', (msg: IIRCMessage) => {
|
|
this.validateNick(msg.nickname, (valid: boolean) => {
|
|
const to = msg.to === this.irc.options.nick ? msg.nickname : msg.to;
|
|
const newMessage = new IRCMessage(
|
|
EMessageType.message,
|
|
msg.message,
|
|
this,
|
|
{ id: msg.nickname, name: msg.nickname },
|
|
{ id: to, name: to },
|
|
valid === false);
|
|
|
|
if (msg.to === this.irc.options.nick) {
|
|
newMessage.direct = true;
|
|
}
|
|
|
|
this.plugin.stream.emitTo('channel', 'message', newMessage);
|
|
});
|
|
});
|
|
}
|
|
|
|
public resolve(message: IMessage, ...data: any[]): void {
|
|
const response = this.parseMessage(...data);
|
|
|
|
if (!this.irc.alive || !response) {
|
|
return;
|
|
}
|
|
|
|
this.irc.message(message.target!.id, response);
|
|
}
|
|
|
|
public async sendTo(target: string, ...data: any[]): Promise<boolean> {
|
|
const response = this.parseMessage(...data);
|
|
|
|
if (!this.irc.alive || !response) {
|
|
return false;
|
|
}
|
|
|
|
const rxSplit = target.split('/');
|
|
if (rxSplit.length !== 3 || rxSplit[0] !== 'irc' || rxSplit[1] !== this.name) {
|
|
return false;
|
|
}
|
|
|
|
this.irc.message(rxSplit[2], response);
|
|
return true;
|
|
}
|
|
|
|
private parseMessage(...data: any[]): string | null {
|
|
let response = util.format(data[0], ...data.slice(1));
|
|
if (!response) {
|
|
return null;
|
|
}
|
|
|
|
if (Array.isArray(data[0])) {
|
|
try {
|
|
response = this.format.compose(data[0]);
|
|
} catch (e: any) {
|
|
logger.error('[%s] Failed to compose message:', this.fullName, e.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return response;
|
|
}
|
|
}
|
|
|
|
/*
|
|
TODO: Control system
|
|
Temporary documentation:
|
|
{
|
|
name: 'service-name',
|
|
restart: false,
|
|
irc: {
|
|
nick: 'Squeebot',
|
|
host: 'localhost',
|
|
port: 6667,
|
|
password: null,
|
|
sasl: false,
|
|
ssl: false,
|
|
channels: [],
|
|
nickserv: {
|
|
enabled: false,
|
|
command: 'STATUS'
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
|
|
@InjectService(IRCProtocol)
|
|
@Configurable({ instances: [] })
|
|
class IRCServicePlugin extends Plugin {
|
|
@Auto()
|
|
initialize(): void {
|
|
const protoList = this.validateConfiguration();
|
|
this.startAll(protoList);
|
|
}
|
|
|
|
private startAll(list: any[]): void {
|
|
for (const ins of list) {
|
|
const newProto = new IRCProtocol(this, ins);
|
|
logger.log('[%s] Starting IRC service "%s".', this.name, ins.name);
|
|
this.monitor(newProto);
|
|
this.service?.use(newProto, true);
|
|
}
|
|
}
|
|
|
|
private monitor(proto: Protocol): void {
|
|
proto.on('running', () => this.emit('protocolNew', proto));
|
|
proto.on('stopped', () => this.emit('protocolExit', proto));
|
|
}
|
|
|
|
private validateConfiguration(): any[] {
|
|
if (!this.config.config.instances) {
|
|
throw new Error('Configuration incomplete!');
|
|
}
|
|
|
|
const instances = this.config.config.instances;
|
|
const runnables: any[] = [];
|
|
for (const ins of instances) {
|
|
if (!ins.name || !ins.irc) {
|
|
throw new Error('Invalid instance configuration!');
|
|
}
|
|
|
|
const irc = ins.irc;
|
|
if (!irc.nick || !irc.host) {
|
|
logger.warn('[%s] Instance named %s was skipped for invalid configuration.',
|
|
this.name, ins.name);
|
|
continue;
|
|
}
|
|
|
|
runnables.push(ins);
|
|
}
|
|
|
|
return runnables;
|
|
}
|
|
|
|
@EventListener('pluginUnload')
|
|
unloadEventHandler(plugin: string | Plugin): void {
|
|
if (plugin === this.name || plugin === this) {
|
|
logger.debug('[%s]', this.name, 'shutting down..');
|
|
this.config.save().then(() =>
|
|
this.service!.stopAll().then(() =>
|
|
this.emit('pluginUnloaded', this)));
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = IRCServicePlugin;
|