Factor out protected rooms config management from Mjolnir.

The combination of `resyncJoinedRooms`, `unprotectedWatchedListRooms`,
`explicitlyProtectedRoomIds`, `protectedJoinedRoomIds` was incomprehensible.
https://github.com/matrix-org/mjolnir/issues/370

Separating out the management of `explicitlyProtectedRoomIds`, then
making sure all policy lists have to be explicitly protected
(in either setting of `config.protectAllJoinedRooms`) will make
this code much much simpler.
We will later change the `status` command to explicitly show
which lists are watched and which are watched and protected.
This commit is contained in:
gnuxie 2022-10-13 17:19:32 +01:00 committed by Gnuxie
parent da084328a9
commit 58e36d4e23
5 changed files with 140 additions and 7 deletions

View File

@ -36,6 +36,7 @@
"typescript-formatter": "^7.2"
},
"dependencies": {
"await-lock": "^2.2.2",
"express": "^4.17",
"html-to-text": "^8.0.0",
"humanize-duration": "^3.27.1",

View File

@ -35,7 +35,7 @@ import RuleServer from "./models/RuleServer";
import { ThrottlingQueue } from "./queues/ThrottlingQueue";
import { IConfig } from "./config";
import PolicyList from "./models/PolicyList";
import { ProtectedRooms } from "./ProtectedRooms";
import { ProtectedRoomsSet } from "./ProtectedRoomsSet";
import ManagementRoomOutput from "./ManagementRoomOutput";
import { ProtectionManager } from "./protections/ProtectionManager";
import { RoomMemberManager } from "./RoomMembers";
@ -45,7 +45,6 @@ export const STATE_CHECKING_PERMISSIONS = "checking_permissions";
export const STATE_SYNCING = "syncing";
export const STATE_RUNNING = "running";
const PROTECTED_ROOMS_EVENT_TYPE = "org.matrix.mjolnir.protected_rooms";
const WATCHED_LISTS_EVENT_TYPE = "org.matrix.mjolnir.watched_lists";
const WARN_UNPROTECTED_ROOM_EVENT_PREFIX = "org.matrix.mjolnir.unprotected_room_warning.for.";
@ -78,7 +77,7 @@ export class Mjolnir {
* These are eventually are exluded from `protectedRooms` in `applyUnprotectedRooms` via `resyncJoinedRooms`.
*/
private unprotectedWatchedListRooms: string[] = [];
public readonly protectedRoomsTracker: ProtectedRooms;
public readonly protectedRoomsTracker: ProtectedRoomsSet;
private webapis: WebAPIs;
public taskQueue: ThrottlingQueue;
/**
@ -272,7 +271,7 @@ export class Mjolnir {
this.managementRoomOutput = new ManagementRoomOutput(managementRoomId, client, config);
const protections = new ProtectionManager(this);
this.protectedRoomsTracker = new ProtectedRooms(client, clientUserId, managementRoomId, this.managementRoomOutput, protections, config);
this.protectedRoomsTracker = new ProtectedRoomsSet(client, clientUserId, managementRoomId, this.managementRoomOutput, protections, config);
}
public get lists(): PolicyList[] {

129
src/ProtectedRoomsConfig.ts Normal file
View File

@ -0,0 +1,129 @@
/*
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.
*/
import AwaitLock from 'await-lock';
import { extractRequestError, LogService, MatrixClient, Permalinks } from "matrix-bot-sdk";
import { IConfig } from "./config";
const PROTECTED_ROOMS_EVENT_TYPE = "org.matrix.mjolnir.protected_rooms";
/**
* Manages the set of rooms that the user has EXPLICITLY asked to be protected.
*/
export default class ProtectedRoomsConfig {
/**
* These are rooms that we EXPLICITLY asked Mjolnir to protect, usually via the `rooms add` command.
* These are NOT all of the rooms that mjolnir is protecting as with `config.protectAllJoinedRooms`.
*/
private explicitlyProtectedRooms = new Set</*room id*/string>();
/** This is to prevent clobbering the account data for the protected rooms if several rooms are explicitly protected concurrently. */
private accountDataLock = new AwaitLock();
constructor(private readonly client: MatrixClient) {
}
/**
* Load any rooms that have been explicitly protected from a Mjolnir config.
* Will also ensure we are able to join all of the rooms.
* @param config The config to load the rooms from under `config.protectedRooms`.
*/
public async loadProtectedRoomsFromConfig(config: IConfig): Promise<void> {
// Ensure we're also joined to the rooms we're protecting
LogService.info("ProtectedRoomsConfig", "Resolving protected rooms...");
const joinedRooms = await this.client.getJoinedRooms();
for (const roomRef of config.protectedRooms) {
const permalink = Permalinks.parseUrl(roomRef);
if (!permalink.roomIdOrAlias) continue;
let roomId = await this.client.resolveRoom(permalink.roomIdOrAlias);
if (!joinedRooms.includes(roomId)) {
roomId = await this.client.joinRoom(permalink.roomIdOrAlias, permalink.viaServers);
}
this.explicitlyProtectedRooms.add(roomId);
}
}
/**
* Load any rooms that have been explicitly protected from the account data of the mjolnir user.
* Will not ensure we can join all the rooms. This so mjolnir can continue to operate if bogus rooms have been persisted to the account data.
*/
public async loadProtectedRoomsFromAccountData(): Promise<void> {
LogService.debug("ProtectedRoomsConfig", "Loading protected rooms...");
try {
const data: { rooms?: string[] } | null = await this.client.getAccountData(PROTECTED_ROOMS_EVENT_TYPE);
if (data && data['rooms']) {
for (const roomId of data['rooms']) {
this.explicitlyProtectedRooms.add(roomId);
}
}
} catch (e) {
if (e.statusCode === 404) {
LogService.warn("ProtectedRoomsConfig", "Couldn't find any explicitly protected rooms from Mjolnir's account data, assuming first start.", extractRequestError(e));
} else {
throw e;
}
}
}
/**
* Save the room as explicitly protected.
* @param roomId The room to persist as explicitly protected.
*/
public async addProtectedRoom(roomId: string): Promise<void> {
this.explicitlyProtectedRooms.add(roomId);
await this.saveProtectedRoomsToAccountData();
}
/**
* Remove the room from the explicitly protected set of rooms.
* @param roomId The room that should no longer be persisted as protected.
*/
public async removeProtectedRoom(roomId: string): Promise<void> {
this.explicitlyProtectedRooms.delete(roomId);
await this.saveProtectedRoomsToAccountData([roomId]);
}
/**
* Get the set of explicitly protected rooms.
* This will NOT be the complete set of protected rooms, if `config.protectAllJoinedRooms` is true and should never be treated as the complete set.
* @returns The rooms that are marked as explicitly protected in both the config and Mjolnir's account data.
*/
public getExplicitlyProtectedRooms(): string[] {
return [...this.explicitlyProtectedRooms.keys()]
}
/**
* Persist the set of explicitly protected rooms to the client's account data.
* @param excludeRooms Rooms that should not be persisted to the account data, and removed if already present.
*/
private async saveProtectedRoomsToAccountData(excludeRooms: string[] = []): Promise<void> {
// NOTE: this stops Mjolnir from racing with itself when saving the config
// but it doesn't stop a third party client on the same account racing with us instead.
await this.accountDataLock.acquireAsync();
try {
const additionalProtectedRooms: string[] = await this.client.getAccountData(PROTECTED_ROOMS_EVENT_TYPE)
.then((rooms: {rooms?: string[]}) => Array.isArray(rooms?.rooms) ? rooms.rooms : [])
.catch(e => (LogService.warn("ProtectedRoomsConfig", "Could not load protected rooms from account data", extractRequestError(e)), []));
const roomsToSave = new Set([...this.explicitlyProtectedRooms.keys(), ...additionalProtectedRooms]);
excludeRooms.forEach(roomsToSave.delete, roomsToSave);
await this.client.setAccountData(PROTECTED_ROOMS_EVENT_TYPE, { rooms: Array.from(roomsToSave.keys()) });
} finally {
this.accountDataLock.release();
}
}
}

View File

@ -42,7 +42,7 @@ import { htmlEscape } from "./utils";
* It is also important not to tie this to the one group of rooms that a mjolnir may watch
* as in future we might want to borrow this class to represent a space https://github.com/matrix-org/mjolnir/issues/283.
*/
export class ProtectedRooms {
export class ProtectedRoomsSet {
private protectedRooms = new Set</* room id */string>();
@ -228,7 +228,7 @@ export class ProtectedRooms {
}
}
public async addProtectedRoom(roomId: string): Promise<void> {
public addProtectedRoom(roomId: string): void {
if (this.protectedRooms.has(roomId)) {
// we need to protect ourselves form syncing all the lists unnecessarily
// as Mjolnir does call this method repeatedly.
@ -236,7 +236,6 @@ export class ProtectedRooms {
}
this.protectedRooms.add(roomId);
this.protectedRoomActivityTracker.addProtectedRoom(roomId);
await this.syncLists(this.config.verboseLogging);
}
public removeProtectedRoom(roomId: string): void {

View File

@ -389,6 +389,11 @@ asynckit@^0.4.0:
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
await-lock@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.2.2.tgz#a95a9b269bfd2f69d22b17a321686f551152bcef"
integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==
aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"