Skip to content

Commit 58e36d4

Browse files
committed
Factor out protected rooms config management from Mjolnir.
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.
1 parent da08432 commit 58e36d4

File tree

5 files changed

+140
-7
lines changed

5 files changed

+140
-7
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"typescript-formatter": "^7.2"
3737
},
3838
"dependencies": {
39+
"await-lock": "^2.2.2",
3940
"express": "^4.17",
4041
"html-to-text": "^8.0.0",
4142
"humanize-duration": "^3.27.1",

src/Mjolnir.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import RuleServer from "./models/RuleServer";
3535
import { ThrottlingQueue } from "./queues/ThrottlingQueue";
3636
import { IConfig } from "./config";
3737
import PolicyList from "./models/PolicyList";
38-
import { ProtectedRooms } from "./ProtectedRooms";
38+
import { ProtectedRoomsSet } from "./ProtectedRoomsSet";
3939
import ManagementRoomOutput from "./ManagementRoomOutput";
4040
import { ProtectionManager } from "./protections/ProtectionManager";
4141
import { RoomMemberManager } from "./RoomMembers";
@@ -45,7 +45,6 @@ export const STATE_CHECKING_PERMISSIONS = "checking_permissions";
4545
export const STATE_SYNCING = "syncing";
4646
export const STATE_RUNNING = "running";
4747

48-
const PROTECTED_ROOMS_EVENT_TYPE = "org.matrix.mjolnir.protected_rooms";
4948
const WATCHED_LISTS_EVENT_TYPE = "org.matrix.mjolnir.watched_lists";
5049
const WARN_UNPROTECTED_ROOM_EVENT_PREFIX = "org.matrix.mjolnir.unprotected_room_warning.for.";
5150

@@ -78,7 +77,7 @@ export class Mjolnir {
7877
* These are eventually are exluded from `protectedRooms` in `applyUnprotectedRooms` via `resyncJoinedRooms`.
7978
*/
8079
private unprotectedWatchedListRooms: string[] = [];
81-
public readonly protectedRoomsTracker: ProtectedRooms;
80+
public readonly protectedRoomsTracker: ProtectedRoomsSet;
8281
private webapis: WebAPIs;
8382
public taskQueue: ThrottlingQueue;
8483
/**
@@ -272,7 +271,7 @@ export class Mjolnir {
272271

273272
this.managementRoomOutput = new ManagementRoomOutput(managementRoomId, client, config);
274273
const protections = new ProtectionManager(this);
275-
this.protectedRoomsTracker = new ProtectedRooms(client, clientUserId, managementRoomId, this.managementRoomOutput, protections, config);
274+
this.protectedRoomsTracker = new ProtectedRoomsSet(client, clientUserId, managementRoomId, this.managementRoomOutput, protections, config);
276275
}
277276

278277
public get lists(): PolicyList[] {

src/ProtectedRoomsConfig.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
Copyright 2019, 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import AwaitLock from 'await-lock';
18+
import { extractRequestError, LogService, MatrixClient, Permalinks } from "matrix-bot-sdk";
19+
import { IConfig } from "./config";
20+
const PROTECTED_ROOMS_EVENT_TYPE = "org.matrix.mjolnir.protected_rooms";
21+
22+
/**
23+
* Manages the set of rooms that the user has EXPLICITLY asked to be protected.
24+
*/
25+
export default class ProtectedRoomsConfig {
26+
27+
/**
28+
* These are rooms that we EXPLICITLY asked Mjolnir to protect, usually via the `rooms add` command.
29+
* These are NOT all of the rooms that mjolnir is protecting as with `config.protectAllJoinedRooms`.
30+
*/
31+
private explicitlyProtectedRooms = new Set</*room id*/string>();
32+
/** This is to prevent clobbering the account data for the protected rooms if several rooms are explicitly protected concurrently. */
33+
private accountDataLock = new AwaitLock();
34+
35+
constructor(private readonly client: MatrixClient) {
36+
37+
}
38+
39+
/**
40+
* Load any rooms that have been explicitly protected from a Mjolnir config.
41+
* Will also ensure we are able to join all of the rooms.
42+
* @param config The config to load the rooms from under `config.protectedRooms`.
43+
*/
44+
public async loadProtectedRoomsFromConfig(config: IConfig): Promise<void> {
45+
// Ensure we're also joined to the rooms we're protecting
46+
LogService.info("ProtectedRoomsConfig", "Resolving protected rooms...");
47+
const joinedRooms = await this.client.getJoinedRooms();
48+
for (const roomRef of config.protectedRooms) {
49+
const permalink = Permalinks.parseUrl(roomRef);
50+
if (!permalink.roomIdOrAlias) continue;
51+
52+
let roomId = await this.client.resolveRoom(permalink.roomIdOrAlias);
53+
if (!joinedRooms.includes(roomId)) {
54+
roomId = await this.client.joinRoom(permalink.roomIdOrAlias, permalink.viaServers);
55+
}
56+
this.explicitlyProtectedRooms.add(roomId);
57+
}
58+
}
59+
60+
/**
61+
* Load any rooms that have been explicitly protected from the account data of the mjolnir user.
62+
* 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.
63+
*/
64+
public async loadProtectedRoomsFromAccountData(): Promise<void> {
65+
LogService.debug("ProtectedRoomsConfig", "Loading protected rooms...");
66+
try {
67+
const data: { rooms?: string[] } | null = await this.client.getAccountData(PROTECTED_ROOMS_EVENT_TYPE);
68+
if (data && data['rooms']) {
69+
for (const roomId of data['rooms']) {
70+
this.explicitlyProtectedRooms.add(roomId);
71+
}
72+
}
73+
} catch (e) {
74+
if (e.statusCode === 404) {
75+
LogService.warn("ProtectedRoomsConfig", "Couldn't find any explicitly protected rooms from Mjolnir's account data, assuming first start.", extractRequestError(e));
76+
} else {
77+
throw e;
78+
}
79+
}
80+
}
81+
82+
/**
83+
* Save the room as explicitly protected.
84+
* @param roomId The room to persist as explicitly protected.
85+
*/
86+
public async addProtectedRoom(roomId: string): Promise<void> {
87+
this.explicitlyProtectedRooms.add(roomId);
88+
await this.saveProtectedRoomsToAccountData();
89+
}
90+
91+
/**
92+
* Remove the room from the explicitly protected set of rooms.
93+
* @param roomId The room that should no longer be persisted as protected.
94+
*/
95+
public async removeProtectedRoom(roomId: string): Promise<void> {
96+
this.explicitlyProtectedRooms.delete(roomId);
97+
await this.saveProtectedRoomsToAccountData([roomId]);
98+
}
99+
100+
/**
101+
* Get the set of explicitly protected rooms.
102+
* This will NOT be the complete set of protected rooms, if `config.protectAllJoinedRooms` is true and should never be treated as the complete set.
103+
* @returns The rooms that are marked as explicitly protected in both the config and Mjolnir's account data.
104+
*/
105+
public getExplicitlyProtectedRooms(): string[] {
106+
return [...this.explicitlyProtectedRooms.keys()]
107+
}
108+
109+
/**
110+
* Persist the set of explicitly protected rooms to the client's account data.
111+
* @param excludeRooms Rooms that should not be persisted to the account data, and removed if already present.
112+
*/
113+
private async saveProtectedRoomsToAccountData(excludeRooms: string[] = []): Promise<void> {
114+
// NOTE: this stops Mjolnir from racing with itself when saving the config
115+
// but it doesn't stop a third party client on the same account racing with us instead.
116+
await this.accountDataLock.acquireAsync();
117+
try {
118+
const additionalProtectedRooms: string[] = await this.client.getAccountData(PROTECTED_ROOMS_EVENT_TYPE)
119+
.then((rooms: {rooms?: string[]}) => Array.isArray(rooms?.rooms) ? rooms.rooms : [])
120+
.catch(e => (LogService.warn("ProtectedRoomsConfig", "Could not load protected rooms from account data", extractRequestError(e)), []));
121+
122+
const roomsToSave = new Set([...this.explicitlyProtectedRooms.keys(), ...additionalProtectedRooms]);
123+
excludeRooms.forEach(roomsToSave.delete, roomsToSave);
124+
await this.client.setAccountData(PROTECTED_ROOMS_EVENT_TYPE, { rooms: Array.from(roomsToSave.keys()) });
125+
} finally {
126+
this.accountDataLock.release();
127+
}
128+
}
129+
}

src/ProtectedRooms.ts renamed to src/ProtectedRoomsSet.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import { htmlEscape } from "./utils";
4242
* It is also important not to tie this to the one group of rooms that a mjolnir may watch
4343
* as in future we might want to borrow this class to represent a space https://github.com/matrix-org/mjolnir/issues/283.
4444
*/
45-
export class ProtectedRooms {
45+
export class ProtectedRoomsSet {
4646

4747
private protectedRooms = new Set</* room id */string>();
4848

@@ -228,15 +228,14 @@ export class ProtectedRooms {
228228
}
229229
}
230230

231-
public async addProtectedRoom(roomId: string): Promise<void> {
231+
public addProtectedRoom(roomId: string): void {
232232
if (this.protectedRooms.has(roomId)) {
233233
// we need to protect ourselves form syncing all the lists unnecessarily
234234
// as Mjolnir does call this method repeatedly.
235235
return;
236236
}
237237
this.protectedRooms.add(roomId);
238238
this.protectedRoomActivityTracker.addProtectedRoom(roomId);
239-
await this.syncLists(this.config.verboseLogging);
240239
}
241240

242241
public removeProtectedRoom(roomId: string): void {

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,11 @@ asynckit@^0.4.0:
389389
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
390390
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
391391

392+
await-lock@^2.2.2:
393+
version "2.2.2"
394+
resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.2.2.tgz#a95a9b269bfd2f69d22b17a321686f551152bcef"
395+
integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==
396+
392397
aws-sign2@~0.7.0:
393398
version "0.7.0"
394399
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"

0 commit comments

Comments
 (0)