diff --git a/src/commands/AddRemoveProtectedRoomsCommand.ts b/src/commands/AddRemoveProtectedRoomsCommand.ts deleted file mode 100644 index d8221cba..00000000 --- a/src/commands/AddRemoveProtectedRoomsCommand.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (C) 2022 Gnuxie - * 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 2020-2021 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 { LogLevel, LogService } from "matrix-bot-sdk"; - -// !mjolnir rooms add -export async function execAddProtectedRoom(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { - const protectedRoomId = await mjolnir.client.joinRoom(parts[3]); - await mjolnir.addProtectedRoom(protectedRoomId); - await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅'); -} - -// !mjolnir rooms remove -export async function execRemoveProtectedRoom(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { - const protectedRoomId = await mjolnir.client.resolveRoom(parts[3]); - await mjolnir.removeProtectedRoom(protectedRoomId); - try { - await mjolnir.client.leaveRoom(protectedRoomId); - } catch (e) { - LogService.warn("AddRemoveProtectedRoomsCommand", e); - await mjolnir.managementRoomOutput.logMessage(LogLevel.WARN, "AddRemoveProtectedRoomsCommand", `Failed to leave ${protectedRoomId} - the room is no longer being protected, but the bot could not leave`, protectedRoomId); - } - await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅'); -} diff --git a/src/commands/CommandHandler.ts b/src/commands/CommandHandler.ts index 893fc705..fea8811d 100644 --- a/src/commands/CommandHandler.ts +++ b/src/commands/CommandHandler.ts @@ -38,8 +38,6 @@ import { execDisableProtection, execEnableProtection, execListProtections, execConfigGetProtection, execConfigSetProtection, execConfigAddProtection, execConfigRemoveProtection } from "./ProtectionsCommands"; -import { execListProtectedRooms } from "./ListProtectedRoomsCommand"; -import { execAddProtectedRoom, execRemoveProtectedRoom } from "./AddRemoveProtectedRoomsCommand"; import { execSetPowerLevelCommand } from "./SetPowerLevelCommand"; import { execResolveCommand } from "./ResolveAlias"; import { execKickCommand } from "./KickCommand"; @@ -64,13 +62,13 @@ defineCommandTable("synapse admin"); import "./HijackRoomCommand"; import "./ShutdownRoomCommand"; import "./DeactivateCommand"; -import "./AddRemoveProtectedRoomsCommand"; import "./AliasCommands"; defineCommandTable("mjolnir").importTable(findCommandTable("synapse admin")); import "./Ban"; import "./Unban"; import "./StatusCommand"; +import "./Rooms"; import "./Rules"; import "./WatchUnwatchCommand"; import "./Help"; @@ -114,12 +112,6 @@ export async function handleCommand(roomId: string, event: { content: { body: st 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] === 'resolve' && parts.length > 2) { return await execResolveCommand(roomId, event, mjolnir, parts); } else if (parts[1] === 'powerlevel' && parts.length > 3) { @@ -141,7 +133,6 @@ export async function handleCommand(roomId: string, event: { content: { body: st 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)); } } diff --git a/src/commands/ListProtectedRoomsCommand.ts b/src/commands/ListProtectedRoomsCommand.ts deleted file mode 100644 index 38701b4c..00000000 --- a/src/commands/ListProtectedRoomsCommand.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (C) 2022 Gnuxie - * 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 2020 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 { RichReply } from "matrix-bot-sdk"; -import { Permalinks } from "./interface-manager/Permalinks"; - -// !mjolnir rooms -export async function execListProtectedRooms(roomId: string, event: any, mjolnir: Mjolnir) { - const rooms = mjolnir.protectedRoomsTracker.getProtectedRooms(); - let html = `Protected rooms (${rooms.length}):
    `; - let text = `Protected rooms (${rooms.length}):\n`; - - let hasRooms = false; - for (const protectedRoomId of rooms) { - hasRooms = true; - - const roomUrl = Permalinks.forRoom(protectedRoomId); - html += `
  • ${protectedRoomId}
  • `; - text += `* ${roomUrl}\n`; - } - - html += "
"; - - if (!hasRooms) { - html = "No protected rooms"; - text = "No protected rooms"; - } - - const reply = RichReply.createFor(roomId, event, text, html); - reply["msgtype"] = "m.notice"; - return mjolnir.client.sendMessage(roomId, reply); -} diff --git a/src/commands/Rooms.tsx b/src/commands/Rooms.tsx new file mode 100644 index 00000000..88c9aed2 --- /dev/null +++ b/src/commands/Rooms.tsx @@ -0,0 +1,134 @@ +/** + * Copyright (C) 2022 Gnuxie + * 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 2020-2021 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 { defineInterfaceCommand, findTableCommand } from "./interface-manager/InterfaceCommand"; +import { findPresentationType, parameters } from "./interface-manager/ParameterParsing"; +import { MjolnirContext } from "./CommandHandler"; +import { MatrixRoomID, MatrixRoomReference } from "./interface-manager/MatrixRoomReference"; +import { CommandResult } from "./interface-manager/Validation"; +import { CommandException } from "./interface-manager/CommandException"; +import { defineMatrixInterfaceAdaptor } from "./interface-manager/MatrixInterfaceAdaptor"; +import { tickCrossRenderer } from "./interface-manager/MatrixHelpRenderer"; +import { DocumentNode } from "./interface-manager/DeadDocument"; +import { JSXFactory } from "./interface-manager/JSXFactory"; +import { renderMatrixAndSend } from "./interface-manager/DeadDocumentMatrix"; +import { Permalinks } from "./interface-manager/Permalinks"; + +defineInterfaceCommand({ + table: "mjolnir", + designator: ["rooms"], + summary: "List all of the protected rooms.", + parameters: parameters([]), + command: async function (this: MjolnirContext, _keywrods): Promise> { + return CommandResult.Ok(this.mjolnir.protectedRoomsTracker.getProtectedRooms()); + } +}) + +function renderProtectedRooms(rooms: string[]): DocumentNode { + return +
+ Protected Rooms ({rooms.length}): +
    + {rooms.map(r =>
  • {r}
  • )} +
+
+
+} + +defineMatrixInterfaceAdaptor({ + interfaceCommand: findTableCommand("mjolnir", "rooms"), + renderer: async function (client, commandRoomId, event, result) { + tickCrossRenderer.call(this, ...arguments); + if (result.isErr()) { + return; // tickCrossRenderer will handle it. + } + await renderMatrixAndSend( + renderProtectedRooms(result.ok), + commandRoomId, event, client + ); + } +}) + +defineInterfaceCommand({ + table: "mjolnir", + designator: ["rooms", "add"], + summary: "Protect the room using the watched policy lists, banning users and synchronizing server ACL.", + parameters: parameters([ + { + name: 'room', + acceptor: findPresentationType("MatrixRoomReference"), + description: 'The room to protect.' + } + ]), + command: async function (this: MjolnirContext, _keywords, roomRef: MatrixRoomReference): Promise> { + const roomIDOrError = await (async () => { + try { + return CommandResult.Ok(await roomRef.joinClient(this.mjolnir.client)); + } catch (e) { + return CommandException.Result( + `The homeserver that Draupnir is hosted on cannot join this room using the room reference provided.\ + Try an alias or the "share room" button in your client to obtain a valid reference to the room.`, + { exception: e } + ); + } + })(); + if (roomIDOrError.isErr()) { + return CommandResult.Err(roomIDOrError.err); + } + await this.mjolnir.addProtectedRoom(roomIDOrError.ok.toRoomIdOrAlias()); + return CommandResult.Ok(undefined); + }, +}) + +defineInterfaceCommand({ + table: "mjolnir", + designator: ["rooms", "remove"], + summary: "Stop protecting the room and leave.", + parameters: parameters([ + { + name: 'room', + acceptor: findPresentationType("MatrixRoomReference"), + description: 'The room to stop protecting.' + } + ]), + command: async function (this: MjolnirContext, _keywords, roomRef: MatrixRoomReference): Promise> { + const roomID = await roomRef.resolve(this.mjolnir.client); + try { + await this.mjolnir.client.leaveRoom(roomID.toRoomIdOrAlias()); + } catch (exception) { + return CommandException.Result(`Failed to leave ${roomRef.toPermalink()} - the room is no longer being protected, but the bot could not leave.`, { exception }); + } + return CommandResult.Ok(undefined); + }, +}) + +for (const designator of [["rooms", "add"], ["rooms", "remove"]]) { + defineMatrixInterfaceAdaptor({ + interfaceCommand: findTableCommand("mjolnir", ...designator), + renderer: tickCrossRenderer, + }) +} diff --git a/src/commands/interface-manager/CommandReader.ts b/src/commands/interface-manager/CommandReader.ts index dbc760f6..7cd12b0a 100644 --- a/src/commands/interface-manager/CommandReader.ts +++ b/src/commands/interface-manager/CommandReader.ts @@ -5,6 +5,7 @@ import { UserID } from "matrix-bot-sdk"; import { MatrixRoomReference } from "./MatrixRoomReference"; +import { Permalinks } from "./Permalinks"; export interface ISuperCoolStream { readonly source: T @@ -91,15 +92,15 @@ const WHITESPACE = [' ', '\r', '\f', '\v', '\n', '\t']; * @returns ReadItems that have been read from this command. */ export function readCommand(string: string): ReadItem[] { - return readCommandFromStream(new StringStream(string)); + return readCommandFromStream(new StringStream(string)) } function readCommandFromStream(stream: StringStream): ReadItem[] { - const words: any[] = []; + const words: ReadItem[] = []; while (stream.peekChar() !== undefined && (eatWhitespace(stream), true) && stream.peekChar() !== undefined) { words.push(readItem(stream)); } - return words; + return words.map(applyPostReadTransformersToReadItem); } function eatWhitespace(stream: StringStream): void { @@ -152,6 +153,37 @@ function defineReadItem(dispatchCharacter: string, macro: ReadMacro) { WORD_DISPATCH_CHARACTERS.set(dispatchCharacter, macro); } +type PostReadStringReplaceTransformer = (item: string) => ReadItem|string; +type TransformerEntry = { regex: RegExp, transformer: PostReadStringReplaceTransformer }; +const POST_READ_TRANSFORMERS = new Map(); + + +/** + * Define a function that will be applied to ReadItem's that are strings that + * also match the regex. + * If the regex matches, the transformer function will be called with the read item + * and given the oppertunity to return a new version of the item. + * + * This is mainly used to transform URLs into a MatrixRoomReference. + */ +function definePostReadReplace(regex: RegExp, transformer: PostReadStringReplaceTransformer) { + if (POST_READ_TRANSFORMERS.has(regex.source)) { + throw new TypeError(`A transformer has already been defined for the regexp ${regex.source}`); + } + POST_READ_TRANSFORMERS.set(regex.source, { regex, transformer }) +} + +function applyPostReadTransformersToReadItem(item: ReadItem): ReadItem { + if (typeof item === 'string') { + for (const [_key, { regex, transformer }] of POST_READ_TRANSFORMERS) { + if (regex.test(item)) { + return transformer(item); + } + } + } + return item; +} + /** * Helper that consumes from `stream` and appends to `output` until a character is peeked matching `regex`. * @param regex A regex for a character to stop at. @@ -233,3 +265,13 @@ function readKeyword(stream: StringStream): Keyword { defineReadItem('-', readKeyword); defineReadItem(':', readKeyword); + +definePostReadReplace(/^https:\/\/matrix\.to/, input => { + const url = Permalinks.parseUrl(input); + if (url.eventId !== undefined) { + // don't know what to turn event references into yet. + return input; + } else { + return MatrixRoomReference.fromPermalink(input); + } +}) diff --git a/src/commands/interface-manager/MatrixHelpRenderer.tsx b/src/commands/interface-manager/MatrixHelpRenderer.tsx index a4fb10c8..0275b669 100644 --- a/src/commands/interface-manager/MatrixHelpRenderer.tsx +++ b/src/commands/interface-manager/MatrixHelpRenderer.tsx @@ -11,6 +11,7 @@ import { JSXFactory } from "./JSXFactory"; import { DocumentNode } from "./DeadDocument"; import { renderMatrixAndSend } from "./DeadDocumentMatrix"; import { CommandException } from "./CommandException"; +import { LogService } from "matrix-bot-sdk"; function requiredArgument(argumentName: string): string { return `<${argumentName}>`; @@ -73,12 +74,15 @@ export async function renderHelp(client: MatrixSendClient, commandRoomId: string export async function tickCrossRenderer(this: MatrixInterfaceAdaptor, client: MatrixSendClient, commandRoomId: string, event: any, result: CommandResult): Promise { const react = async (emote: string) => { - await client.unstableApis.addReactionToEvent(commandRoomId, event['event_id'], emote); + try { + await client.unstableApis.addReactionToEvent(commandRoomId, event['event_id'], emote); + } catch (e) { + LogService.error("tickCrossRenderer", "Couldn't react to the event", event['event_id'], e); + } } if (result.isOk()) { await react('✅') } else { - await react('❌'); if (result.err instanceof ArgumentParseError) { await renderMatrixAndSend( renderArgumentParseError(this.interfaceCommand, result.err), @@ -86,6 +90,8 @@ export async function tickCrossRenderer(this: MatrixInterfaceAdaptor, error: export function renderMentionPill(mxid: string, displayName: string): DocumentNode { const url = `https://matrix.to/#/${mxid}`; return {displayName} -} \ No newline at end of file +}