diff --git a/src/commands/CommandHandler.ts b/src/commands/CommandHandler.ts index 73de0a8..bef687b 100644 --- a/src/commands/CommandHandler.ts +++ b/src/commands/CommandHandler.ts @@ -41,18 +41,200 @@ import { execMakeRoomAdminCommand } from "./MakeRoomAdminCommand"; import { execSinceCommand } from "./SinceCommand"; import { Lexer } from "./Lexer"; - export const COMMAND_PREFIX = "!mjolnir"; +type Command = { + cmd: string, + help: string, + code: (roomId: string, event: {content: {body: string}}, mjolnir: Mjolnir, parts: string[], lexer: Lexer) => Promise, +}; +const COMMANDS: Command[] = [{ + cmd: '', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execStatusCommand(roomId, event, mjolnir, parts.slice(2)) +}, + { + cmd: 'ban', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execBanCommand(roomId, event, mjolnir, parts), +}, + { + cmd: 'unban', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execUnbanCommand(roomId, event, mjolnir, parts), +}, + { + cmd: 'rules', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execRulesMatchingCommand(roomId, event, mjolnir, parts[3]), +}, +{ + cmd: 'rules', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execDumpRulesCommand(roomId, event, mjolnir), +}, + { + cmd: 'sync', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execSyncCommand(roomId, event, mjolnir), +}, + { + cmd: 'verify', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execPermissionCheckCommand(roomId, event, mjolnir), +}, + { + cmd: 'list', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execCreateListCommand(roomId, event, mjolnir, parts), +}, + { + cmd: 'watch', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execWatchCommand(roomId, event, mjolnir, parts), +}, + { + cmd: 'unwatch', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execUnwatchCommand(roomId, event, mjolnir, parts), +}, + { + cmd: 'redact', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execRedactCommand(roomId, event, mjolnir, parts), +}, + { + cmd: 'import', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execImportCommand(roomId, event, mjolnir, parts), +}, + { + cmd: 'default', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execSetDefaultListCommand(roomId, event, mjolnir, parts), +}, + { + cmd: 'deactivate', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execDeactivateCommand(roomId, event, mjolnir, parts), +}, + { + cmd: 'protections', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execListProtections(roomId, event, mjolnir, parts), +}, + { + cmd: 'enable', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execEnableProtection(roomId, event, mjolnir, parts), +}, + { + cmd: 'disable', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execDisableProtection(roomId, event, mjolnir, parts), +}, + { + cmd: 'config', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execConfigSetProtection(roomId, event, mjolnir, parts.slice(3)), +}, + { + cmd: 'config', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execConfigAddProtection(roomId, event, mjolnir, parts.slice(3)), +}, + { + cmd: 'config', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execConfigRemoveProtection(roomId, event, mjolnir, parts.slice(3)), +}, + { + cmd: 'config', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execConfigGetProtection(roomId, event, mjolnir, parts.slice(3)), +}, + { + cmd: 'rooms', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execAddProtectedRoom(roomId, event, mjolnir, parts), +}, + { + cmd: 'rooms', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execRemoveProtectedRoom(roomId, event, mjolnir, parts), +}, + { + cmd: 'rooms', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execListProtectedRooms(roomId, event, mjolnir), +}, + { + cmd: 'move', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execMoveAliasCommand(roomId, event, mjolnir, parts), +}, + { + cmd: 'directory', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execAddRoomToDirectoryCommand(roomId, event, mjolnir, parts), +}, + { + cmd: 'directory', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execRemoveRoomFromDirectoryCommand(roomId, event, mjolnir, parts), +}, + { + cmd: 'alias', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execAddAliasCommand(roomId, event, mjolnir, parts), +}, + { + cmd: 'alias', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execRemoveAliasCommand(roomId, event, mjolnir, parts), +}, + { + cmd: 'resolve', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execResolveCommand(roomId, event, mjolnir, parts), +}, + { + cmd: 'powerlevel', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execSetPowerLevelCommand(roomId, event, mjolnir, parts), +}, + { + cmd: 'shutdown', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execShutdownRoomCommand(roomId, event, mjolnir, parts), +}, + { + cmd: 'since', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execSinceCommand(roomId, event, mjolnir, lexer), +}, + { + cmd: 'kick', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execKickCommand(roomId, event, mjolnir, parts), +}, + { + cmd: 'make', + help: "FIXME", + code: (roomId, event, mjolnir, parts, lexer) => execMakeRoomAdminCommand(roomId, event, mjolnir, parts), +}]; + export async function handleCommand(roomId: string, event: { content: { body: string } }, mjolnir: Mjolnir) { const line = event['content']['body']; const parts = line.trim().split(' ').filter(p => p.trim().length > 0); + console.debug("YORIC", "line", line); const lexer = new Lexer(line); - lexer.consume("command"); // Consume `!mjolnir`. - let cmd = parts.length === 1 ? null : lexer.consume("id").text; + lexer.token("command"); // Consume `!mjolnir`. + const cmd = lexer.token("id").text; + console.debug("YORIC", "cmd", cmd); try { - if (parts.length === 1 || cmd === 'status') { + if (cmd === '' || cmd === 'status') { return await execStatusCommand(roomId, event, mjolnir, parts.slice(2)); } else if (cmd === 'ban' && parts.length > 2) { return await execBanCommand(roomId, event, mjolnir, parts); diff --git a/src/commands/Lexer.ts b/src/commands/Lexer.ts index f26904c..8057b95 100644 --- a/src/commands/Lexer.ts +++ b/src/commands/Lexer.ts @@ -18,47 +18,47 @@ export class Lexer extends TokenizrClass { ctx.ignore() }) + // Command rules, e.g. `!mjolnir` + this.rule("command", /![a-zA-Z_]+/, (ctx) => { + ctx.accept("command"); + }); + // Identifier rules, used e.g. for subcommands `get`, `set` ... - this.rule(/[a-zA-Z_]+/, (ctx) => { + this.rule("id", /[a-zA-Z_]+/, (ctx) => { ctx.accept("id"); }); - // User IDs - this.rule(/@[a-zA-Z0-9_.=\-/]+:.+/, (ctx) => { + // Users + this.rule("userID", /@[a-zA-Z0-9_.=\-/]+:.+/, (ctx) => { ctx.accept("userID"); }); - this.rule(/@[a-zA-Z0-9_.=\-?*/]+:.+/, (ctx) => { + this.rule("globUserID", /@[a-zA-Z0-9_.=\-?*/]+:.+/, (ctx) => { ctx.accept("globUserID"); }); - // User IDs - this.rule(/![a-zA-Z0-9_.=\-/]+:.+/, (ctx) => { + // Rooms + this.rule("roomID", /![a-zA-Z0-9_.=\-/]+:.+/, (ctx) => { ctx.accept("roomID"); }); - this.rule(/#[a-zA-Z0-9_.=\-/]+:.+/, (ctx) => { + this.rule("roomAlias", /#[a-zA-Z0-9_.=\-/]+:.+/, (ctx) => { ctx.accept("roomAlias"); }); - this.rule(/[#!][a-zA-Z0-9_.=\-/]+:.+/, (ctx) => { + this.rule("roomAliasOrID", /[#!][a-zA-Z0-9_.=\-/]+:.+/, (ctx) => { ctx.accept("roomAliasOrID"); }); - + // Numbers. - this.rule(/[+-]?[0-9]+/, (ctx, match) => { + this.rule("int", /[+-]?[0-9]+/, (ctx, match) => { ctx.accept("int", parseInt(match[0])) }); // Quoted strings. - this.rule(/"((?:\\"|[^\r\n])*)"/, (ctx, match) => { + this.rule("string", /"((?:\\"|[^\r\n])*)"/, (ctx, match) => { ctx.accept("string", match[1].replace(/\\"/g, "\"")) }); - // Arbitrary non-space content. - this.rule(/\S+/, (ctx) => { - ctx.accept("nospace"); - }); - // Dates and durations. - this.rule(/\S+/, (ctx, match) => { + this.rule("dateOrDuration", /\S+/, (ctx, match) => { let date = new Date(match[0]); if (!date || Number.isNaN(date.getDate())) { let duration = parseDuration(match[0]); @@ -73,13 +73,20 @@ export class Lexer extends TokenizrClass { }); // Jokers. - this.rule(/\*/, (ctx) => { + this.rule("STAR", /\*/, (ctx) => { ctx.accept("STAR"); }); - this.rule(/.*/, ctx => { - ctx.accept("EVERYTHING ELSE"); + this.rule(/./, (ctx) => { + ctx.accept("ANYTHING ELSE") }); this.input(string); } + + public token(state?: string): TokenizrModule.Token { + if (typeof state === "string") { + this.state(state); + } + return super.token(); + } } diff --git a/src/commands/SinceCommand.ts b/src/commands/SinceCommand.ts index f0bfafd..3870855 100644 --- a/src/commands/SinceCommand.ts +++ b/src/commands/SinceCommand.ts @@ -69,15 +69,10 @@ function formatResult(action: string, targetRoomId: string, recentJoins: Join[], // - attempts to execute action; // - in case of success, returns `{ok: undefined}`, in case of error, returns `{error: string}`. async function execSinceCommandAux(destinationRoomId: string, event: any, mjolnir: Mjolnir, lexer: Lexer): Promise> { - console.debug("YORIC", "lexer", lexer); - // Attempt to parse `` as a date or duration. let dateOrDurationToken: Date | number; try { - dateOrDurationToken = lexer.alternatives( - () => lexer.consume("date"), - () => lexer.consume("duration") - ); + dateOrDurationToken = lexer.token("dateOrDuration").value; } catch (ex) { return { error: "Invalid " }; } @@ -93,7 +88,7 @@ async function execSinceCommandAux(destinationRoomId: string, event: any, mjolni } // Attempt to parse `` as Action. - let actionToken = lexer.consume("id").text; + let actionToken = lexer.token("id").text; let action: Action | null = null; for (let key in Action) { const maybeAction = Action[key as keyof typeof Action]; @@ -108,7 +103,7 @@ async function execSinceCommandAux(destinationRoomId: string, event: any, mjolni console.debug("YORIC", "action", action); // Attempt to parse `` as a number. - const maxEntries = lexer.consume("int").value as number; + const maxEntries = lexer.token("int").value as number; console.debug("YORIC", "maxEntries", maxEntries); // Now list affected rooms. @@ -116,8 +111,8 @@ async function execSinceCommandAux(destinationRoomId: string, event: any, mjolni do { try { let token = lexer.alternatives( - () => lexer.consume("STAR"), - () => lexer.consume("roomAliasOrID"), + () => lexer.token("STAR"), + () => lexer.token("roomAliasOrID"), ); if (token.type == "STAR") { for (let roomId of Object.keys(mjolnir.protectedRooms)) { @@ -147,8 +142,8 @@ async function execSinceCommandAux(destinationRoomId: string, event: any, mjolni // Parse everything else as ``, stripping quotes if any have been added. const reason = lexer.alternatives( - () => lexer.consume("string"), - () => lexer.consume("EVERYTHING ELSE") + () => lexer.token("string"), + () => lexer.token("EVERYTHING ELSE") ).text; console.debug("YORIC", "reason", reason);