From de9a0a078b09d6ebea58299ec8965484bb54cb16 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Mon, 2 Jan 2023 19:26:01 +0000 Subject: [PATCH] Two appservice bot commands to test with. One lists unstarted mjolnirs, and one allows you to try restart them. --- .../bot/AppserviceCommandHandler.ts | 72 +++++++++++++++ src/appservice/bot/ListCommand.ts | 91 +++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 src/appservice/bot/AppserviceCommandHandler.ts create mode 100644 src/appservice/bot/ListCommand.ts diff --git a/src/appservice/bot/AppserviceCommandHandler.ts b/src/appservice/bot/AppserviceCommandHandler.ts new file mode 100644 index 00000000..b7e00633 --- /dev/null +++ b/src/appservice/bot/AppserviceCommandHandler.ts @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2022 Gnuxie + * All rights reserved. + */ + +import { WeakEvent } from 'matrix-appservice-bridge'; +import { readCommand } from '../../commands/interface-manager/CommandReader'; +import { defineCommandTable, defineInterfaceCommand, findCommandTable } from '../../commands/interface-manager/InterfaceCommand'; +import { defineMatrixInterfaceAdaptor, findMatrixInterfaceAdaptor, MatrixContext } from '../../commands/interface-manager/MatrixInterfaceAdaptor'; +import { ArgumentStream, paramaters } from '../../commands/interface-manager/ParamaterParsing'; +import { MjolnirAppService } from '../AppService'; +import { CommandResult } from '../../commands/interface-manager/Validation'; +import { renderHelp } from '../../commands/interface-manager/MatrixHelpRenderer'; + +defineCommandTable("appservice bot"); + +export interface AppserviceContext extends MatrixContext { + appservice: MjolnirAppService; +} + +export type AppserviceBaseExecutor = (this: AppserviceContext, ...args: any[]) => Promise>; + +import '../../commands/interface-manager/MatrixPresentations'; +import './ListCommand'; + + +const helpCommand = defineInterfaceCommand({ + paramaters: paramaters([]), + table: "appservice bot", + command: async function() { return CommandResult.Ok(findCommandTable("appservice bot").getCommands()) }, + designator: ["help"], + summary: "Display this message" +}) + +defineMatrixInterfaceAdaptor({ + interfaceCommand: helpCommand, + renderer: renderHelp +}) + +export class AppserviceCommandHandler { + private readonly commandTable = findCommandTable("appservice bot"); + + constructor ( + private readonly appservice: MjolnirAppService + ) { + + } + + public handleEvent(mxEvent: WeakEvent): void { + if (mxEvent.type !== 'm.room.message' && mxEvent.room_id !== this.appservice.config.adminRoom) { + return; + } + const body = typeof mxEvent.content['body'] === 'string' ? mxEvent.content['body'] : ''; + if (body.startsWith(this.appservice.bridge.getBot().getUserId())) { + const readItems = readCommand(body).slice(1); // remove "!mjolnir" + const argumentStream = new ArgumentStream(readItems); + const command = this.commandTable.findAMatchingCommand(argumentStream); + // FIXME: what if there's no command, print help? + if (command) { + const adaptor = findMatrixInterfaceAdaptor(command); + const context: AppserviceContext = { + appservice: this.appservice, + roomId: mxEvent.room_id, + event: mxEvent, + client: this.appservice.bridge.getBot().getClient() + }; + adaptor.invoke(context, context, ...argumentStream.rest()); + return; + } + } + } +} diff --git a/src/appservice/bot/ListCommand.ts b/src/appservice/bot/ListCommand.ts new file mode 100644 index 00000000..db2b1746 --- /dev/null +++ b/src/appservice/bot/ListCommand.ts @@ -0,0 +1,91 @@ +/** + * Copyright (C) 2022 Gnuxie + * All rights reserved. + */ + +import { defineMatrixInterfaceAdaptor, MatrixContext, MatrixInterfaceAdaptor } from '../../commands/interface-manager/MatrixInterfaceAdaptor'; +import { MatrixSendClient } from '../../MatrixEmitter'; +import { htmlEscape } from '../../utils'; +import { UnstartedMjolnir } from '../MjolnirManager'; +import { BaseFunction, defineInterfaceCommand } from '../../commands/interface-manager/InterfaceCommand'; +import { findPresentationType, paramaters } from '../../commands/interface-manager/ParamaterParsing'; +import { AppserviceBaseExecutor } from './AppserviceCommandHandler'; +import { UserID } from 'matrix-bot-sdk'; +import { CommandError, CommandResult } from '../../commands/interface-manager/Validation'; +import { tickCrossRenderer } from '../../commands/interface-manager/MatrixHelpRenderer'; + +/** + * There is ovbiously something we're doing very wrong here, + * all just to satisfy type contstraints. + * Really there should only be `defineApplicationCommand` + * and `defineMatrixInterfaceCommand` but we have to do this + * long winded dance just to make type contstraints work. + * There might be a better way, but at what cost. + */ + +const listUnstarted = defineInterfaceCommand({ + designator: ["list", "unstarted"], + table: "appservice bot", + paramaters: paramaters([]), + command: async function() { + return CommandResult.Ok(this.appservice.mjolnirManager.getUnstartedMjolnirs()); + }, + summary: "List any Mjolnir that failed to start." +}); + +defineMatrixInterfaceAdaptor({ + interfaceCommand: listUnstarted, + renderer: async function(this: MatrixInterfaceAdaptor, client: MatrixSendClient, commandRoomId: string, event: any, result: CommandResult) { + tickCrossRenderer.call(this, client, commandRoomId, event, result); // don't await, it doesn't really matter. + if (result.isErr()) { + return; // just let the default handler deal with it. + } + const unstarted = result.ok; + let text = ''; + let html = ''; + html += `Unstarted mjolnir: ${unstarted.length}
`; + text += `Unstarted mjolnir: ${unstarted.length}\n`; + + html += "
  • "; + for (const mjolnir of unstarted) { + html += `
      ${htmlEscape(mjolnir.mjolnirRecord.owner)}, ${htmlEscape(mjolnir.mxid.toString())}, ${mjolnir.failCode}:\n${mjolnir.cause}
    ` + text += ` * ${htmlEscape(mjolnir.mjolnirRecord.owner)}, ${htmlEscape(mjolnir.mxid.toString())}, ${mjolnir.failCode}: ${mjolnir.cause}`; + } + html += "
  • "; + await client.sendMessage(commandRoomId, { + msgtype: 'm.notice', + body: text, + formatted_body: html, + }); + } +}) + +// We need a "default" adaptor that needs to be explicitly defined still +// (since you need to know if you have not created an adaptor) +// but can be composed onto the end of existing adaptors easily +// e.g. read recipt and tick vs cross. +const restart = defineInterfaceCommand({ + designator: ["restart"], + table: "appservice bot", + paramaters: paramaters([ + { + name: "mjolnir", + acceptor: findPresentationType("UserID"), + } + ]), + command: async(context, mjolnirId: UserID): Promise> => { + const mjolnirManager = context.appservice.mjolnirManager; + const mjolnir = mjolnirManager.findUnstartedMjolnir(mjolnirId.localpart); + if (mjolnir?.mjolnirRecord === undefined) { + return CommandError.Result(`We can't find the unstarted mjolnir ${mjolnirId}, is it running?`); + } + await mjolnirManager.startMjolnir(mjolnir?.mjolnirRecord); + return CommandResult.Ok(true); + }, + summary: "Attempt to restart a Mjolnir." +}) + +defineMatrixInterfaceAdaptor({ + interfaceCommand: restart, + renderer: tickCrossRenderer +})