Skip to content
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

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Reworking CLI tokenization
  • Loading branch information
Yoric committed Jul 19, 2022
commit 5395a660718e34d678349ee511656113e665e169
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"start:dev": "yarn build && node --async-stack-traces lib/index.js",
"test": "ts-mocha --project ./tsconfig.json test/commands/**/*.ts",
"test:integration": "NODE_ENV=harness ts-mocha --async-stack-traces --require test/integration/fixtures.ts --timeout 300000 --project ./tsconfig.json \"test/integration/**/*Test.ts\"",
"test:YORIC": "NODE_ENV=harness ts-mocha --async-stack-traces --require test/integration/fixtures.ts --timeout 300000 --project ./tsconfig.json \"test/integration/roomMembersTest.ts\"",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oopsie

"test:manual": "NODE_ENV=harness ts-node test/integration/manualLaunchScript.ts",
"version": "sed -i '/# version automated/s/[0-9][0-9]*\\.[0-9][0-9]*\\.[0-9][0-9]*/'$npm_package_version'/' synapse_antispam/setup.py && git add synapse_antispam/setup.py && cat synapse_antispam/setup.py"
},
Expand Down Expand Up @@ -45,7 +46,7 @@
"jsdom": "^16.6.0",
"matrix-bot-sdk": "^0.5.19",
"parse-duration": "^1.0.2",
"shell-quote": "^1.7.3"
"tokenizr": "^1.6.7"
},
"engines": {
"node": ">=16.0.0"
Expand Down
85 changes: 42 additions & 43 deletions src/commands/CommandHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,90 +38,89 @@ 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);

// 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 line = event['content']['body'];
const parts = line.trim().split(' ').filter(p => p.trim().length > 0);
const lexer = new Lexer(line);
lexer.consume("command"); // Consume `!mjolnir`.
let cmd = parts.length === 1 ? null : lexer.consume("id").text;

try {
if (parts.length === 1 || parts[1] === 'status') {
if (parts.length === 1 || 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) {
return await execMakeRoomAdminCommand(roomId, event, mjolnir, parts);
} else {
// Help menu
Expand Down
85 changes: 85 additions & 0 deletions src/commands/Lexer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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;

/**
* A lexer for common cases.
*/
export class Lexer extends TokenizrClass {
constructor(string: string) {
super();

// Ignore whitespace.
this.rule(/\s+/, (ctx) => {
ctx.ignore()
})

// Identifier rules, used e.g. for subcommands `get`, `set` ...
Yoric marked this conversation as resolved.
Show resolved Hide resolved
this.rule(/[a-zA-Z_]+/, (ctx) => {
ctx.accept("id");
});

// User IDs
this.rule(/@[a-zA-Z0-9_.=\-/]+:.+/, (ctx) => {
ctx.accept("userID");
});
this.rule(/@[a-zA-Z0-9_.=\-?*/]+:.+/, (ctx) => {
ctx.accept("globUserID");
});

// User IDs
this.rule(/![a-zA-Z0-9_.=\-/]+:.+/, (ctx) => {
ctx.accept("roomID");
});
this.rule(/#[a-zA-Z0-9_.=\-/]+:.+/, (ctx) => {
ctx.accept("roomAlias");
});
this.rule(/[#!][a-zA-Z0-9_.=\-/]+:.+/, (ctx) => {
ctx.accept("roomAliasOrID");
});

// Numbers.
this.rule(/[+-]?[0-9]+/, (ctx, match) => {
ctx.accept("int", parseInt(match[0]))
});

// Quoted strings.
this.rule(/"((?:\\"|[^\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) => {
let date = new Date(match[0]);
if (!date || Number.isNaN(date.getDate())) {
let duration = parseDuration(match[0]);
if (!duration || Number.isNaN(duration)) {
ctx.reject();
} else {
ctx.accept("duration", duration);
}
} else {
ctx.accept("date", date);
}
});

// Jokers.
this.rule(/\*/, (ctx) => {
ctx.accept("STAR");
});
this.rule(/.*/, ctx => {
ctx.accept("EVERYTHING ELSE");
});

this.input(string);
}
}
Loading