diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts index 92d4b0fc..1dffd072 100644 --- a/src/Mjolnir.ts +++ b/src/Mjolnir.ts @@ -41,7 +41,7 @@ import { WebAPIs } from "./webapis/WebAPIs"; import RuleServer from "./models/RuleServer"; import { ThrottlingQueue } from "./queues/ThrottlingQueue"; import { getDefaultConfig, IConfig } from "./config"; -import { PolicyListManager } from "./models/PolicyList"; +import { PolicyListManager } from "./models/PolicyListManager"; import { ProtectedRoomsSet } from "./ProtectedRoomsSet"; import ManagementRoomOutput from "./ManagementRoomOutput"; import { ProtectionManager } from "./protections/ProtectionManager"; diff --git a/src/models/PolicyList.ts b/src/models/PolicyList.ts index 2472b807..e298a378 100644 --- a/src/models/PolicyList.ts +++ b/src/models/PolicyList.ts @@ -25,15 +25,12 @@ limitations under the License. * are NOT distributed, contributed, committed, or licensed under the Apache License. */ -import { LogLevel, LogService, Permalinks, RoomCreateOptions, UserID } from "matrix-bot-sdk"; +import { LogService, RoomCreateOptions, UserID } from "matrix-bot-sdk"; import { EventEmitter } from "events"; import { ALL_RULE_TYPES, EntityType, ListRule, Recommendation, ROOM_RULE_TYPES, RULE_ROOM, RULE_SERVER, RULE_USER, SERVER_RULE_TYPES, USER_RULE_TYPES } from "./ListRule"; import { MatrixSendClient } from "../MatrixEmitter"; import AwaitLock from "await-lock"; import { monotonicFactory } from "ulidx"; -import { Mjolnir } from "../Mjolnir"; -import { MatrixDataManager, RawSchemedData, SCHEMA_VERSION_KEY } from "./MatrixDataManager"; -import { MatrixRoomReference } from "../commands/interface-manager/MatrixRoomReference"; /** * Account data event type used to store the permalinks to each of the policylists. @@ -50,7 +47,7 @@ export const WATCHED_LISTS_EVENT_TYPE = "org.matrix.mjolnir.watched_lists"; /** * A prefix used to record that we have already warned at least once that a PolicyList room is unprotected. */ -const WARN_UNPROTECTED_ROOM_EVENT_PREFIX = "org.matrix.mjolnir.unprotected_room_warning.for."; +export const WARN_UNPROTECTED_ROOM_EVENT_PREFIX = "org.matrix.mjolnir.unprotected_room_warning.for."; export const SHORTCODE_EVENT_TYPE = "org.matrix.mjolnir.shortcode"; export enum ChangeType { @@ -84,7 +81,7 @@ export interface ListRuleChange { readonly previousState?: any, } -declare interface PolicyList { +export declare interface PolicyList { // PolicyList.update is emitted when the PolicyList has pulled new rules from Matrix and informs listeners of any changes. on(event: 'PolicyList.update', listener: (list: PolicyList, changes: ListRuleChange[], revision: Revision) => void): this emit(event: 'PolicyList.update', list: PolicyList, changes: ListRuleChange[], revision: Revision): boolean @@ -103,7 +100,7 @@ declare interface PolicyList { * you cannot rely on the timeline portion of `/sync` to provide a consistent view of the room state as you * receive events in stream order. */ -class PolicyList extends EventEmitter { +export class PolicyList extends EventEmitter { private shortcode: string | null = null; // A map of state events indexed first by state type and then state keys. private state: Map> = new Map(); @@ -617,162 +614,3 @@ export class Revision { return this.ulid > revision.ulid; } } - -type WatchedListsEvent = RawSchemedData & { references?: string[] }; - -/** - * Manages the policy lists that a Mjolnir watches - */ -export class PolicyListManager extends MatrixDataManager { - private policyLists: PolicyList[]; - - protected schema = []; - protected isAllowedToInferNoVersionAsZero = true; - - constructor(private readonly mjolnir: Mjolnir) { - super(); - } - - public get lists(): PolicyList[] { - return [...this.policyLists]; - } - - /** - * Helper for constructing `PolicyList`s and making sure they have the right listeners set up. - * @param roomId The room id for the `PolicyList`. - * @param roomRef A reference (matrix.to URL) for the `PolicyList`. - */ - private async addPolicyList(roomId: string, roomRef: string): Promise { - const list = new PolicyList(roomId, roomRef, this.mjolnir.client); - this.mjolnir.ruleServer?.watch(list); - await list.updateList(); - this.policyLists.push(list); - this.mjolnir.protectedRoomsTracker.watchList(list); - - return list; - } - - public async watchList(roomRef: string): Promise { - const joinedRooms = await this.mjolnir.client.getJoinedRooms(); - const permalink = Permalinks.parseUrl(roomRef); - if (!permalink.roomIdOrAlias) return null; - - const roomId = await this.mjolnir.client.resolveRoom(permalink.roomIdOrAlias); - if (!joinedRooms.includes(roomId)) { - await this.mjolnir.client.joinRoom(roomId, permalink.viaServers); - } - - if (this.policyLists.find(b => b.roomId === roomId)) { - // This room was already in our list of policy rooms, nothing else to do. - // Note that we bailout *after* the call to `joinRoom`, in case a user - // calls `watchList` in an attempt to repair something that was broken, - // e.g. a Mjölnir who could not join the room because of alias resolution - // or server being down, etc. - return null; - } - - const list = await this.addPolicyList(roomId, roomRef); - - await this.storeMatixData(); - await this.warnAboutUnprotectedPolicyListRoom(roomId); - - return list; - } - - public async unwatchList(roomRef: string): Promise { - const permalink = Permalinks.parseUrl(roomRef); - if (!permalink.roomIdOrAlias) return null; - - const roomId = await this.mjolnir.client.resolveRoom(permalink.roomIdOrAlias); - const list = this.policyLists.find(b => b.roomId === roomId) || null; - if (list) { - this.policyLists.splice(this.policyLists.indexOf(list), 1); - this.mjolnir.ruleServer?.unwatch(list); - this.mjolnir.protectedRoomsTracker.unwatchList(list); - } - - await this.storeMatixData(); - return list; - } - - protected async createFirstData(): Promise { - return { [SCHEMA_VERSION_KEY]: 0 }; - } - - protected async requestMatrixData(): Promise { - try { - return await this.mjolnir.client.getAccountData(WATCHED_LISTS_EVENT_TYPE); - } catch (e) { - if (e.statusCode === 404) { - LogService.warn('Mjolnir', "Couldn't find account data for Mjolnir's watched lists, assuming first start.", e); - return this.createFirstData(); - } else { - throw e; - } - } - } - - /** - * Load the watched policy lists from account data, only used when Mjolnir is initialized. - */ - public async start() { - this.policyLists = []; - const watchedListsEvent = await super.loadData(); - - await Promise.all( - (watchedListsEvent?.references || []).map(async (roomRef: string) => { - const roomReference = await MatrixRoomReference.fromPermalink(roomRef).resolve(this.mjolnir.client) - .catch(ex => { - LogService.error("PolicyListManager", "Failed to load watched lists for this mjolnir", ex); - return Promise.reject(ex); - } - ); - await roomReference.joinClient(this.mjolnir.client); - await this.warnAboutUnprotectedPolicyListRoom(roomReference.toRoomIdOrAlias()); - // TODO, FIXME: fix this so that it stores room references and not this utter junk. - await this.addPolicyList(roomReference.toRoomIdOrAlias(), roomReference.toPermalink()); - }) - ); - } - - /** - * Store to account the list of policy rooms. - */ - protected async storeMatixData() { - let list = this.policyLists.map(b => b.roomRef); - await this.mjolnir.client.setAccountData(WATCHED_LISTS_EVENT_TYPE, { - references: list, - }); - } - - /** - * Check whether a policy list room is protected. If not, display - * a user-readable warning. - * - * We store as account data the list of room ids for which we have - * already displayed the warning, to avoid bothering users at every - * single startup. - * - * @param roomId The id of the room to check/warn. - */ - private async warnAboutUnprotectedPolicyListRoom(roomId: string) { - if (!this.mjolnir.config.protectAllJoinedRooms) { - return; // doesn't matter - } - if (this.mjolnir.explicitlyProtectedRooms.includes(roomId)) { - return; // explicitly protected - } - - try { - const accountData: { warned: boolean } | null = await this.mjolnir.client.getAccountData(WARN_UNPROTECTED_ROOM_EVENT_PREFIX + roomId); - if (accountData && accountData.warned) { - return; // already warned - } - } catch (e) { - // Expect that we haven't warned yet. - } - - await this.mjolnir.managementRoomOutput.logMessage(LogLevel.WARN, "Mjolnir", `Not protecting ${roomId} - it is a ban list that this bot did not create. Add the room as protected if it is supposed to be protected. This warning will not appear again.`, roomId); - await this.mjolnir.client.setAccountData(WARN_UNPROTECTED_ROOM_EVENT_PREFIX + roomId, { warned: true }); - } -} diff --git a/src/models/PolicyListManager.ts b/src/models/PolicyListManager.ts new file mode 100644 index 00000000..325bb68c --- /dev/null +++ b/src/models/PolicyListManager.ts @@ -0,0 +1,193 @@ +/** + * Copyright (C) 2022-2023 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 2019-2023 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 { LogLevel, LogService, Permalinks } from "matrix-bot-sdk"; +import { Mjolnir } from "../Mjolnir"; +import { MatrixDataManager, RawSchemedData, SCHEMA_VERSION_KEY } from "./MatrixDataManager"; +import { MatrixRoomReference } from "../commands/interface-manager/MatrixRoomReference"; +import { PolicyList, WATCHED_LISTS_EVENT_TYPE, WARN_UNPROTECTED_ROOM_EVENT_PREFIX } from "./PolicyList"; + +type WatchedListsEvent = RawSchemedData & { references?: string[]; }; +/** + * Manages the policy lists that a Mjolnir watches + */ + +export class PolicyListManager extends MatrixDataManager { + private policyLists: PolicyList[]; + + protected schema = []; + protected isAllowedToInferNoVersionAsZero = true; + + constructor(private readonly mjolnir: Mjolnir) { + super(); + } + + public get lists(): PolicyList[] { + return [...this.policyLists]; + } + + /** + * Helper for constructing `PolicyList`s and making sure they have the right listeners set up. + * @param roomId The room id for the `PolicyList`. + * @param roomRef A reference (matrix.to URL) for the `PolicyList`. + */ + private async addPolicyList(roomId: string, roomRef: string): Promise { + const list = new PolicyList(roomId, roomRef, this.mjolnir.client); + this.mjolnir.ruleServer?.watch(list); + await list.updateList(); + this.policyLists.push(list); + this.mjolnir.protectedRoomsTracker.watchList(list); + + return list; + } + + public async watchList(roomRef: string): Promise { + const joinedRooms = await this.mjolnir.client.getJoinedRooms(); + const permalink = Permalinks.parseUrl(roomRef); + if (!permalink.roomIdOrAlias) + return null; + + const roomId = await this.mjolnir.client.resolveRoom(permalink.roomIdOrAlias); + if (!joinedRooms.includes(roomId)) { + await this.mjolnir.client.joinRoom(roomId, permalink.viaServers); + } + + if (this.policyLists.find(b => b.roomId === roomId)) { + // This room was already in our list of policy rooms, nothing else to do. + // Note that we bailout *after* the call to `joinRoom`, in case a user + // calls `watchList` in an attempt to repair something that was broken, + // e.g. a Mjölnir who could not join the room because of alias resolution + // or server being down, etc. + return null; + } + + const list = await this.addPolicyList(roomId, roomRef); + + await this.storeMatixData(); + await this.warnAboutUnprotectedPolicyListRoom(roomId); + + return list; + } + + public async unwatchList(roomRef: string): Promise { + const permalink = Permalinks.parseUrl(roomRef); + if (!permalink.roomIdOrAlias) + return null; + + const roomId = await this.mjolnir.client.resolveRoom(permalink.roomIdOrAlias); + const list = this.policyLists.find(b => b.roomId === roomId) || null; + if (list) { + this.policyLists.splice(this.policyLists.indexOf(list), 1); + this.mjolnir.ruleServer?.unwatch(list); + this.mjolnir.protectedRoomsTracker.unwatchList(list); + } + + await this.storeMatixData(); + return list; + } + + protected async createFirstData(): Promise { + return { [SCHEMA_VERSION_KEY]: 0 }; + } + + protected async requestMatrixData(): Promise { + try { + return await this.mjolnir.client.getAccountData(WATCHED_LISTS_EVENT_TYPE); + } catch (e) { + if (e.statusCode === 404) { + LogService.warn('PolicyListManager', "Couldn't find account data for Mjolnir's watched lists, assuming first start.", e); + return this.createFirstData(); + } else { + throw e; + } + } + } + + /** + * Load the watched policy lists from account data, only used when Mjolnir is initialized. + */ + public async start() { + this.policyLists = []; + const watchedListsEvent = await super.loadData(); + + await Promise.all( + (watchedListsEvent?.references || []).map(async (roomRef: string) => { + const roomReference = await MatrixRoomReference.fromPermalink(roomRef).resolve(this.mjolnir.client) + .catch(ex => { + LogService.error("PolicyListManager", "Failed to load watched lists for this mjolnir", ex); + return Promise.reject(ex); + } + ); + await roomReference.joinClient(this.mjolnir.client); + await this.warnAboutUnprotectedPolicyListRoom(roomReference.toRoomIdOrAlias()); + // TODO, FIXME: fix this so that it stores room references and not this utter junk. + await this.addPolicyList(roomReference.toRoomIdOrAlias(), roomReference.toPermalink()); + }) + ); + } + + /** + * Store to account the list of policy rooms. + */ + protected async storeMatixData() { + let list = this.policyLists.map(b => b.roomRef); + await this.mjolnir.client.setAccountData(WATCHED_LISTS_EVENT_TYPE, { + references: list, + }); + } + + /** + * Check whether a policy list room is protected. If not, display + * a user-readable warning. + * + * We store as account data the list of room ids for which we have + * already displayed the warning, to avoid bothering users at every + * single startup. + * + * @param roomId The id of the room to check/warn. + */ + private async warnAboutUnprotectedPolicyListRoom(roomId: string) { + if (!this.mjolnir.config.protectAllJoinedRooms) { + return; // doesn't matter + } + if (this.mjolnir.explicitlyProtectedRooms.includes(roomId)) { + return; // explicitly protected + } + + try { + const accountData: { warned: boolean; } | null = await this.mjolnir.client.getAccountData(WARN_UNPROTECTED_ROOM_EVENT_PREFIX + roomId); + if (accountData && accountData.warned) { + return; // already warned + } + } catch (e) { + // Expect that we haven't warned yet. + } + + await this.mjolnir.managementRoomOutput.logMessage(LogLevel.WARN, "Mjolnir", `Not protecting ${roomId} - it is a ban list that this bot did not create. Add the room as protected if it is supposed to be protected. This warning will not appear again.`, roomId); + await this.mjolnir.client.setAccountData(WARN_UNPROTECTED_ROOM_EVENT_PREFIX + roomId, { warned: true }); + } +}