service-discord/discord/plugin.ts

228 lines
5.7 KiB
TypeScript

import {
Plugin,
Configurable,
EventListener,
InjectService
} from '@squeebot/core/lib/plugin';
import util from 'util';
import Discord from 'discord.js';
import { logger } from '@squeebot/core/lib/core';
import {
EMessageType,
Formatter,
IMessage,
IMessageTarget,
MarkdownFormatter,
Protocol
} from '@squeebot/core/lib/types';
class DiscordMessageAdapter 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 get fullSenderID(): string {
return this.source.fullName + '/' + this.sender.id;
}
public get fullRoomID(): string {
if (!this.target) {
return this.source.fullName;
}
if (this.target.server) {
return this.source.fullName + '/s:' + this.target.server + '/' + this.target.id;
}
return this.source.fullName + '/' + this.target.id;
}
public get text(): string {
return this.data.content;
}
public resolve(...args: any[]): void {
this.resolved = true;
this.source.resolve(this, ...args);
}
public mention(user: IMessageTarget): string {
return `<@${user.id}>`;
}
}
class DiscordProtocol extends Protocol {
public format: Formatter = new MarkdownFormatter();
public type = 'DiscordProtocol';
private client = new Discord.Client();
private eventsAttached = false;
public start(...args: any[]): void {
this.running = true;
this.client.once('ready', () => {
if (this.client.user) {
this.me.id = this.client.user.id;
this.me.name = this.client.user.username;
}
this.emit('running');
});
this.attachEvents();
this.client.login(this.config.token);
}
public stop(force = false): void {
if (!this.running) {
return;
}
this.running = false;
this.stopped = true;
this.client.destroy();
if (force) {
this.failed = true;
}
this.emit('stopped');
}
private attachEvents(): void {
if (this.eventsAttached) {
return;
}
this.eventsAttached = true;
this.client.on('error', (e) => this.emit('error', e));
this.client.on('disconnect', () => this.stop(true));
this.client.on('message', (message) => {
if (this.me && this.me.id && message.author.id === this.me.id) {
return;
}
const chanName = 'name' in message.channel ? message.channel.name : message.author.username;
const msg = new DiscordMessageAdapter(EMessageType.message,
message,
this,
{ name: message.author.username, id: message.author.id },
{ name: chanName, id: message.channel.id, server: message.guild ? message.guild.id : undefined });
this.plugin.stream.emitTo('channel', 'message', msg);
});
this.client.on('guildMemberAdd', (member) => {
if (this.me && this.me.id && member.user.id === this.me.id) {
return;
}
const newMessage = new DiscordMessageAdapter(
EMessageType.roomJoin,
member,
this,
{ id: member.user.id, name: member.user.username },
{ server: member.guild.id, id: '', name: ''});
this.plugin.stream.emitTo('channel', 'event', newMessage);
});
this.client.on('guildMemberRemove', (member) => {
if (!member.user) {
return;
}
if (this.me && this.me.id && member.user.id === this.me.id) {
return;
}
const newMessage = new DiscordMessageAdapter(
EMessageType.roomLeave,
member,
this,
{ id: member.user.id, name: member.user.username },
{ server: member.guild.id, id: '', name: ''});
this.plugin.stream.emitTo('channel', 'event', newMessage);
});
}
public resolve(msg: DiscordMessageAdapter, ...data: any[]): void {
let response = util.format(data[0], ...data.slice(1));
if (!response) {
return;
}
if (Array.isArray(data[0])) {
try {
response = this.format.compose(data[0]);
} catch (e) {
logger.error('[%s] Failed to compose message:', this.fullName, e.message);
return;
}
}
msg.data.channel.send(response).catch((e: Error) => {
logger.error(e);
});
}
}
@InjectService(DiscordProtocol)
@Configurable({instances: []})
class DiscordServicePlugin extends Plugin {
initialize(): void {
const protoList = this.validateConfiguration();
this.startAll(protoList);
}
private startAll(list: any[]): void {
for (const ins of list) {
const newProto = new DiscordProtocol(this, ins);
logger.log('[%s] Starting Discord 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.enabled === false) {
continue;
}
if (!ins.name || !ins.token) {
throw new Error('Invalid instance configuration!');
}
runnables.push(ins);
}
return runnables;
}
@EventListener('pluginUnload')
public unloadEventHandler(plugin: string | Plugin): void {
if (plugin === this.name || plugin === this) {
this.config.save().then(() =>
this.service?.stopAll().then(() =>
this.emit('pluginUnloaded', this)));
}
}
}
module.exports = DiscordServicePlugin;