From 41cc36e4c664757c0803d82e73c862702d64602f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 27 Sep 2019 15:15:10 -0600 Subject: [PATCH] Refactor bot into its own class --- src/Mjolnir.ts | 90 +++++++++++++++++++++++++++++ src/commands/CommandHandler.ts | 32 +++++++++++ src/commands/StatusCommand.ts | 46 +++++++++++++++ src/index.ts | 101 +++------------------------------ 4 files changed, 177 insertions(+), 92 deletions(-) create mode 100644 src/Mjolnir.ts create mode 100644 src/commands/CommandHandler.ts create mode 100644 src/commands/StatusCommand.ts diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts new file mode 100644 index 0000000..9121907 --- /dev/null +++ b/src/Mjolnir.ts @@ -0,0 +1,90 @@ +/* +Copyright 2019 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. +*/ + +import { MatrixClient } from "matrix-bot-sdk"; +import BanList, { ALL_RULE_TYPES } from "./models/BanList"; +import { applyServerAcls } from "./actions/ApplyAcl"; +import { RoomUpdateError } from "./models/RoomUpdateError"; +import { COMMAND_PREFIX, handleCommand } from "./commands/CommandHandler"; + +export class Mjolnir { + constructor( + public readonly client: MatrixClient, + private managementRoomId: string, + public readonly protectedRooms: { [roomId: string]: string }, + public readonly banLists: BanList[], + ) { + client.on("room.event", this.handleEvent.bind(this)); + + client.on("room.message", async (roomId, event) => { + if (roomId !== managementRoomId) return; + if (!event['content']) return; + + const content = event['content']; + if (content['msgtype'] === "m.text" && content['body'] && content['body'].startsWith(COMMAND_PREFIX)) { + await client.sendReadReceipt(roomId, event['event_id']); + return handleCommand(roomId, event, this); + } + }); + } + + public start() { + return this.client.start(); + } + + private async handleEvent(roomId: string, event: any) { + if (!event['state_key']) return; // we also don't do anything with state events that have no state key + + if (ALL_RULE_TYPES.includes(event['type'])) { + for (const list of this.banLists) { + if (list.roomId !== roomId) continue; + await list.updateList(); + } + + const errors = await applyServerAcls(this.banLists, Object.keys(this.protectedRooms), this.client); + return this.printActionResult(errors); + } else if (event['type'] === "m.room.member") { + // TODO: Check membership against ban banLists + } + } + + private async printActionResult(errors: RoomUpdateError[]) { + let html = ""; + let text = ""; + + if (errors.length > 0) { + html += `${errors.length} errors updating protected rooms!
"; + } else { + html += `Updated all protected rooms with new rules successfully.`; + text += "Updated all protected rooms with new rules successfully"; + } + + const message = { + msgtype: "m.notice", + body: text, + format: "org.matrix.custom.html", + formatted_body: html, + }; + return this.client.sendMessage(this.managementRoomId, message); + } +} diff --git a/src/commands/CommandHandler.ts b/src/commands/CommandHandler.ts new file mode 100644 index 0000000..f8fe788 --- /dev/null +++ b/src/commands/CommandHandler.ts @@ -0,0 +1,32 @@ +/* +Copyright 2019 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. +*/ + +import { MatrixClient } from "matrix-bot-sdk"; +import { Mjolnir } from "../Mjolnir"; +import { execStatusCommand } from "./StatusCommand"; + +export const COMMAND_PREFIX = "!mjolnir"; + +export function handleCommand(roomId: string, event: any, mjolnir: Mjolnir) { + const cmd = event['content']['body']; + const parts = cmd.trim().split(' '); + + if (parts.length === 1) { + return execStatusCommand(roomId, event, mjolnir); + } else { + // TODO: Help menu + } +} diff --git a/src/commands/StatusCommand.ts b/src/commands/StatusCommand.ts new file mode 100644 index 0000000..4163bd2 --- /dev/null +++ b/src/commands/StatusCommand.ts @@ -0,0 +1,46 @@ +/* +Copyright 2019 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. +*/ + +import { Mjolnir } from "../Mjolnir"; + +export async function execStatusCommand(roomId: string, event: any, mjolnir: Mjolnir) { + let html = ""; + let text = ""; + + // Append header information first + html += "Running:
"; + text += "Running: ✅\n"; + html += `Protected rooms: ${Object.keys(mjolnir.protectedRooms).length}
`; + text += `Protected rooms: ${mjolnir.protectedRooms.length}\n`; + + // Append list information + html += "Subscribed ban lists:
"; + + const message = { + msgtype: "m.notice", + body: text, + format: "org.matrix.custom.html", + formatted_body: html, + }; + return mjolnir.client.sendMessage(roomId, message); +} diff --git a/src/index.ts b/src/index.ts index b09f442..9f5101c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,48 +27,21 @@ import config from "./config"; import BanList, { ALL_RULE_TYPES } from "./models/BanList"; import { applyServerAcls } from "./actions/ApplyAcl"; import { RoomUpdateError } from "./models/RoomUpdateError"; +import { Mjolnir } from "./Mjolnir"; LogService.setLogger(new RichConsoleLogger()); const storage = new SimpleFsStorageProvider(path.join(config.dataPath, "bot.json")); const client = new MatrixClient(config.homeserverUrl, config.accessToken, storage); -const lists: BanList[] = []; -let managementRoomId = ""; -const protectedRooms: { [roomId: string]: string } = {}; if (config.autojoin) { AutojoinRoomsMixin.setupOnClient(client); } -client.on("room.event", async (roomId, event) => { - if (!event['state_key']) return; // we also don't do anything with state events that have no state key - - if (ALL_RULE_TYPES.includes(event['type'])) { - for (const list of lists) { - if (list.roomId !== roomId) continue; - await list.updateList(); - } - - const errors = await applyServerAcls(lists, Object.keys(protectedRooms), client); - return printActionResult(errors); - } else if (event['type'] === "m.room.member") { - // TODO: Check membership against ban lists - } -}); - -client.on("room.message", async (roomId, event) => { - if (roomId !== managementRoomId) return; - if (!event['content']) return; - - const content = event['content']; - if (content['msgtype'] === "m.text" && content['body'] === "!mjolnir") { - await client.sendReadReceipt(roomId, event['event_id']); - return printStatus(roomId); - } -}); - - (async function () { + const banLists: BanList[] = []; + const protectedRooms:{[roomId: string]: string} = {}; + // Ensure we're in all the rooms we expect to be in const joinedRooms = await client.getJoinedRooms(); for (const roomRef of config.banLists) { @@ -82,7 +55,7 @@ client.on("room.message", async (roomId, event) => { const list = new BanList(roomId, roomRef, client); await list.updateList(); - lists.push(list); + banLists.push(list); } // Ensure we're also joined to the rooms we're protecting @@ -99,70 +72,14 @@ client.on("room.message", async (roomId, event) => { } // Ensure we're also in the management room - managementRoomId = await client.joinRoom(config.managementRoom); + const managementRoomId = await client.joinRoom(config.managementRoom); await client.sendNotice(managementRoomId, "Mjolnir is starting up. Use !mjolnir to query status."); + const bot = new Mjolnir(client, managementRoomId, protectedRooms, banLists); + await bot.start(); + // TODO: Check permissions for mjolnir in protected rooms // TODO: Complain about permission changes in protected rooms (including after power levels change) - await client.start(); LogService.info("index", "Bot started!") })(); - -async function printStatus(roomId: string) { - const rooms = await client.getJoinedRooms(); - - let html = ""; - let text = ""; - - // Append header information first - html += "Running:
"; - text += "Running: ✅\n"; - html += `Protected rooms: ${Object.keys(protectedRooms).length}
`; - text += `Protected rooms: ${rooms.length}\n`; - - // Append list information - html += "Subscribed lists:
"; - - const message = { - msgtype: "m.notice", - body: text, - format: "org.matrix.custom.html", - formatted_body: html, - }; - return client.sendMessage(roomId, message); -} - -async function printActionResult(errors: RoomUpdateError[]) { - let html = ""; - let text = ""; - - if (errors.length > 0) { - html += `${errors.length} errors updating protected rooms!
"; - } else { - html += `Updated all protected rooms with new rules successfully.`; - text += "Updated all protected rooms with new rules successfully"; - } - - const message = { - msgtype: "m.notice", - body: text, - format: "org.matrix.custom.html", - formatted_body: html, - }; - return client.sendMessage(managementRoomId, message); -}