Files
Draupnir/src/commands/CommandHandler.ts
T

222 lines
16 KiB
TypeScript

/**
* Copyright (C) 2022 Gnuxie <Gnuxie@protonmail.com>
* All rights reserved.
*
* This file is modified and is NOT licensed under the Apache License.
* This modified file incorperates work from mjolnir
* https://github.com/matrix-org/mjolnir
* which included the following license notice:
Copyright 2019-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, committed, or licensed under the Apache License.
*/
import { Mjolnir } from "../Mjolnir";
import { showJoinsStatus } from "./JoinsCommand";
import { execDumpRulesCommand, execRulesMatchingCommand } from "./DumpRulesCommand";
import { LogService, RichReply } from "matrix-bot-sdk";
import { htmlEscape } from "../utils";
import { execSyncCommand } from "./SyncCommand";
import { execPermissionCheckCommand } from "./PermissionCheckCommand";
import { execCreateListCommand } from "./CreateBanListCommand";
import { execUnwatchCommand, execWatchCommand } from "./WatchUnwatchCommand";
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 { execListProtectedRooms } from "./ListProtectedRoomsCommand";
import { execAddProtectedRoom, execRemoveProtectedRoom } from "./AddRemoveProtectedRoomsCommand";
import { execAddRoomToDirectoryCommand, execRemoveRoomFromDirectoryCommand } from "./AddRemoveRoomFromDirectoryCommand";
import { execSetPowerLevelCommand } from "./SetPowerLevelCommand";
import { execShutdownRoomCommand } from "./ShutdownRoomCommand";
import { execAddAliasCommand, execMoveAliasCommand, execRemoveAliasCommand, execResolveCommand } from "./AliasCommands";
import { execKickCommand } from "./KickCommand";
import { parse as tokenize } from "shell-quote";
import { execSinceCommand } from "./SinceCommand";
import { readCommand } from "./interface-manager/CommandReader";
import { BaseFunction, CommandTable, defineCommandTable } from "./interface-manager/InterfaceCommand";
import { findMatrixInterfaceAdaptor, MatrixContext } from "./interface-manager/MatrixInterfaceAdaptor";
import { ArgumentStream } from "./interface-manager/ParameterParsing";
import { CommandResult } from "./interface-manager/Validation";
import { CommandException } from "./interface-manager/CommandException";
import { tickCrossRenderer } from "./interface-manager/MatrixHelpRenderer";
export interface MjolnirContext extends MatrixContext {
mjolnir: Mjolnir,
}
export type MjolnirBaseExecutor = (this: MjolnirContext, ...args: any[]) => Promise<CommandResult<any>>;
defineCommandTable("mjolnir");
import "./interface-manager/MatrixPresentations";
import "./HijackRoomCommand";
import "./Ban";
import "./Unban";
import "./StatusCommand";
export const COMMAND_PREFIX = "!mjolnir";
export async function handleCommand(roomId: string, event: { content: { body: string } }, mjolnir: Mjolnir, commandTable: CommandTable<BaseFunction>) {
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);
try {
if (parts.length === 1 || parts[1] === 'joins') {
return await showJoinsStatus(roomId, event, mjolnir, parts.slice(/* ["joins"] */ 2));
} else if (parts[1] === 'rules' && parts.length === 4 && parts[2] === 'matching') {
return await execRulesMatchingCommand(roomId, event, mjolnir, parts[3])
} else if (parts[1] === 'rules') {
return await execDumpRulesCommand(roomId, event, mjolnir);
} else if (parts[1] === 'sync') {
return await execSyncCommand(roomId, event, mjolnir);
} else if (parts[1] === 'verify') {
return await execPermissionCheckCommand(roomId, event, mjolnir);
} else if (parts.length >= 5 && parts[1] === 'list' && parts[2] === 'create') {
return await execCreateListCommand(roomId, event, mjolnir, parts);
} else if (parts[1] === 'watch' && parts.length > 1) {
return await execWatchCommand(roomId, event, mjolnir, parts);
} else if (parts[1] === 'unwatch' && parts.length > 1) {
return await execUnwatchCommand(roomId, event, mjolnir, parts);
} else if (parts[1] === 'redact' && parts.length > 1) {
return await execRedactCommand(roomId, event, mjolnir, parts);
} else if (parts[1] === 'import' && parts.length > 2) {
return await execImportCommand(roomId, event, mjolnir, parts);
} else if (parts[1] === 'default' && parts.length > 2) {
return await execSetDefaultListCommand(roomId, event, mjolnir, parts);
} else if (parts[1] === 'deactivate' && parts.length > 2) {
return await execDeactivateCommand(roomId, event, mjolnir, parts);
} else if (parts[1] === 'protections') {
return await execListProtections(roomId, event, mjolnir, parts);
} else if (parts[1] === 'enable' && parts.length > 1) {
return await execEnableProtection(roomId, event, mjolnir, parts);
} else if (parts[1] === 'disable' && parts.length > 1) {
return await execDisableProtection(roomId, event, mjolnir, parts);
} else if (parts[1] === '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) {
return await execConfigAddProtection(roomId, event, mjolnir, parts.slice(3))
} else if (parts[1] === 'config' && parts[2] === 'remove' && parts.length > 3) {
return await execConfigRemoveProtection(roomId, event, mjolnir, parts.slice(3))
} else if (parts[1] === 'config' && parts[2] === 'get') {
return await execConfigGetProtection(roomId, event, mjolnir, parts.slice(3))
} else if (parts[1] === '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') {
return await execRemoveProtectedRoom(roomId, event, mjolnir, parts);
} else if (parts[1] === 'rooms' && parts.length === 2) {
return await execListProtectedRooms(roomId, event, mjolnir);
} else if (parts[1] === 'move' && parts.length > 3) {
return await execMoveAliasCommand(roomId, event, mjolnir, parts);
} else if (parts[1] === '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') {
return await execRemoveRoomFromDirectoryCommand(roomId, event, mjolnir, parts);
} else if (parts[1] === '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') {
return await execRemoveAliasCommand(roomId, event, mjolnir, parts);
} else if (parts[1] === 'resolve' && parts.length > 2) {
return await execResolveCommand(roomId, event, mjolnir, parts);
} else if (parts[1] === 'powerlevel' && parts.length > 3) {
return await execSetPowerLevelCommand(roomId, event, mjolnir, parts);
} else if (parts[1] === '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) {
return await execKickCommand(roomId, event, mjolnir, parts);
} else {
const readItems = readCommand(cmd).slice(1); // remove "!mjolnir"
const stream = new ArgumentStream(readItems);
const command = commandTable.findAMatchingCommand(stream);
if (command) {
const adaptor = findMatrixInterfaceAdaptor(command);
const mjolnirContext: MjolnirContext = {
mjolnir, roomId, event, client: mjolnir.client, emitter: mjolnir.matrixEmitter,
};
try {
return await adaptor.invoke(mjolnirContext, mjolnirContext, ...stream.rest());
} catch (e) {
const commandError = new CommandException(e, 'Unknown Unexpected Error');
LogService.error("CommandHandler", commandError.uuid, commandError.message, e);
await tickCrossRenderer.call(mjolnirContext, mjolnir.client, roomId, event, CommandResult.Err(commandError));
}
}
// Help menu
const menu = "" +
"!mjolnir - Print status information\n" +
"!mjolnir status - Print status information\n" +
"!mjolnir status protection <protection> [subcommand] - Print status information for a protection\n" +
"!mjolnir ban <list shortcode> <user|room|server> <glob> [reason] - Adds an entity to the ban list\n" +
"!mjolnir unban <list shortcode> <user|room|server> <glob> [apply] - Removes an entity from the ban list. If apply is 'true', the users matching the glob will actually be unbanned\n" +
"!mjolnir redact <user ID> [room alias/ID] [limit] - Redacts messages by the sender in the target room (or all rooms), up to a maximum number of events in the backlog (default 1000)\n" +
"!mjolnir redact <event permalink> - Redacts a message by permalink\n" +
"!mjolnir kick <glob> [room alias/ID] [reason] - Kicks a user or all of those matching a glob in a particular room or all protected rooms\n" +
"!mjolnir rules - Lists the rules currently in use by Mjolnir\n" +
"!mjolnir rules matching <user|room|server> - Lists the rules in use that will match this entity e.g. `!rules matching @foo:example.com` will show all the user and server rules, including globs, that match this user\n" +
"!mjolnir sync - Force updates of all lists and re-apply rules\n" +
"!mjolnir verify - Ensures Mjolnir can moderate all your rooms\n" +
"!mjolnir list create <shortcode> <alias localpart> - Creates a new ban list with the given shortcode and alias\n" +
"!mjolnir watch <room alias/ID> - Watches a ban list\n" +
"!mjolnir unwatch <room alias/ID> - Unwatches a ban list\n" +
"!mjolnir import <room alias/ID> <list shortcode> - Imports bans and ACLs into the given list\n" +
"!mjolnir default <shortcode> - Sets the default list for commands\n" +
"!mjolnir deactivate <user ID> - Deactivates a user ID\n" +
"!mjolnir protections - List all available protections\n" +
"!mjolnir enable <protection> - Enables a particular protection\n" +
"!mjolnir disable <protection> - Disables a particular protection\n" +
"!mjolnir config set <protection>.<setting> [value] - Change a protection setting\n" +
"!mjolnir config add <protection>.<setting> [value] - Add a value to a list protection setting\n" +
"!mjolnir config remove <protection>.<setting> [value] - Remove a value from a list protection setting\n" +
"!mjolnir config get [protection] - List protection settings\n" +
"!mjolnir rooms - Lists all the protected rooms\n" +
"!mjolnir rooms add <room alias/ID> - Adds a protected room (may cause high server load)\n" +
"!mjolnir rooms remove <room alias/ID> - Removes a protected room\n" +
"!mjolnir move <room alias> <room alias/ID> - Moves a <room alias> to a new <room ID>\n" +
"!mjolnir directory add <room alias/ID> - Publishes a room in the server's room directory\n" +
"!mjolnir directory remove <room alias/ID> - Removes a room from the server's room directory\n" +
"!mjolnir alias add <room alias> <target room alias/ID> - Adds <room alias> to <target room>\n" +
"!mjolnir alias remove <room alias> - Deletes the room alias from whatever room it is attached to\n" +
"!mjolnir resolve <room alias> - Resolves a room alias to a room ID\n" +
"!mjolnir since <date>/<duration> <action> <limit> [rooms...] [reason] - Apply an action ('kick', 'ban', 'mute', 'unmute' or 'show') to all users who joined a room since <date>/<duration> (up to <limit> users)\n" +
"!mjolnir shutdown room <room alias/ID> [message] - Uses the bot's account to shut down a room, preventing access to the room on this server\n" +
"!mjolnir powerlevel <user ID> <power level> [room alias/ID] - Sets the power level of the user in the specified room (or all protected rooms)\n" +
"!mjolnir help - This menu\n";
const html = `<b>Mjolnir help:</b><br><pre><code>${htmlEscape(menu)}</code></pre>`;
const text = `Mjolnir help:\n${menu}`;
const reply = RichReply.createFor(roomId, event, text, html);
reply["msgtype"] = "m.notice";
return await mjolnir.client.sendMessage(roomId, reply);
}
} catch (e) {
LogService.error("CommandHandler", e);
const text = "There was an error processing your command - see console/log for details";
const reply = RichReply.createFor(roomId, event, text, text);
reply["msgtype"] = "m.notice";
return await mjolnir.client.sendMessage(roomId, reply);
}
}