service-matrix/matrix/plugin.ts

272 lines
6.9 KiB
TypeScript

import {
Plugin,
Configurable,
EventListener,
InjectService
} from '@squeebot/core/lib/plugin';
import util from 'util';
import {
MatrixClient,
AutojoinRoomsMixin,
RichReply,
SimpleFsStorageProvider,
LogService,
LogLevel,
} from 'matrix-bot-sdk';
import { logger } from '@squeebot/core/lib/core';
import {
EMessageType,
Formatter,
HTMLFormatter,
IMessage,
IMessageTarget,
Protocol
} from '@squeebot/core/lib/types';
class MatrixMessageAdapter 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;
}
return this.source.fullName + '/' + this.target.id;
}
public get text(): string {
return this.data.content.body;
}
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 `<a href="https://matrix.to/#/${user.id}">${user.id}</a>`;
}
}
class MatrixProtocol extends Protocol {
public format: Formatter = new HTMLFormatter();
public type = 'MatrixProtocol';
public nameCache: Map<string, string> = new Map<string, string>();
private client?: MatrixClient;
public async handler(roomId: any, event: any): Promise<void> {
// Don't handle events that don't have contents (they were probably redacted)
if (!this.client || !this.me || !event.content) { return; }
// Don't handle non-text events
if (event.content.msgtype !== 'm.text') { return; }
const msg = event.content.body;
// filter out events sent by the bot itself
if (event.sender === this.me.id || !msg) { return; }
const senderName = await this.getUserDisplayName(event.sender);
const roomName = await this.getRoomDisplayName(roomId);
const newMessage = new MatrixMessageAdapter(
EMessageType.message,
event,
this,
{ id: event.sender, name: senderName, },
{ id: roomId, name: roomName, }
);
this.plugin.stream.emitTo('channel', 'message', newMessage);
}
public async getUserDisplayName(id: string): Promise<string> {
if (this.nameCache.has(id)) {
return this.nameCache.get(id) as string;
}
const profile = await this.client?.getUserProfile(id);
if (!profile || !profile.displayname) {
return id;
}
this.nameCache.set(id, profile.displayname);
return profile.displayname;
}
public async getRoomDisplayName(id: string): Promise<string> {
if (this.nameCache.has(id)) {
return this.nameCache.get(id) as string;
}
const roomState = await this.client?.getRoomStateEvent(id, 'm.room.name', '');
if (!roomState || !roomState.name) {
return id;
}
this.nameCache.set(id, roomState.name);
return roomState.name;
}
private async getSelf(): Promise<void> {
if (!this.client) {
return;
}
const id = await this.client.getUserId();
const name = await this.getUserDisplayName(id);
this.me = { id, name };
}
public start(...args: any[]): void {
const stpath = `.matrix-${this.config.name}.db.json`;
const storage = new SimpleFsStorageProvider(stpath);
this.client = new MatrixClient(this.config.homeserver, this.config.token, storage);
AutojoinRoomsMixin.setupOnClient(this.client);
LogService.setLevel(LogLevel.ERROR);
this.client.on('room.message', (...dargs) =>
this.handler(dargs[0], dargs[1])
.catch(e => logger.error(e)));
this.client.start()
.then(() => {
this.running = true;
logger.info('[%s] Protocol "%s" ready', this.plugin.manifest.name, this.config.name);
this.getSelf();
})
.catch((e) => {
this.stop(true);
logger.error('[%s] Protocol "%s" failed to start',
this.plugin.manifest.name, this.config.name);
});
}
public stop(force = false): void {
if (!this.running && !force) {
return;
}
if (this.client) {
this.client.stop();
this.client = undefined;
}
this.running = false;
this.stopped = true;
if (force) {
this.failed = true;
}
this.emit('stopped');
}
public resolve(msg: MatrixMessageAdapter, ...data: any[]): void {
if (!msg.target) {
return;
}
this.sendTo(`${this.fullName}/${msg.target.id}`, ...data);
}
public async sendTo(target: string, ...data: any[]): Promise<boolean> {
let response = util.format(data[0], ...data.slice(1));
const rxSplit = target.split('/');
if (!response || !target || rxSplit.length !== 3) {
return false;
}
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 false;
}
}
await this.client?.sendMessage(rxSplit[2], {
msgtype: 'm.text',
body: this.format.strip(response),
format: 'org.matrix.custom.html',
formatted_body: response.replace('\n', '<br />'),
});
return true;
}
}
@InjectService(MatrixProtocol)
@Configurable({instances: []})
class MatrixServicePlugin extends Plugin {
initialize(): void {
const protoList = this.validateConfiguration();
this.startAll(protoList);
}
private startAll(list: any[]): void {
for (const ins of list) {
const newProto = new MatrixProtocol(this, ins);
logger.log('[%s] Starting Matrix 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 || !ins.homeserver) {
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 = MatrixServicePlugin;