Skip to content

Commit

Permalink
Factor out protected rooms config management from Mjolnir.
Browse files Browse the repository at this point in the history
The combination of `resyncJoinedRooms`, `unprotectedWatchedListRooms`,
`explicitlyProtectedRoomIds`, `protectedJoinedRoomIds` was incomprehensible.
#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.
  • Loading branch information
Gnuxie committed Oct 19, 2022
1 parent da08432 commit 2ad1c11
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 7 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 3 additions & 4 deletions src/Mjolnir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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.";

Expand Down Expand Up @@ -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;
/**
Expand Down Expand Up @@ -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[] {
Expand Down
129 changes: 129 additions & 0 deletions src/ProtectedRoomsConfig.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
5 changes: 2 additions & 3 deletions src/ProtectedRooms.ts → src/ProtectedRoomsSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>();

Expand Down Expand Up @@ -228,15 +228,14 @@ 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.
return;
}
this.protectedRooms.add(roomId);
this.protectedRoomActivityTracker.addProtectedRoom(roomId);
await this.syncLists(this.config.verboseLogging);
}

public removeProtectedRoom(roomId: string): void {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 2ad1c11

Please sign in to comment.