diff --git a/src/appservice/bot/ListCommand.tsx b/src/appservice/bot/ListCommand.tsx new file mode 100644 index 00000000..e8b2b407 --- /dev/null +++ b/src/appservice/bot/ListCommand.tsx @@ -0,0 +1,97 @@ +/** + * Copyright (C) 2022 Gnuxie + * All rights reserved. + */ + +import { defineMatrixInterfaceAdaptor, MatrixContext, MatrixInterfaceAdaptor } from '../../commands/interface-manager/MatrixInterfaceAdaptor'; +import { MatrixSendClient } from '../../MatrixEmitter'; +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'; +import { JSXFactory } from '../../commands/interface-manager/JSXFactory'; +import { renderMatrixAndSend } from '../../commands/interface-manager/DeadDocumentMatrix'; + +/** + * 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." +}); + +// Hmm what if leter on we used OL and the numbers could be a presentation type +// and be used similar to like #=1 and #1. +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; + await renderMatrixAndSend( +

+ Unstarted Mjolnir: {unstarted.length} +

    + {unstarted.map(mjolnir => { + return
  • + {mjolnir.mjolnirRecord.owner}, + {mjolnir.mxid.toString()} + {mjolnir.failCode}: +
    + {mjolnir.cause} +
  • + })} +
+

, + commandRoomId, + event, + client + ); + } +}) + +// 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 +}) diff --git a/src/commands/interface-manager/DeadDocument.ts b/src/commands/interface-manager/DeadDocument.ts index 5018b089..c12f6b97 100644 --- a/src/commands/interface-manager/DeadDocument.ts +++ b/src/commands/interface-manager/DeadDocument.ts @@ -37,6 +37,8 @@ export enum NodeTag { UnorderedList = 'ul', ListItem = 'li', LineBreak = 'br', + BoldFace = 'b', + ItalicFace = 'i', } /** @@ -57,7 +59,7 @@ export function addChild(this: DeadDocumentN } export function getChildren(this: DeadDocumentNode): (DocumentNode|LeafNode)[] { - return this.children; + return [...this.children]; } export function getFirstChild(this: DeadDocumentNode): DocumentNode|LeafNode|undefined { @@ -271,7 +273,7 @@ export class FringeWalker { this.dynamicEnvironment.pop(node.node); return node.node; } - while(!COMMITTABLE_NODES.has(this.stream.peekItem()?.node.tag)) { + while(this.stream.peekItem() && !COMMITTABLE_NODES.has(this.stream.peekItem().node.tag)) { const node = this.stream.readItem(); switch(node.type) { case FringeType.Pre: @@ -293,35 +295,51 @@ export class FringeWalker { if (this.stream.peekItem() === undefined) { return undefined; } - const node = this.stream.readItem(); - postNode(node); + const node = postNode(this.stream.readItem()); this.commitHook(node, this.context); return node; } } -export interface TagDynamicEnvironmentEntry { - node: DocumentNode, - value: any, +export class TagDynamicEnvironmentEntry { + constructor( + public readonly node: DocumentNode, + public readonly value: any, + public readonly previous: undefined|TagDynamicEnvironmentEntry, + ) { + + } } +// OK we wrote this wrong +// TagDynamicEnvironmentEntries need to be associated with a node +// but not the variables themselves... +// otherwise how can a list item access the indentation width managed by a list or +// ordered list, all three of which have different tags export class TagDynamicEnvironment { - private readonly environments = new Map(); + private readonly environments = new Map(); - public getEnvironment(tag: NodeTag): TagDynamicEnvironmentEntry[] { - return this.environments.get(tag) - ?? ((bootstrap: TagDynamicEnvironmentEntry[]) => (this.environments.set(tag, bootstrap), bootstrap))([]); + public getVariable(variableName: string): any|undefined { + const variableEntry = this.environments.get(variableName); + if (variableEntry) { + return variableEntry.value; + } else { + return undefined; + } } - public bind(node: DocumentNode, value: any) { - const entry = this.getEnvironment(node.tag); - entry.push({ node, value }); + public bind(variableName: string, node: DocumentNode, value: any): any { + const entry = this.environments.get(variableName); + const newEntry = new TagDynamicEnvironmentEntry(node, value, entry); + this.environments.set(node.tag, newEntry); + return value } - public pop(node: DocumentNode) { - const entry = this.getEnvironment(node.tag); - if (Object.is(entry.at(0), node)) { - entry.pop(); + public pop(node: DocumentNode): void { + for (const [variableName, environment] of this.environments.entries()) { + if (Object.is(environment?.node, node)) { + this.environments.set(variableName, environment?.previous); + } } } } diff --git a/src/commands/interface-manager/DeadDocumentHtml.ts b/src/commands/interface-manager/DeadDocumentHtml.ts index ff6201a5..765cd6ba 100644 --- a/src/commands/interface-manager/DeadDocumentHtml.ts +++ b/src/commands/interface-manager/DeadDocumentHtml.ts @@ -41,4 +41,10 @@ HTML_RENDERER.registerRenderer'), +).registerInnerNode(NodeTag.BoldFace, + staticString(''), + staticString('') +).registerInnerNode(NodeTag.ItalicFace, + staticString(''), + staticString('') ); diff --git a/src/commands/interface-manager/DeadDocumentMarkdown.ts b/src/commands/interface-manager/DeadDocumentMarkdown.ts index f4c6e47b..8a41d8d4 100644 --- a/src/commands/interface-manager/DeadDocumentMarkdown.ts +++ b/src/commands/interface-manager/DeadDocumentMarkdown.ts @@ -82,14 +82,26 @@ export class PagedDuplexStream { } public peekPage(): string|undefined { + // We consider a page "ready" when it is no longer the current page. + if (this.pages.length < 2) { + return undefined; + } return this.pages.at(0); } public readPage(): string|undefined { + // We consider a page "ready" when it is no longer the current page. + if (this.pages.length < 2) { + return undefined; + } return this.pages.shift(); } } +enum MarkdownVariables { + IndentationLevel = "indentation level" +} + /** * Hoping to replace this soon? or subclass * FringeWalker so this is just a stream? @@ -107,12 +119,14 @@ export function staticString(string: string): FringeInnerRenderFunction { + const value = environment.getVariable(MarkdownVariables.IndentationLevel); + if (!Number.isInteger(value)) { + throw new TypeError(`Cannot render the list ${node.tag} because someone clobbered the dynamic environment, should only have integers. Did you forget to enclose in
    or
      ?`) + } else { + return value; + } + })(); + context.output.writeString('\n'); - for (let i = 0; i < entry?.value; i++) { + for (let i = 0; i < indentationLevel; i++) { context.output.writeString(' '); } context.output.writeString(' * '); @@ -163,4 +182,10 @@ MARKDOWN_RENDERER.registerRenderer o.ensureNewPage()); + if (outputs.some(o => o.peekPage())) { + await cb(markdownOutput.readPage()!, htmlOutput.readPage()!); + } } export async function renderMatrixAndSend(node: DocumentNode, roomId: string, event: any, client: MatrixSendClient): Promise { diff --git a/src/commands/interface-manager/JSXFactory.ts b/src/commands/interface-manager/JSXFactory.ts index e9a36743..a1c54cba 100644 --- a/src/commands/interface-manager/JSXFactory.ts +++ b/src/commands/interface-manager/JSXFactory.ts @@ -5,22 +5,21 @@ import { DocumentNode, LeafNode, makeDocumentNode, makeLeafNode, NodeTag, TextNode } from "./DeadDocument"; -declare global { - namespace JSX { - interface IntrinsicElements { - [elemName: string]: any; - } - } -} +type rawJSX = DocumentNode|LeafNode|string|number|Array; export function JSXFactory(tag: NodeTag, properties: any, ...rawChildren: (DocumentNode|LeafNode|string)[]) { const node = makeDocumentNode(tag); - const ensureChild = (rawChild: DocumentNode|LeafNode|string) => { + const ensureChild = (rawChild: rawJSX) => { if (typeof rawChild === 'string') { - return makeLeafNode(NodeTag.TextNode, node, rawChild); - } else { + makeLeafNode(NodeTag.TextNode, node, rawChild); + } else if (typeof rawChild === 'number') { + makeLeafNode(NodeTag.TextNode, node, (rawChild as number).toString()); + } else if (Array.isArray(rawChild)) { + rawChild.forEach(ensureChild); + } else if (typeof rawChild.leafNode === 'boolean'){ node.addChild(rawChild); - return rawChild; + } else { + throw new TypeError(`Unexpected raw child ${JSON.stringify(rawChild)}`) } } rawChildren.forEach(ensureChild); @@ -34,3 +33,16 @@ namespace JSXFactory { } } +/** + * Pisses me off that tooling is too dumb to use the above + * https://www.typescriptlang.org/docs/handbook/declaration-merging.html + * https://www.typescriptlang.org/tsconfig#jsxFactory + */ +declare global { + export namespace JSX { + export interface IntrinsicElements { + [elemName: string]: any; + } + } +} +