@@ -15,7 +15,6 @@ limitations under the License.
1515*/
1616
1717import {
18- CreateEvent ,
1918 extractRequestError ,
2019 LogLevel ,
2120 LogService ,
@@ -39,6 +38,7 @@ import { ProtectedRoomsSet } from "./ProtectedRoomsSet";
3938import ManagementRoomOutput from "./ManagementRoomOutput" ;
4039import { ProtectionManager } from "./protections/ProtectionManager" ;
4140import { RoomMemberManager } from "./RoomMembers" ;
41+ import ProtectedRoomsConfig from "./ProtectedRoomsConfig" ;
4242
4343export const STATE_NOT_STARTED = "not_started" ;
4444export 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