diff --git a/src/commands/CommandHandler.ts b/src/commands/CommandHandler.ts index bef687b..4ea42a6 100644 --- a/src/commands/CommandHandler.ts +++ b/src/commands/CommandHandler.ts @@ -28,8 +28,10 @@ import { execRedactCommand } from "./RedactCommand"; import { execImportCommand } from "./ImportCommand"; import { execSetDefaultListCommand } from "./SetDefaultBanListCommand"; import { execDeactivateCommand } from "./DeactivateCommand"; -import { execDisableProtection, execEnableProtection, execListProtections, execConfigGetProtection, - execConfigSetProtection, execConfigAddProtection, execConfigRemoveProtection } from "./ProtectionsCommands"; +import { + execDisableProtection, execEnableProtection, execListProtections, execConfigGetProtection, + execConfigSetProtection, execConfigAddProtection, execConfigRemoveProtection +} from "./ProtectionsCommands"; import { execListProtectedRooms } from "./ListProtectedRoomsCommand"; import { execAddProtectedRoom, execRemoveProtectedRoom } from "./AddRemoveProtectedRoomsCommand"; import { execAddRoomToDirectoryCommand, execRemoveRoomFromDirectoryCommand } from "./AddRemoveRoomFromDirectoryCommand"; @@ -43,198 +45,20 @@ 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.token("command"); // Consume `!mjolnir`. - const cmd = lexer.token("id").text; + const cmd = lexer.alternatives( + () => lexer.token("id").text, + () => null + ); console.debug("YORIC", "cmd", cmd); try { - if (cmd === '' || cmd === 'status') { + if (cmd === null || 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 8057b95..126a140 100644 --- a/src/commands/Lexer.ts +++ b/src/commands/Lexer.ts @@ -58,28 +58,39 @@ export class Lexer extends TokenizrClass { }); // Dates and durations. - this.rule("dateOrDuration", /\S+/, (ctx, match) => { - let date = new Date(match[0]); - if (!date || Number.isNaN(date.getDate())) { - let duration = parseDuration(match[0]); - if (!duration || Number.isNaN(duration)) { - ctx.reject(); + try { + this.rule("dateOrDuration", /(?:"([^"]+)")|(\S+)/, (ctx, match) => { + let content = match[1] || match[2]; + console.debug("YORIC", "Lexer", "dateOrDuration", content); + let date = new Date(content); + console.debug("YORIC", "Lexer", "dateOrDuration", "date", date); + if (!date || Number.isNaN(date.getDate())) { + let duration = parseDuration(content); + console.debug("YORIC", "Lexer", "dateOrDuration", "duration", duration); + if (!duration || Number.isNaN(duration)) { + ctx.reject(); + } else { + ctx.accept("duration", duration); + } } else { - ctx.accept("duration", duration); + ctx.accept("date", date); } - } else { - ctx.accept("date", date); - } - }); + }); + } catch (ex) { + console.error("YORIC", ex); + } // Jokers. this.rule("STAR", /\*/, (ctx) => { ctx.accept("STAR"); }); - this.rule(/./, (ctx) => { - ctx.accept("ANYTHING ELSE") + + // Everything left in the string. + this.rule("ETC", /.*/, (ctx) => { + ctx.accept("ETC") }); + console.debug("YORIC", "Preparing lexer", string); this.input(string); } diff --git a/src/commands/SinceCommand.ts b/src/commands/SinceCommand.ts index 3870855..4143887 100644 --- a/src/commands/SinceCommand.ts +++ b/src/commands/SinceCommand.ts @@ -39,7 +39,13 @@ type Summary = { succeeded: userId[], failed: userId[] }; // !mjolnir since / [...rooms] [...reason] export async function execSinceCommand(destinationRoomId: string, event: any, mjolnir: Mjolnir, lexer: Lexer) { - let result = await execSinceCommandAux(destinationRoomId, event, mjolnir, lexer); + let result; + try { + result = await execSinceCommandAux(destinationRoomId, event, mjolnir, lexer); + } catch (ex) { + result = { error: ex.message }; + console.error("Error executing `since` command", ex); + } if ("error" in result) { mjolnir.client.unstableApis.addReactionToEvent(destinationRoomId, event['event_id'], '❌'); mjolnir.logMessage(LogLevel.WARN, "SinceCommand", result.error); @@ -70,21 +76,16 @@ function formatResult(action: string, targetRoomId: string, recentJoins: Join[], // - 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> { // Attempt to parse `` as a date or duration. - let dateOrDurationToken: Date | number; - try { - dateOrDurationToken = lexer.token("dateOrDuration").value; - } catch (ex) { - return { error: "Invalid " }; - } - console.debug("YORIC", "dateOrDurationToken", dateOrDurationToken); + let dateOrDuration: Date |number = lexer.token("dateOrDuration").value; + console.debug("YORIC", "dateOrDuration", dateOrDuration); let minDate; let maxAgeMS; - if (dateOrDurationToken instanceof Date) { - minDate = dateOrDurationToken; - maxAgeMS = Date.now() - dateOrDurationToken.getTime() as number; + if (dateOrDuration instanceof Date) { + minDate = dateOrDuration; + maxAgeMS = Date.now() - dateOrDuration.getTime() as number; } else { - minDate = new Date(Date.now() - dateOrDurationToken); - maxAgeMS = dateOrDurationToken; + minDate = new Date(Date.now() - dateOrDuration); + maxAgeMS = dateOrDuration; } // Attempt to parse `` as Action. @@ -109,27 +110,29 @@ async function execSinceCommandAux(destinationRoomId: string, event: any, mjolni // Now list affected rooms. const rooms: Set = new Set(); do { - try { - let token = lexer.alternatives( - () => lexer.token("STAR"), - () => lexer.token("roomAliasOrID"), - ); - if (token.type == "STAR") { - for (let roomId of Object.keys(mjolnir.protectedRooms)) { - rooms.add(roomId); - } - continue; - } - if (token.type == "roomAliasOrID") { - const roomId = await mjolnir.client.resolveRoom(token.text); - if (!(roomId in mjolnir.protectedRooms)) { - return mjolnir.logMessage(LogLevel.WARN, "SinceCommand", `This room is not protected: ${htmlEscape(roomId)}.`); - } + let token = lexer.alternatives( + () => lexer.token("STAR"), + () => lexer.token("roomAliasOrID"), + ); + console.debug("YORIC", "token", token); + if (!token) { + // We have reached the end of rooms. + break; + } + if (token.type === "STAR") { + for (let roomId of Object.keys(mjolnir.protectedRooms)) { rooms.add(roomId); - continue; } - } catch (ex) { - // If we're done with rooms, we have entered . + continue; + } else if (token.type === "roomAliasOrID") { + const roomId = await mjolnir.client.resolveRoom(token.text); + if (!(roomId in mjolnir.protectedRooms)) { + return mjolnir.logMessage(LogLevel.WARN, "SinceCommand", `This room is not protected: ${htmlEscape(roomId)}.`); + } + rooms.add(roomId); + continue; + } + if (token.type == 'EOF') { break; } } while(true); @@ -143,8 +146,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.token("string"), - () => lexer.token("EVERYTHING ELSE") - ).text; + () => lexer.token("ETC") + )?.text || ""; console.debug("YORIC", "reason", reason); const progressEventId = await mjolnir.client.unstableApis.addReactionToEvent(destinationRoomId, event['event_id'], '⏳');