-
Notifications
You must be signed in to change notification settings - Fork 56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor command-line handling #335
base: main
Are you sure you want to change the base?
Changes from 5 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"; | ||
|
@@ -38,90 +40,93 @@ import { execShutdownRoomCommand } from "./ShutdownRoomCommand"; | |
import { execAddAliasCommand, execMoveAliasCommand, execRemoveAliasCommand, execResolveCommand } from "./AliasCommands"; | ||
import { execKickCommand } from "./KickCommand"; | ||
import { execMakeRoomAdminCommand } from "./MakeRoomAdminCommand"; | ||
import { parse as tokenize } from "shell-quote"; | ||
import { execSinceCommand } from "./SinceCommand"; | ||
|
||
import { Lexer } from "./Lexer"; | ||
|
||
export const COMMAND_PREFIX = "!mjolnir"; | ||
|
||
export async function handleCommand(roomId: string, event: { content: { body: string } }, mjolnir: Mjolnir) { | ||
const cmd = event['content']['body']; | ||
const parts = cmd.trim().split(' ').filter(p => p.trim().length > 0); | ||
const line = event['content']['body']; | ||
const parts = line.trim().split(' ').filter(p => p.trim().length > 0); | ||
|
||
// A shell-style parser that can parse `"a b c"` (with quotes) as a single argument. | ||
// We do **not** want to parse `#` as a comment start, though. | ||
const tokens = tokenize(cmd.replace("#", "\\#")).slice(/* get rid of ["!mjolnir", command] */ 2); | ||
const lexer = new Lexer(line); | ||
lexer.token("command"); // Consume `!mjolnir`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what happens when the lexer fails then? how different is it from what happens now? |
||
// Extract command. | ||
const cmd = lexer.alternatives( | ||
() => lexer.token("id").text, | ||
() => null | ||
); | ||
|
||
try { | ||
if (parts.length === 1 || parts[1] === 'status') { | ||
if (cmd === null || cmd === 'status') { | ||
return await execStatusCommand(roomId, event, mjolnir, parts.slice(2)); | ||
} else if (parts[1] === 'ban' && parts.length > 2) { | ||
} else if (cmd === 'ban' && parts.length > 2) { | ||
return await execBanCommand(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'unban' && parts.length > 2) { | ||
} else if (cmd === 'unban' && parts.length > 2) { | ||
return await execUnbanCommand(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'rules' && parts.length === 4 && parts[2] === 'matching') { | ||
} else if (cmd === 'rules' && parts.length === 4 && parts[2] === 'matching') { | ||
return await execRulesMatchingCommand(roomId, event, mjolnir, parts[3]) | ||
} else if (parts[1] === 'rules') { | ||
} else if (cmd === 'rules') { | ||
return await execDumpRulesCommand(roomId, event, mjolnir); | ||
} else if (parts[1] === 'sync') { | ||
} else if (cmd === 'sync') { | ||
return await execSyncCommand(roomId, event, mjolnir); | ||
} else if (parts[1] === 'verify') { | ||
} else if (cmd === 'verify') { | ||
return await execPermissionCheckCommand(roomId, event, mjolnir); | ||
} else if (parts.length >= 5 && parts[1] === 'list' && parts[2] === 'create') { | ||
} else if (parts.length >= 5 && cmd === 'list' && parts[2] === 'create') { | ||
return await execCreateListCommand(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'watch' && parts.length > 1) { | ||
} else if (cmd === 'watch' && parts.length > 1) { | ||
return await execWatchCommand(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'unwatch' && parts.length > 1) { | ||
} else if (cmd === 'unwatch' && parts.length > 1) { | ||
return await execUnwatchCommand(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'redact' && parts.length > 1) { | ||
} else if (cmd === 'redact' && parts.length > 1) { | ||
return await execRedactCommand(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'import' && parts.length > 2) { | ||
} else if (cmd === 'import' && parts.length > 2) { | ||
return await execImportCommand(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'default' && parts.length > 2) { | ||
} else if (cmd === 'default' && parts.length > 2) { | ||
return await execSetDefaultListCommand(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'deactivate' && parts.length > 2) { | ||
} else if (cmd === 'deactivate' && parts.length > 2) { | ||
return await execDeactivateCommand(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'protections') { | ||
} else if (cmd === 'protections') { | ||
return await execListProtections(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'enable' && parts.length > 1) { | ||
} else if (cmd === 'enable' && parts.length > 1) { | ||
return await execEnableProtection(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'disable' && parts.length > 1) { | ||
} else if (cmd === 'disable' && parts.length > 1) { | ||
return await execDisableProtection(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'config' && parts[2] === 'set' && parts.length > 3) { | ||
} else if (cmd === 'config' && parts[2] === 'set' && parts.length > 3) { | ||
return await execConfigSetProtection(roomId, event, mjolnir, parts.slice(3)) | ||
} else if (parts[1] === 'config' && parts[2] === 'add' && parts.length > 3) { | ||
} else if (cmd === 'config' && parts[2] === 'add' && parts.length > 3) { | ||
return await execConfigAddProtection(roomId, event, mjolnir, parts.slice(3)) | ||
} else if (parts[1] === 'config' && parts[2] === 'remove' && parts.length > 3) { | ||
} else if (cmd === 'config' && parts[2] === 'remove' && parts.length > 3) { | ||
return await execConfigRemoveProtection(roomId, event, mjolnir, parts.slice(3)) | ||
} else if (parts[1] === 'config' && parts[2] === 'get') { | ||
} else if (cmd === 'config' && parts[2] === 'get') { | ||
return await execConfigGetProtection(roomId, event, mjolnir, parts.slice(3)) | ||
} else if (parts[1] === 'rooms' && parts.length > 3 && parts[2] === 'add') { | ||
} else if (cmd === 'rooms' && parts.length > 3 && parts[2] === 'add') { | ||
return await execAddProtectedRoom(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'rooms' && parts.length > 3 && parts[2] === 'remove') { | ||
} else if (cmd === 'rooms' && parts.length > 3 && parts[2] === 'remove') { | ||
return await execRemoveProtectedRoom(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'rooms' && parts.length === 2) { | ||
} else if (cmd === 'rooms' && parts.length === 2) { | ||
return await execListProtectedRooms(roomId, event, mjolnir); | ||
} else if (parts[1] === 'move' && parts.length > 3) { | ||
} else if (cmd === 'move' && parts.length > 3) { | ||
return await execMoveAliasCommand(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'directory' && parts.length > 3 && parts[2] === 'add') { | ||
} else if (cmd === 'directory' && parts.length > 3 && parts[2] === 'add') { | ||
return await execAddRoomToDirectoryCommand(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'directory' && parts.length > 3 && parts[2] === 'remove') { | ||
} else if (cmd === 'directory' && parts.length > 3 && parts[2] === 'remove') { | ||
return await execRemoveRoomFromDirectoryCommand(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'alias' && parts.length > 4 && parts[2] === 'add') { | ||
} else if (cmd === 'alias' && parts.length > 4 && parts[2] === 'add') { | ||
return await execAddAliasCommand(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'alias' && parts.length > 3 && parts[2] === 'remove') { | ||
} else if (cmd === 'alias' && parts.length > 3 && parts[2] === 'remove') { | ||
return await execRemoveAliasCommand(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'resolve' && parts.length > 2) { | ||
} else if (cmd === 'resolve' && parts.length > 2) { | ||
return await execResolveCommand(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'powerlevel' && parts.length > 3) { | ||
} else if (cmd === 'powerlevel' && parts.length > 3) { | ||
return await execSetPowerLevelCommand(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'shutdown' && parts[2] === 'room' && parts.length > 3) { | ||
} else if (cmd === 'shutdown' && parts[2] === 'room' && parts.length > 3) { | ||
return await execShutdownRoomCommand(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'since') { | ||
return await execSinceCommand(roomId, event, mjolnir, tokens); | ||
} else if (parts[1] === 'kick' && parts.length > 2) { | ||
} else if (cmd === 'since') { | ||
return await execSinceCommand(roomId, event, mjolnir, lexer); | ||
} else if (cmd === 'kick' && parts.length > 2) { | ||
return await execKickCommand(roomId, event, mjolnir, parts); | ||
} else if (parts[1] === 'make' && parts[2] === 'admin' && parts.length > 3) { | ||
} else if (cmd === 'make' && parts[2] === 'admin' && parts.length > 3) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, we should certainly figure out how to get rid of this giant switch statement and use a table to dispatch commands instead. We'd also be able to isolate commands and stop editing this file each time we need to add/remove one too by doing this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Absolutely, that's the objective of the refactoring. |
||
return await execMakeRoomAdminCommand(roomId, event, mjolnir, parts); | ||
} else { | ||
// Help menu | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import Tokenizr from "tokenizr"; | ||
|
||
// For some reason, different versions of TypeScript seem | ||
// to disagree on how to import Tokenizr | ||
import * as TokenizrModule from "tokenizr"; | ||
import { parseDuration } from "../utils"; | ||
const TokenizrClass = Tokenizr || TokenizrModule; | ||
|
||
const WHITESPACE = /\s+/; | ||
const COMMAND = /![a-zA-Z_]+/; | ||
Yoric marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const IDENTIFIER = /[a-zA-Z_]+/; | ||
const USER_ID = /@[a-zA-Z0-9_.=\-/]+:\S+/; | ||
const GLOB_USER_ID = /@[a-zA-Z0-9_.=\-?*/]+:\S+/; | ||
const ROOM_ID = /![a-zA-Z0-9_.=\-/]+:\S+/; | ||
const ROOM_ALIAS = /#[a-zA-Z0-9_.=\-/]+:\S+/; | ||
const ROOM_ALIAS_OR_ID = /[#!][a-zA-Z0-9_.=\-/]+:\S+/; | ||
const INT = /[+-]?[0-9]+/; | ||
const STRING = /"((?:\\"|[^\r\n])*)"/; | ||
const DATE_OR_DURATION = /(?:"([^"]+)")|([^"]\S+)/; | ||
const STAR = /\*/; | ||
const ETC = /.*$/; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why keep these separate from the lexer rules? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I kinda expect that we're going to need some of these regexps in other places, but I may be wrong. |
||
|
||
/** | ||
* A lexer for command parsing. | ||
* | ||
* Recommended use is `lexer.token("state")`. | ||
*/ | ||
export class Lexer extends TokenizrClass { | ||
constructor(string: string) { | ||
super(); | ||
// Ignore whitespace. | ||
this.rule(WHITESPACE, (ctx) => { | ||
ctx.ignore() | ||
}) | ||
|
||
// Command rules, e.g. `!mjolnir` | ||
this.rule("command", COMMAND, (ctx) => { | ||
ctx.accept("command"); | ||
}); | ||
|
||
// Identifier rules, used e.g. for subcommands `get`, `set` ... | ||
Yoric marked this conversation as resolved.
Show resolved
Hide resolved
|
||
this.rule("id", IDENTIFIER, (ctx) => { | ||
ctx.accept("id"); | ||
}); | ||
|
||
// Users | ||
this.rule("userID", USER_ID, (ctx) => { | ||
ctx.accept("userID"); | ||
}); | ||
this.rule("globUserID", GLOB_USER_ID, (ctx) => { | ||
ctx.accept("globUserID"); | ||
}); | ||
|
||
// Rooms | ||
this.rule("roomID", ROOM_ID, (ctx) => { | ||
ctx.accept("roomID"); | ||
}); | ||
this.rule("roomAlias", ROOM_ALIAS, (ctx) => { | ||
ctx.accept("roomAlias"); | ||
}); | ||
this.rule("roomAliasOrID", ROOM_ALIAS_OR_ID, (ctx) => { | ||
ctx.accept("roomAliasOrID"); | ||
}); | ||
|
||
// Numbers. | ||
this.rule("int", INT, (ctx, match) => { | ||
ctx.accept("int", parseInt(match[0])) | ||
}); | ||
|
||
// Quoted strings. | ||
this.rule("string", STRING, (ctx, match) => { | ||
ctx.accept("string", match[1].replace(/\\"/g, "\"")) | ||
}); | ||
|
||
// Dates and durations. | ||
this.rule("dateOrDuration", DATE_OR_DURATION, (ctx, match) => { | ||
let content = match[1] || match[2]; | ||
let date = new Date(content); | ||
if (!date || Number.isNaN(date.getDate())) { | ||
let duration = parseDuration(content); | ||
if (!duration || Number.isNaN(duration)) { | ||
ctx.reject(); | ||
} else { | ||
ctx.accept("duration", duration); | ||
} | ||
} else { | ||
ctx.accept("date", date); | ||
} | ||
}); | ||
|
||
// Jokers. | ||
this.rule("STAR", STAR, (ctx) => { | ||
ctx.accept("STAR"); | ||
}); | ||
|
||
// Everything left in the string. | ||
this.rule("ETC", ETC, (ctx, match) => { | ||
ctx.accept("ETC", match[0].trim()); | ||
}); | ||
|
||
this.input(string); | ||
} | ||
|
||
public token(state?: string): TokenizrModule.Token { | ||
if (typeof state === "string") { | ||
this.state(state); | ||
} | ||
return super.token(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oopsie