Files
Draupnir/src/commands/interface-manager/InterfaceCommand.ts
T
Gnuxie c936332442 Ban/Unban rework + Prompts for missing arguments (#12)
* basic ban conversion, but i have better ideas

* Still very WIP on CLIM prompt-for-accept semantics.

* Introduce promotable streams.

This allows parameters to specify details to prompt for missing
arguments
and allow for interactive commands.

* Changes that were made before PolicyListManager that no longer make sense

We don't want the default list anymore since we're just going to prompt
with the lists that they can choose from.

* Fix semantics of TagDynamicEnvironment.

Bind and write were wrong and bind was binding to the node name
instead of the variable name.

* The JSX factory can render presentation types to DocumentNodes, unsure if this is the right
move yet but it works

* Attributes for anchor nodes now render properly

* Ban command prompts are working!!!!

* Stub AppserviceBotEmitter.

There isn't much we can do right now until there is time to work on
https://github.com/Gnuxie/Draupnir/issues/13.

* Combine ban/unban syntax.

* Remove old UnbanBanCommands.

WARNING: There is a major difference in that the ban command no longer supports
globs, I don't think?

* Activate new unban command.

* The presentation type boolean will have to be just a string for now.

I don't think it makes sense to read them into actual booleans.

* configurable defaults for ban reason.
2023-02-08 12:50:23 +00:00

194 lines
8.0 KiB
TypeScript

/**
* Copyright (C) 2022 Gnuxie <Gnuxie@protonmail.com>
* All rights reserved.
*
* This file incorperates work from mjolnir
* https://github.com/matrix-org/mjolnir
* Which includes the following license notice:
*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*
* However, this file is modified and the modifications in this file
* are NOT distributed, contributed, or committed under the Apache License.
*/
/**
* When we do move these components into their own library,
* I'd like to remove the dependency on matrix-bot-sdk.
*/
import { ParamaterParser, IArgumentStream, IArgumentListParser, ParsedKeywords, ArgumentStream } from "./ParamaterParsing";
import { CommandResult } from "./Validation";
/**
* 💀 . o O ( at least I don't have to remember the types )
* https://matrix-client.matrix.org/_matrix/media/r0/download/matrix.org/nbisOFhCcTzNicZrfixWMHZn
* Probably am "doing something wrong", and no, trying to make this protocol isn't it.
*/
export type BaseFunction = (keywords: ParsedKeywords, ...args: any) => Promise<CommandResult<any>>;
type CommandLookupEntry<ExecutorType extends BaseFunction> = {
next?: Map<string, CommandLookupEntry<ExecutorType>>,
current?: InterfaceCommand<ExecutorType>
};
export class CommandTable<ExecutorType extends BaseFunction> {
private readonly flattenedCommands = new Set<InterfaceCommand<BaseFunction>>();
private readonly commands: CommandLookupEntry<ExecutorType> = { };
constructor() {
}
/**
* Used to render the help command.
* @returns All of the commands in this table.
*/
public getCommands(): InterfaceCommand<BaseFunction>[] {
return [...this.flattenedCommands.values()];
}
// We use the argument stream so that they can use stream.rest() to get the unconsumed arguments.
public findAMatchingCommand(stream: IArgumentStream) {
const tableHelper = (table: CommandLookupEntry<ExecutorType>, argumentStream: IArgumentStream): undefined|InterfaceCommand<ExecutorType> => {
if (argumentStream.peekItem() === undefined || typeof argumentStream.peekItem() !== 'string') {
// Then they might be using something like "!mjolnir status"
return table.current;
}
const entry = table.next?.get(argumentStream.readItem() as string);
if (!entry) {
// The reason there's no match is because this is the command arguments, rather than subcommand notation.
return table.current;
} else {
return tableHelper(entry, argumentStream);
}
};
return tableHelper(this.commands, stream);
}
public internCommand(command: InterfaceCommand<ExecutorType>) {
const internCommandHelper = (table: CommandLookupEntry<ExecutorType>, designator: string[]): void => {
if (designator.length === 0) {
if (table.current) {
throw new TypeError(`There is already a command for ${JSON.stringify(designator)}`)
}
table.current = command;
this.flattenedCommands.add(command);
} else {
if (table.next === undefined) {
table.next = new Map();
}
const nextLookupEntry = {};
table.next!.set(designator.shift()!, nextLookupEntry);
internCommandHelper(nextLookupEntry, designator);
}
}
internCommandHelper(this.commands, [...command.designator]);
}
}
const COMMAND_TABLE_TABLE = new Map<string|symbol, CommandTable<BaseFunction>>();
export function defineCommandTable(name: string|symbol) {
if (COMMAND_TABLE_TABLE.has(name)) {
throw new TypeError(`A table called ${name.toString()} already exists`);
}
COMMAND_TABLE_TABLE.set(name, new CommandTable());
}
export function findCommandTable<ExecutorType extends BaseFunction>(name: string|symbol): CommandTable<ExecutorType> {
const entry = COMMAND_TABLE_TABLE.get(name);
if (!entry) {
throw new TypeError(`Couldn't find a table called ${name.toString()}`);
}
return entry as CommandTable<ExecutorType>;
}
/**
* Used to find a table command at the internal DSL level, not as a client for commands.
*/
export function findTableCommand<ExecutorType extends BaseFunction>(tableName: string|symbol, ...designator: string[]): InterfaceCommand<ExecutorType> {
const table = findCommandTable(tableName);
const command = table.findAMatchingCommand(new ArgumentStream(designator));
if (command === undefined || !designator.every(part => command.designator.includes(part))) {
throw new TypeError(`Could not find a table command in the table ${tableName.toString()} with the designator ${JSON.stringify(designator)}`)
}
return command as InterfaceCommand<ExecutorType>;
}
export class InterfaceCommand<ExecutorType extends BaseFunction> {
constructor(
public readonly argumentListParser: IArgumentListParser,
private readonly command: ExecutorType,
public readonly designator: string[],
/** A short one line summary of what the command does to display alongside it's help */
public readonly summary: string,
/** A longer description that goes into detail. */
public readonly description?: string,
) {
}
// Really, surely this should be part of invoke?
// probably... it's just that means that invoke has to return the validation result lol.
// Though this makes no sense if parsing is part of finding a matching command.
public async parseArguments(stream: IArgumentStream): ReturnType<ParamaterParser> {
return await this.argumentListParser.parse(stream);
}
public invoke(context: ThisParameterType<ExecutorType>, ...args: Parameters<ExecutorType>): ReturnType<ExecutorType> {
return this.command.apply(context, args);
}
public async parseThenInvoke(context: ThisParameterType<ExecutorType>, stream: IArgumentStream): Promise<ReturnType<ExecutorType>> {
const paramaterDescription = await this.parseArguments(stream);
if (paramaterDescription.isErr()) {
// The inner type is irrelevant when it is Err, i don't know how to encode this in TS's type system but whatever.
return paramaterDescription as ReturnType<Awaited<ExecutorType>>;
}
return await this.command.apply(context, [
paramaterDescription.ok.keywords,
...paramaterDescription.ok.immediateArguments,
...paramaterDescription.ok.rest ?? []
]);
}
}
// Shouldn't there be a callback interface.
// imagine the old ban or sync command, each time you check a room
// you add a callback to say when a user has been banned or a room
// has been cleared or there was an error applying a ban to that room.
// There could be a description in defineInterfaceComomand
// for what each callback is and does for the adaptors to hook into.
export function defineInterfaceCommand<ExecutorType extends BaseFunction>(description: {
paramaters: IArgumentListParser,
table: string|symbol,
command: ExecutorType,
designator: string[],
summary: string,
description?: string,
}) {
const command = new InterfaceCommand<ExecutorType>(
description.paramaters,
description.command,
description.designator,
description.summary,
description.description,
);
const table = findCommandTable(description.table);
table.internCommand(command);
return command;
}