Skip to content

Commit 97673cd

Browse files
committed
Make Mjolnir use ProtectedRoomsConfig
#370
1 parent 58e36d4 commit 97673cd

File tree

1 file changed

+75
-114
lines changed

1 file changed

+75
-114
lines changed

src/Mjolnir.ts

Lines changed: 75 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ limitations under the License.
1515
*/
1616

1717
import {
18-
CreateEvent,
1918
extractRequestError,
2019
LogLevel,
2120
LogService,
@@ -39,6 +38,7 @@ import { ProtectedRoomsSet } from "./ProtectedRoomsSet";
3938
import ManagementRoomOutput from "./ManagementRoomOutput";
4039
import { ProtectionManager } from "./protections/ProtectionManager";
4140
import { RoomMemberManager } from "./RoomMembers";
41+
import ProtectedRoomsConfig from "./ProtectedRoomsConfig";
4242

4343
export const STATE_NOT_STARTED = "not_started";
4444
export const STATE_CHECKING_PERMISSIONS = "checking_permissions";
@@ -64,19 +64,8 @@ export class Mjolnir {
6464
* but have been flagged by the automatic spam detection as suispicous
6565
*/
6666
private unlistedUserRedactionQueue = new UnlistedUserRedactionQueue();
67-
/**
68-
* Every room that we are joined to except the management room. Used to implement `config.protectAllJoinedRooms`.
69-
*/
70-
private protectedJoinedRoomIds: string[] = [];
71-
/**
72-
* These are rooms that were explicitly said to be protected either in the config, or by what is present in the account data for `org.matrix.mjolnir.protected_rooms`.
73-
*/
74-
private explicitlyProtectedRoomIds: string[] = [];
75-
/**
76-
* These are rooms that we have joined to watch the list, but don't have permission to protect.
77-
* These are eventually are exluded from `protectedRooms` in `applyUnprotectedRooms` via `resyncJoinedRooms`.
78-
*/
79-
private unprotectedWatchedListRooms: string[] = [];
67+
68+
private protectedRoomsConfig: ProtectedRoomsConfig;
8069
public readonly protectedRoomsTracker: ProtectedRoomsSet;
8170
private webapis: WebAPIs;
8271
public taskQueue: ThrottlingQueue;
@@ -153,21 +142,7 @@ export class Mjolnir {
153142
*/
154143
static async setupMjolnirFromConfig(client: MatrixClient, config: IConfig): Promise<Mjolnir> {
155144
const policyLists: PolicyList[] = [];
156-
const protectedRooms: { [roomId: string]: string } = {};
157145
const joinedRooms = await client.getJoinedRooms();
158-
// Ensure we're also joined to the rooms we're protecting
159-
LogService.info("index", "Resolving protected rooms...");
160-
for (const roomRef of config.protectedRooms) {
161-
const permalink = Permalinks.parseUrl(roomRef);
162-
if (!permalink.roomIdOrAlias) continue;
163-
164-
let roomId = await client.resolveRoom(permalink.roomIdOrAlias);
165-
if (!joinedRooms.includes(roomId)) {
166-
roomId = await client.joinRoom(permalink.roomIdOrAlias, permalink.viaServers);
167-
}
168-
169-
protectedRooms[roomId] = roomRef;
170-
}
171146

172147
// Ensure we're also in the management room
173148
LogService.info("index", "Resolving management room...");
@@ -177,7 +152,7 @@ export class Mjolnir {
177152
}
178153

179154
const ruleServer = config.web.ruleServer ? new RuleServer() : null;
180-
const mjolnir = new Mjolnir(client, await client.getUserId(), managementRoomId, config, protectedRooms, policyLists, ruleServer);
155+
const mjolnir = new Mjolnir(client, await client.getUserId(), managementRoomId, config, policyLists, ruleServer);
181156
await mjolnir.managementRoomOutput.logMessage(LogLevel.INFO, "index", "Mjolnir is starting up. Use !mjolnir to query status.");
182157
Mjolnir.addJoinOnInviteListener(mjolnir, client, config);
183158
return mjolnir;
@@ -188,16 +163,11 @@ export class Mjolnir {
188163
private readonly clientUserId: string,
189164
public readonly managementRoomId: string,
190165
public readonly config: IConfig,
191-
/*
192-
* All the rooms that Mjolnir is protecting and their permalinks.
193-
* If `config.protectAllJoinedRooms` is specified, then `protectedRooms` will be all joined rooms except watched banlists that we can't protect (because they aren't curated by us).
194-
*/
195-
public readonly protectedRooms: { [roomId: string]: string },
196166
private policyLists: PolicyList[],
197167
// Combines the rules from ban lists so they can be served to a homeserver module or another consumer.
198168
public readonly ruleServer: RuleServer | null,
199169
) {
200-
this.explicitlyProtectedRoomIds = Object.keys(this.protectedRooms);
170+
this.protectedRoomsConfig = new ProtectedRoomsConfig(client);
201171

202172
// Setup bot.
203173

@@ -296,9 +266,6 @@ export class Mjolnir {
296266
*/
297267
public async start() {
298268
try {
299-
// Start the bot.
300-
await this.client.start();
301-
302269
// Start the web server.
303270
console.log("Starting web server");
304271
await this.webapis.start();
@@ -321,27 +288,21 @@ export class Mjolnir {
321288
this.currentState = STATE_CHECKING_PERMISSIONS;
322289

323290
await this.managementRoomOutput.logMessage(LogLevel.DEBUG, "Mjolnir@startup", "Loading protected rooms...");
291+
await this.protectedRoomsConfig.loadProtectedRoomsFromConfig(this.config);
292+
await this.protectedRoomsConfig.loadProtectedRoomsFromAccountData();
293+
this.protectedRoomsConfig.getExplicitlyProtectedRooms().forEach(this.protectRoom, this);
324294
await this.resyncJoinedRooms(false);
325-
try {
326-
const data: { rooms?: string[] } | null = await this.client.getAccountData(PROTECTED_ROOMS_EVENT_TYPE);
327-
if (data && data['rooms']) {
328-
for (const roomId of data['rooms']) {
329-
this.protectedRooms[roomId] = Permalinks.forRoom(roomId);
330-
this.explicitlyProtectedRoomIds.push(roomId);
331-
}
332-
}
333-
} catch (e) {
334-
LogService.warn("Mjolnir", extractRequestError(e));
335-
}
336295
await this.buildWatchedPolicyLists();
337-
this.applyUnprotectedRooms();
338296
await this.protectionManager.start();
339297

340298
if (this.config.verifyPermissionsOnStartup) {
341299
await this.managementRoomOutput.logMessage(LogLevel.INFO, "Mjolnir@startup", "Checking permissions...");
342300
await this.protectedRoomsTracker.verifyPermissions(this.config.verboseLogging);
343301
}
344302

303+
// Start the bot.
304+
await this.client.start();
305+
345306
this.currentState = STATE_SYNCING;
346307
if (this.config.syncOnStartup) {
347308
await this.managementRoomOutput.logMessage(LogLevel.INFO, "Mjolnir@startup", "Syncing lists...");
@@ -374,72 +335,82 @@ export class Mjolnir {
374335
this.reportPoller?.stop();
375336
}
376337

338+
/**
339+
* Explicitly protect this room, adding it to the account data.
340+
* Should NOT be used to protect a room to implement e.g. `config.protectAllJoinedRooms`,
341+
* use `protectRoom` instead.
342+
* @param roomId The room to be explicitly protected by mjolnir and persisted in config.
343+
*/
377344
public async addProtectedRoom(roomId: string) {
378-
this.protectedRooms[roomId] = Permalinks.forRoom(roomId);
379-
this.roomJoins.addRoom(roomId);
380-
this.protectedRoomsTracker.addProtectedRoom(roomId);
381-
382-
const unprotectedIdx = this.unprotectedWatchedListRooms.indexOf(roomId);
383-
if (unprotectedIdx >= 0) this.unprotectedWatchedListRooms.splice(unprotectedIdx, 1);
384-
this.explicitlyProtectedRoomIds.push(roomId);
345+
await this.protectedRoomsConfig.addProtectedRoom(roomId);
346+
this.protectRoom(roomId);
347+
}
385348

386-
let additionalProtectedRooms: { rooms?: string[] } | null = null;
387-
try {
388-
additionalProtectedRooms = await this.client.getAccountData(PROTECTED_ROOMS_EVENT_TYPE);
389-
} catch (e) {
390-
LogService.warn("Mjolnir", extractRequestError(e));
391-
}
392-
const rooms = (additionalProtectedRooms?.rooms ?? []);
393-
rooms.push(roomId);
394-
await this.client.setAccountData(PROTECTED_ROOMS_EVENT_TYPE, { rooms: rooms });
349+
/**
350+
* Protect the room, but do not persist it to the account data.
351+
* @param roomId The room to protect.
352+
*/
353+
private protectRoom(roomId: string): void {
354+
this.protectedRoomsTracker.addProtectedRoom(roomId);
355+
this.roomJoins.addRoom(roomId);
395356
}
396357

358+
/**
359+
* Remove a room from the explicitly protect set of rooms that is persisted to account data.
360+
* Should NOT be used to remove a room that we have left, e.g. when implementing `config.protectAllJoinedRooms`,
361+
* use `unprotectRoom` instead.
362+
* @param roomId The room to remove from account data and stop protecting.
363+
*/
397364
public async removeProtectedRoom(roomId: string) {
398-
delete this.protectedRooms[roomId];
365+
await this.protectedRoomsConfig.removeProtectedRoom(roomId);
366+
this.unprotectRoom(roomId);
367+
}
368+
369+
/**
370+
* Unprotect a room.
371+
* @param roomId The room to stop protecting.
372+
*/
373+
private unprotectRoom(roomId: string): void {
399374
this.roomJoins.removeRoom(roomId);
400375
this.protectedRoomsTracker.removeProtectedRoom(roomId);
401-
402-
const idx = this.explicitlyProtectedRoomIds.indexOf(roomId);
403-
if (idx >= 0) this.explicitlyProtectedRoomIds.splice(idx, 1);
404-
405-
let additionalProtectedRooms: { rooms?: string[] } | null = null;
406-
try {
407-
additionalProtectedRooms = await this.client.getAccountData(PROTECTED_ROOMS_EVENT_TYPE);
408-
} catch (e) {
409-
LogService.warn("Mjolnir", extractRequestError(e));
410-
}
411-
additionalProtectedRooms = { rooms: additionalProtectedRooms?.rooms?.filter(r => r !== roomId) ?? [] };
412-
await this.client.setAccountData(PROTECTED_ROOMS_EVENT_TYPE, additionalProtectedRooms);
413376
}
414377

415-
// See https://github.com/matrix-org/mjolnir/issues/370.
416-
private async resyncJoinedRooms(withSync = true) {
378+
/**
379+
* Resynchronize the protected rooms with rooms that the mjolnir user is joined to.
380+
* This is to implement `config.protectAllJoinedRooms` functionality.
381+
* @param withSync Whether to synchronize all protected rooms with the watched policy lists afterwards.
382+
*/
383+
private async resyncJoinedRooms(withSync = true): Promise<void> {
417384
if (!this.config.protectAllJoinedRooms) return;
418385

419-
const joinedRoomIds = (await this.client.getJoinedRooms())
420-
.filter(r => r !== this.managementRoomId && !this.unprotectedWatchedListRooms.includes(r));
421-
const oldRoomIdsSet = new Set(this.protectedJoinedRoomIds);
422-
const joinedRoomIdsSet = new Set(joinedRoomIds);
386+
// We filter out all policy rooms so that we only protect ones that are
387+
// explicitly protected, so that we don't try to protect lists that we are just watching.
388+
const filterOutManagementAndPolicyRooms = (roomId: string) => {
389+
const policyListIds = this.policyLists.map(list => list.roomId);
390+
return roomId !== this.managementRoomId && !policyListIds.includes(roomId);
391+
};
392+
393+
const joinedRoomIdsToProtect = new Set([
394+
...(await this.client.getJoinedRooms()).filter(filterOutManagementAndPolicyRooms),
395+
// We do this specifically so policy lists that have been explicitly marked as protected
396+
// will be protected.
397+
...this.protectedRoomsConfig.getExplicitlyProtectedRooms(),
398+
]);
399+
const previousRoomIdsProtecting = new Set(this.protectedRoomsTracker.getProtectedRooms());
423400
// find every room that we have left (since last time)
424-
for (const roomId of oldRoomIdsSet.keys()) {
425-
if (!joinedRoomIdsSet.has(roomId)) {
401+
for (const roomId of previousRoomIdsProtecting.keys()) {
402+
if (!joinedRoomIdsToProtect.has(roomId)) {
426403
// Then we have left this room.
427-
delete this.protectedRooms[roomId];
428-
this.roomJoins.removeRoom(roomId);
404+
this.unprotectRoom(roomId);
429405
}
430406
}
431407
// find every room that we have joined (since last time).
432-
for (const roomId of joinedRoomIdsSet.keys()) {
433-
if (!oldRoomIdsSet.has(roomId)) {
408+
for (const roomId of joinedRoomIdsToProtect.keys()) {
409+
if (!previousRoomIdsProtecting.has(roomId)) {
434410
// Then we have joined this room
435-
this.roomJoins.addRoom(roomId);
436-
this.protectedRooms[roomId] = Permalinks.forRoom(roomId);
411+
this.protectRoom(roomId);
437412
}
438413
}
439-
// update our internal representation of joined rooms.
440-
this.protectedJoinedRoomIds = joinedRoomIds;
441-
442-
this.applyUnprotectedRooms();
443414

444415
if (withSync) {
445416
await this.protectedRoomsTracker.syncLists(this.config.verboseLogging);
@@ -505,13 +476,7 @@ export class Mjolnir {
505476

506477
public async warnAboutUnprotectedPolicyListRoom(roomId: string) {
507478
if (!this.config.protectAllJoinedRooms) return; // doesn't matter
508-
if (this.explicitlyProtectedRoomIds.includes(roomId)) return; // explicitly protected
509-
510-
const createEvent = new CreateEvent(await this.client.getRoomStateEvent(roomId, "m.room.create", ""));
511-
if (createEvent.creator === await this.client.getUserId()) return; // we created it
512-
513-
if (!this.unprotectedWatchedListRooms.includes(roomId)) this.unprotectedWatchedListRooms.push(roomId);
514-
this.applyUnprotectedRooms();
479+
if (this.protectedRoomsConfig.getExplicitlyProtectedRooms().includes(roomId)) return; // explicitly protected
515480

516481
try {
517482
const accountData: { warned: boolean } | null = await this.client.getAccountData(WARN_UNPROTECTED_ROOM_EVENT_PREFIX + roomId);
@@ -525,16 +490,8 @@ export class Mjolnir {
525490
}
526491

527492
/**
528-
* So this is called to retroactively remove protected rooms from Mjolnir's internal model of joined rooms.
529-
* This is really shit and needs to be changed asap. Unacceptable even.
493+
* Load the watched policy lists from account data, only used when Mjolnir is initialized.
530494
*/
531-
private applyUnprotectedRooms() {
532-
for (const roomId of this.unprotectedWatchedListRooms) {
533-
delete this.protectedRooms[roomId];
534-
this.protectedRoomsTracker.removeProtectedRoom(roomId);
535-
}
536-
}
537-
538495
private async buildWatchedPolicyLists() {
539496
this.policyLists = [];
540497
const joinedRooms = await this.client.getJoinedRooms();
@@ -543,7 +500,11 @@ export class Mjolnir {
543500
try {
544501
watchedListsEvent = await this.client.getAccountData(WATCHED_LISTS_EVENT_TYPE);
545502
} catch (e) {
546-
// ignore - not important
503+
if (e.statusCode === 404) {
504+
LogService.warn('Mjolnir', "Couldn't find account data for Mjolnir's watched lists, assuming first start.", extractRequestError(e));
505+
} else {
506+
throw e;
507+
}
547508
}
548509

549510
for (const roomRef of (watchedListsEvent?.references || [])) {

0 commit comments

Comments
 (0)