2019-09-27 17:15:10 -04:00
/ *
2021-07-01 17:11:27 -04:00
Copyright 2019 - 2021 The Matrix . org Foundation C . I . C .
2019-09-27 17:15:10 -04:00
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 .
* /
2021-07-01 17:11:27 -04:00
import {
CreateEvent ,
extractRequestError ,
LogLevel ,
LogService ,
MatrixClient ,
MatrixGlob ,
2021-09-27 10:52:28 -04:00
MembershipEvent ,
2021-07-01 17:11:27 -04:00
Permalinks ,
2022-02-15 10:44:41 -05:00
UserID ,
TextualMessageEventContent
2021-07-01 17:11:27 -04:00
} from "matrix-bot-sdk" ;
2021-10-07 08:42:08 -04:00
2022-02-15 08:51:20 -05:00
import BanList , { ALL_RULE_TYPES as ALL_BAN_LIST_RULE_TYPES , ListRuleChange , RULE_ROOM , RULE_SERVER , RULE_USER } from "./models/BanList" ;
2019-09-27 17:15:10 -04:00
import { applyServerAcls } from "./actions/ApplyAcl" ;
import { RoomUpdateError } from "./models/RoomUpdateError" ;
import { COMMAND_PREFIX , handleCommand } from "./commands/CommandHandler" ;
2019-09-27 21:54:13 -04:00
import { applyUserBans } from "./actions/ApplyBan" ;
2019-10-04 23:02:37 -04:00
import config from "./config" ;
2019-11-06 21:17:11 -05:00
import ErrorCache , { ERROR_KIND_FATAL , ERROR_KIND_PERMISSION } from "./ErrorCache" ;
2022-02-24 07:43:31 -05:00
import { Protection } from "./protections/IProtection" ;
2019-12-04 20:46:29 -05:00
import { PROTECTIONS } from "./protections/protections" ;
2022-01-25 09:47:50 -05:00
import { ProtectionSettingValidationError } from "./protections/ProtectionSettings" ;
2021-09-14 09:36:53 -04:00
import { UnlistedUserRedactionQueue } from "./queues/UnlistedUserRedactionQueue" ;
2020-06-12 10:03:08 -04:00
import { Healthz } from "./health/healthz" ;
2021-09-14 07:17:29 -04:00
import { EventRedactionQueue , RedactUserInRoom } from "./queues/EventRedactionQueue" ;
2022-02-02 07:43:05 -05:00
import { htmlEscape } from "./utils" ;
2021-11-09 07:15:49 -05:00
import { ReportManager } from "./report/ReportManager" ;
2021-10-07 08:42:08 -04:00
import { WebAPIs } from "./webapis/WebAPIs" ;
2022-02-15 10:44:41 -05:00
import { replaceRoomIdsWithPills } from "./utils" ;
2021-10-22 04:47:05 -04:00
import RuleServer from "./models/RuleServer" ;
2019-09-27 17:15:10 -04:00
2022-02-15 10:44:41 -05:00
const levelToFn = {
[ LogLevel . DEBUG . toString ( ) ] : LogService . debug ,
[ LogLevel . INFO . toString ( ) ] : LogService . info ,
[ LogLevel . WARN . toString ( ) ] : LogService . warn ,
[ LogLevel . ERROR . toString ( ) ] : LogService . error ,
} ;
2019-10-04 23:22:18 -04:00
export const STATE_NOT_STARTED = "not_started" ;
export const STATE_CHECKING_PERMISSIONS = "checking_permissions" ;
export const STATE_SYNCING = "syncing" ;
export const STATE_RUNNING = "running" ;
2019-10-08 15:58:31 -04:00
const WATCHED_LISTS_EVENT_TYPE = "org.matrix.mjolnir.watched_lists" ;
2019-12-04 20:46:29 -05:00
const ENABLED_PROTECTIONS_EVENT_TYPE = "org.matrix.mjolnir.enabled_protections" ;
2020-01-21 15:43:36 -05:00
const PROTECTED_ROOMS_EVENT_TYPE = "org.matrix.mjolnir.protected_rooms" ;
2020-02-18 19:06:27 -05:00
const WARN_UNPROTECTED_ROOM_EVENT_PREFIX = "org.matrix.mjolnir.unprotected_room_warning.for." ;
2019-10-08 15:58:31 -04:00
2019-09-27 17:15:10 -04:00
export class Mjolnir {
2019-09-27 18:04:08 -04:00
private displayName : string ;
private localpart : string ;
2019-10-04 23:22:18 -04:00
private currentState : string = STATE_NOT_STARTED ;
2022-02-24 07:43:31 -05:00
public protections = new Map < string / * protection name * / , Protection > ( ) ;
2021-09-15 06:06:03 -04:00
/ * *
* This is for users who are not listed on a watchlist ,
* but have been flagged by the automatic spam detection as suispicous
* /
2021-09-14 09:36:53 -04:00
private unlistedUserRedactionQueue = new UnlistedUserRedactionQueue ( ) ;
2021-09-15 06:06:03 -04:00
/ * *
* This is a queue for redactions to process after mjolnir
* has finished applying ACL and bans when syncing .
* /
2021-09-14 07:17:29 -04:00
private eventRedactionQueue = new EventRedactionQueue ( ) ;
2019-12-09 21:56:12 -05:00
private automaticRedactionReasons : MatrixGlob [ ] = [ ] ;
2020-01-21 17:19:03 -05:00
private protectedJoinedRoomIds : string [ ] = [ ] ;
2020-02-18 19:06:27 -05:00
private explicitlyProtectedRoomIds : string [ ] = [ ] ;
private knownUnprotectedRooms : string [ ] = [ ] ;
2021-10-07 08:42:08 -04:00
private webapis : WebAPIs ;
2021-09-27 10:52:28 -04:00
/ * *
* Adds a listener to the client that will automatically accept invitations .
* @param { MatrixClient } client
* @param options By default accepts invites from anyone .
* @param { string } options . managementRoom The room to report ignored invitations to if ` recordIgnoredInvites ` is true .
* @param { boolean } options . recordIgnoredInvites Whether to report invites that will be ignored to the ` managementRoom ` .
* @param { boolean } options . autojoinOnlyIfManager Whether to only accept an invitation by a user present in the ` managementRoom ` .
* @param { string } options . acceptInvitesFromGroup A group of users to accept invites from , ignores invites form users not in this group .
* /
2022-02-02 07:43:05 -05:00
private static addJoinOnInviteListener ( mjolnir : Mjolnir , client : MatrixClient , options : { [ key : string ] : any } ) {
2021-09-27 10:52:28 -04:00
client . on ( "room.invite" , async ( roomId : string , inviteEvent : any ) = > {
const membershipEvent = new MembershipEvent ( inviteEvent ) ;
const reportInvite = async ( ) = > {
if ( ! options . recordIgnoredInvites ) return ; // Nothing to do
2022-01-17 11:24:12 -05:00
await client . sendMessage ( mjolnir . managementRoomId , {
2021-09-27 10:52:28 -04:00
msgtype : "m.text" ,
body : ` ${ membershipEvent . sender } has invited me to ${ roomId } but the config prevents me from accepting the invitation. `
+ ` If you would like this room protected, use "!mjolnir rooms add ${ roomId } " so I can accept the invite. ` ,
format : "org.matrix.custom.html" ,
formatted_body : ` ${ htmlEscape ( membershipEvent . sender ) } has invited me to ${ htmlEscape ( roomId ) } but the config prevents me from `
+ ` accepting the invitation. If you would like this room protected, use <code>!mjolnir rooms add ${ htmlEscape ( roomId ) } </code> `
+ ` so I can accept the invite. ` ,
} ) ;
} ;
if ( options . autojoinOnlyIfManager ) {
2022-01-17 11:24:12 -05:00
const managers = await client . getJoinedRoomMembers ( mjolnir . managementRoomId ) ;
2021-09-27 10:52:28 -04:00
if ( ! managers . includes ( membershipEvent . sender ) ) return reportInvite ( ) ; // ignore invite
} else {
const groupMembers = await client . unstableApis . getGroupUsers ( options . acceptInvitesFromGroup ) ;
const userIds = groupMembers . map ( m = > m . user_id ) ;
if ( ! userIds . includes ( membershipEvent . sender ) ) return reportInvite ( ) ; // ignore invite
}
return client . joinRoom ( roomId ) ;
} ) ;
}
/ * *
* Create a new Mjolnir instance from a client and the options in the configuration file , ready to be started .
* @param { MatrixClient } client The client for Mjolnir to use .
* @returns A new Mjolnir instance that can be started without further setup .
* /
static async setupMjolnirFromConfig ( client : MatrixClient ) : Promise < Mjolnir > {
const banLists : BanList [ ] = [ ] ;
const protectedRooms : { [ roomId : string ] : string } = { } ;
const joinedRooms = await client . getJoinedRooms ( ) ;
// Ensure we're also joined to the rooms we're protecting
LogService . info ( "index" , "Resolving protected rooms..." ) ;
for ( const roomRef of config . protectedRooms ) {
const permalink = Permalinks . parseUrl ( roomRef ) ;
if ( ! permalink . roomIdOrAlias ) continue ;
let roomId = await client . resolveRoom ( permalink . roomIdOrAlias ) ;
if ( ! joinedRooms . includes ( roomId ) ) {
roomId = await client . joinRoom ( permalink . roomIdOrAlias , permalink . viaServers ) ;
}
protectedRooms [ roomId ] = roomRef ;
}
// Ensure we're also in the management room
LogService . info ( "index" , "Resolving management room..." ) ;
const managementRoomId = await client . resolveRoom ( config . managementRoom ) ;
if ( ! joinedRooms . includes ( managementRoomId ) ) {
2022-01-17 11:24:12 -05:00
await client . joinRoom ( config . managementRoom ) ;
2021-09-27 10:52:28 -04:00
}
2021-10-22 04:47:05 -04:00
const ruleServer = config . web . ruleServer ? new RuleServer ( ) : null ;
const mjolnir = new Mjolnir ( client , managementRoomId , protectedRooms , banLists , ruleServer ) ;
2022-02-15 10:44:41 -05:00
await mjolnir . logMessage ( LogLevel . INFO , "index" , "Mjolnir is starting up. Use !mjolnir to query status." ) ;
2022-01-17 11:24:12 -05:00
Mjolnir . addJoinOnInviteListener ( mjolnir , client , config ) ;
return mjolnir ;
2021-09-27 10:52:28 -04:00
}
2019-09-27 17:15:10 -04:00
constructor (
2022-01-04 06:33:08 -05:00
public readonly client : MatrixClient ,
2022-01-17 11:24:12 -05:00
public readonly managementRoomId : string ,
2019-09-27 17:15:10 -04:00
public readonly protectedRooms : { [ roomId : string ] : string } ,
2019-10-08 13:25:57 -04:00
private banLists : BanList [ ] ,
2021-10-22 04:47:05 -04:00
// Combines the rules from ban lists so they can be served to a homeserver module or another consumer.
public readonly ruleServer : RuleServer | null ,
2019-09-27 17:15:10 -04:00
) {
2020-02-18 19:06:27 -05:00
this . explicitlyProtectedRoomIds = Object . keys ( this . protectedRooms ) ;
2019-12-09 21:56:12 -05:00
for ( const reason of config . automaticallyRedactForReasons ) {
this . automaticRedactionReasons . push ( new MatrixGlob ( reason . toLowerCase ( ) ) ) ;
}
2021-10-07 08:42:08 -04:00
// Setup bot.
2019-09-27 17:15:10 -04:00
client . on ( "room.event" , this . handleEvent . bind ( this ) ) ;
client . on ( "room.message" , async ( roomId , event ) = > {
2022-01-17 11:24:12 -05:00
if ( roomId !== this . managementRoomId ) return ;
2019-09-27 17:15:10 -04:00
if ( ! event [ 'content' ] ) return ;
const content = event [ 'content' ] ;
2019-09-27 18:04:08 -04:00
if ( content [ 'msgtype' ] === "m.text" && content [ 'body' ] ) {
2020-02-12 17:27:27 -05:00
const prefixes = [
COMMAND_PREFIX ,
this . localpart + ":" ,
this . displayName + ":" ,
await client . getUserId ( ) + ":" ,
2020-02-18 15:46:31 -05:00
this . localpart + " " ,
this . displayName + " " ,
await client . getUserId ( ) + " " ,
2020-02-12 17:27:27 -05:00
. . . config . commands . additionalPrefixes . map ( p = > ` ! ${ p } ` ) ,
. . . config . commands . additionalPrefixes . map ( p = > ` ${ p } : ` ) ,
2020-02-18 15:46:31 -05:00
. . . config . commands . additionalPrefixes . map ( p = > ` ${ p } ` ) ,
2020-02-12 17:27:27 -05:00
. . . config . commands . additionalPrefixes ,
] ;
if ( config . commands . allowNoPrefix ) prefixes . push ( "!" ) ;
2021-06-14 09:43:50 -04:00
const prefixUsed = prefixes . find ( p = > content [ 'body' ] . toLowerCase ( ) . startsWith ( p . toLowerCase ( ) ) ) ;
2019-09-27 22:07:16 -04:00
if ( ! prefixUsed ) return ;
// rewrite the event body to make the prefix uniform (in case the bot has spaces in its display name)
2020-02-12 17:27:27 -05:00
let restOfBody = content [ 'body' ] . substring ( prefixUsed . length ) ;
if ( ! restOfBody . startsWith ( " " ) ) restOfBody = ` ${ restOfBody } ` ;
event [ 'content' ] [ 'body' ] = COMMAND_PREFIX + restOfBody ;
2019-11-06 20:46:49 -05:00
LogService . info ( "Mjolnir" , ` Command being run by ${ event [ 'sender' ] } : ${ event [ 'content' ] [ 'body' ] } ` ) ;
2019-09-27 18:04:08 -04:00
2019-09-27 17:15:10 -04:00
await client . sendReadReceipt ( roomId , event [ 'event_id' ] ) ;
return handleCommand ( roomId , event , this ) ;
}
} ) ;
2019-09-27 18:04:08 -04:00
2020-01-21 17:19:03 -05:00
client . on ( "room.join" , ( roomId : string , event : any ) = > {
LogService . info ( "Mjolnir" , ` Joined ${ roomId } ` ) ;
return this . resyncJoinedRooms ( ) ;
} ) ;
client . on ( "room.leave" , ( roomId : string , event : any ) = > {
LogService . info ( "Mjolnir" , ` Left ${ roomId } ` ) ;
return this . resyncJoinedRooms ( ) ;
} ) ;
2019-09-27 18:04:08 -04:00
client . getUserId ( ) . then ( userId = > {
this . localpart = userId . split ( ':' ) [ 0 ] . substring ( 1 ) ;
return client . getUserProfile ( userId ) ;
} ) . then ( profile = > {
if ( profile [ 'displayname' ] ) {
this . displayName = profile [ 'displayname' ] ;
}
2020-01-21 15:43:36 -05:00
} ) ;
2021-10-07 08:42:08 -04:00
// Setup Web APIs
console . log ( "Creating Web APIs" ) ;
2022-02-08 08:07:42 -05:00
const reportManager = new ReportManager ( this ) ;
reportManager . on ( "report.new" , this . handleReport ) ;
this . webapis = new WebAPIs ( reportManager , this . ruleServer ) ;
2019-09-27 17:15:10 -04:00
}
2019-10-08 13:25:57 -04:00
public get lists ( ) : BanList [ ] {
return this . banLists ;
}
2019-10-04 23:22:18 -04:00
public get state ( ) : string {
return this . currentState ;
}
2022-02-24 07:43:31 -05:00
public get enabledProtections ( ) : Protection [ ] {
2022-02-02 10:31:11 -05:00
return [ . . . this . protections . values ( ) ] . filter ( p = > p . enabled ) ;
2019-12-04 20:46:29 -05:00
}
2021-09-15 06:06:03 -04:00
/ * *
2021-09-16 06:46:44 -04:00
* Returns the handler to flag a user for redaction , removing any future messages that they send .
* Typically this is used by the flooding or image protection on users that have not been banned from a list yet .
* It cannot used to redact any previous messages the user has sent , in that cas you should use the ` EventRedactionQueue ` .
2021-09-15 06:06:03 -04:00
* /
2021-09-14 09:36:53 -04:00
public get unlistedUserRedactionHandler ( ) : UnlistedUserRedactionQueue {
return this . unlistedUserRedactionQueue ;
2019-12-09 21:15:51 -05:00
}
2019-12-09 21:56:12 -05:00
public get automaticRedactGlobs ( ) : MatrixGlob [ ] {
return this . automaticRedactionReasons ;
}
2021-10-07 08:42:08 -04:00
/ * *
* Start Mjölnir .
* /
public async start() {
try {
// Start the bot.
await this . client . start ( ) ;
// Start the web server.
console . log ( "Starting web server" ) ;
await this . webapis . start ( ) ;
// Load the state.
2019-10-04 23:22:34 -04:00
this . currentState = STATE_CHECKING_PERMISSIONS ;
2020-02-18 19:06:27 -05:00
2022-02-15 10:44:41 -05:00
await this . logMessage ( LogLevel . DEBUG , "Mjolnir@startup" , "Loading protected rooms..." ) ;
2020-01-21 17:19:03 -05:00
await this . resyncJoinedRooms ( false ) ;
2020-01-21 15:43:36 -05:00
try {
2022-02-02 07:43:05 -05:00
const data : { rooms? : string [ ] } | null = await this . client . getAccountData ( PROTECTED_ROOMS_EVENT_TYPE ) ;
2020-02-18 19:06:27 -05:00
if ( data && data [ 'rooms' ] ) {
for ( const roomId of data [ 'rooms' ] ) {
this . protectedRooms [ roomId ] = Permalinks . forRoom ( roomId ) ;
this . explicitlyProtectedRoomIds . push ( roomId ) ;
}
}
2020-01-21 15:43:36 -05:00
} catch ( e ) {
2021-07-01 17:11:27 -04:00
LogService . warn ( "Mjolnir" , extractRequestError ( e ) ) ;
2020-01-21 15:43:36 -05:00
}
2020-02-18 19:06:27 -05:00
await this . buildWatchedBanLists ( ) ;
this . applyUnprotectedRooms ( ) ;
if ( config . verifyPermissionsOnStartup ) {
2022-02-15 10:44:41 -05:00
await this . logMessage ( LogLevel . INFO , "Mjolnir@startup" , "Checking permissions..." ) ;
2020-02-18 19:06:27 -05:00
await this . verifyPermissions ( config . verboseLogging ) ;
}
2021-10-07 08:42:08 -04:00
2020-02-18 19:06:27 -05:00
this . currentState = STATE_SYNCING ;
2019-10-04 23:02:37 -04:00
if ( config . syncOnStartup ) {
2022-02-15 10:44:41 -05:00
await this . logMessage ( LogLevel . INFO , "Mjolnir@startup" , "Syncing lists..." ) ;
2019-10-04 23:38:50 -04:00
await this . syncLists ( config . verboseLogging ) ;
2022-02-02 10:31:11 -05:00
await this . registerProtections ( ) ;
2019-10-04 23:02:37 -04:00
}
2021-10-07 08:42:08 -04:00
2019-10-04 23:22:34 -04:00
this . currentState = STATE_RUNNING ;
2020-06-12 10:03:08 -04:00
Healthz . isHealthy = true ;
2022-02-15 10:44:41 -05:00
await this . logMessage ( LogLevel . INFO , "Mjolnir@startup" , "Startup complete. Now monitoring rooms." ) ;
2021-10-07 08:42:08 -04:00
} catch ( err ) {
2020-06-12 10:03:08 -04:00
try {
LogService . error ( "Mjolnir" , "Error during startup:" ) ;
2021-07-01 17:11:27 -04:00
LogService . error ( "Mjolnir" , extractRequestError ( err ) ) ;
2022-02-15 10:44:41 -05:00
await this . logMessage ( LogLevel . ERROR , "Mjolnir@startup" , "Startup failed due to error - see console" ) ;
2020-06-12 10:03:08 -04:00
} catch ( e ) {
// If we failed to handle the error, just crash
console . error ( e ) ;
process . exit ( 1 ) ;
}
2021-10-07 08:42:08 -04:00
}
2019-09-27 17:15:10 -04:00
}
2021-09-27 10:52:28 -04:00
/ * *
* Stop Mjolnir from syncing and processing commands .
* /
2021-09-22 12:59:11 -04:00
public stop() {
2021-09-30 10:32:20 -04:00
LogService . info ( "Mjolnir" , "Stopping Mjolnir..." ) ;
2021-09-22 12:59:11 -04:00
this . client . stop ( ) ;
2021-10-07 08:42:08 -04:00
this . webapis . stop ( ) ;
2021-09-22 12:59:11 -04:00
}
2022-02-15 10:44:41 -05:00
public async logMessage ( level : LogLevel , module : string , message : string | any , additionalRoomIds : string [ ] | string | null = null , isRecursive = false ) : Promise < any > {
if ( ! additionalRoomIds ) additionalRoomIds = [ ] ;
if ( ! Array . isArray ( additionalRoomIds ) ) additionalRoomIds = [ additionalRoomIds ] ;
if ( config . RUNTIME . client && ( config . verboseLogging || LogLevel . INFO . includes ( level ) ) ) {
let clientMessage = message ;
if ( level === LogLevel . WARN ) clientMessage = ` ⚠ | ${ message } ` ;
if ( level === LogLevel . ERROR ) clientMessage = ` ‼ | ${ message } ` ;
const client = config . RUNTIME . client ;
const managementRoomId = await client . resolveRoom ( config . managementRoom ) ;
const roomIds = [ managementRoomId , . . . additionalRoomIds ] ;
let evContent : TextualMessageEventContent = {
body : message ,
formatted_body : htmlEscape ( message ) ,
msgtype : "m.notice" ,
format : "org.matrix.custom.html" ,
} ;
if ( ! isRecursive ) {
evContent = await replaceRoomIdsWithPills ( this , clientMessage , new Set ( roomIds ) , "m.notice" ) ;
}
await client . sendMessage ( managementRoomId , evContent ) ;
}
levelToFn [ level . toString ( ) ] ( module , message ) ;
}
2020-01-21 15:43:36 -05:00
public async addProtectedRoom ( roomId : string ) {
2020-01-21 17:19:03 -05:00
this . protectedRooms [ roomId ] = Permalinks . forRoom ( roomId ) ;
2020-01-21 15:43:36 -05:00
2020-02-18 19:06:27 -05:00
const unprotectedIdx = this . knownUnprotectedRooms . indexOf ( roomId ) ;
if ( unprotectedIdx >= 0 ) this . knownUnprotectedRooms . splice ( unprotectedIdx , 1 ) ;
this . explicitlyProtectedRoomIds . push ( roomId ) ;
2022-02-02 07:43:05 -05:00
let additionalProtectedRooms : { rooms? : string [ ] } | null = null ;
2020-01-21 15:43:36 -05:00
try {
additionalProtectedRooms = await this . client . getAccountData ( PROTECTED_ROOMS_EVENT_TYPE ) ;
} catch ( e ) {
2021-07-01 17:11:27 -04:00
LogService . warn ( "Mjolnir" , extractRequestError ( e ) ) ;
2020-01-21 15:43:36 -05:00
}
2022-02-02 07:43:05 -05:00
const rooms = ( additionalProtectedRooms ? . rooms ? ? [ ] ) ;
rooms . push ( roomId ) ;
await this . client . setAccountData ( PROTECTED_ROOMS_EVENT_TYPE , { rooms : rooms } ) ;
2020-01-21 15:43:36 -05:00
await this . syncLists ( config . verboseLogging ) ;
}
public async removeProtectedRoom ( roomId : string ) {
delete this . protectedRooms [ roomId ] ;
2020-02-18 19:06:27 -05:00
const idx = this . explicitlyProtectedRoomIds . indexOf ( roomId ) ;
if ( idx >= 0 ) this . explicitlyProtectedRoomIds . splice ( idx , 1 ) ;
2022-02-02 07:43:05 -05:00
let additionalProtectedRooms : { rooms? : string [ ] } | null = null ;
2020-01-21 15:43:36 -05:00
try {
additionalProtectedRooms = await this . client . getAccountData ( PROTECTED_ROOMS_EVENT_TYPE ) ;
} catch ( e ) {
2021-07-01 17:11:27 -04:00
LogService . warn ( "Mjolnir" , extractRequestError ( e ) ) ;
2020-01-21 15:43:36 -05:00
}
2022-02-02 07:43:05 -05:00
additionalProtectedRooms = { rooms : additionalProtectedRooms?.rooms?.filter ( r = > r !== roomId ) ? ? [ ] } ;
2020-01-21 15:43:36 -05:00
await this . client . setAccountData ( PROTECTED_ROOMS_EVENT_TYPE , additionalProtectedRooms ) ;
}
2020-01-21 17:19:03 -05:00
private async resyncJoinedRooms ( withSync = true ) {
if ( ! config . protectAllJoinedRooms ) return ;
2022-01-17 11:24:12 -05:00
const joinedRoomIds = ( await this . client . getJoinedRooms ( ) ) . filter ( r = > r !== this . managementRoomId ) ;
2020-01-21 17:19:03 -05:00
for ( const roomId of this . protectedJoinedRoomIds ) {
delete this . protectedRooms [ roomId ] ;
}
this . protectedJoinedRoomIds = joinedRoomIds ;
for ( const roomId of joinedRoomIds ) {
this . protectedRooms [ roomId ] = Permalinks . forRoom ( roomId ) ;
}
2020-02-18 19:06:27 -05:00
this . applyUnprotectedRooms ( ) ;
2020-01-21 17:19:03 -05:00
if ( withSync ) {
await this . syncLists ( config . verboseLogging ) ;
}
}
2022-02-02 10:31:11 -05:00
/ *
* Take all the builtin protections , register them to set their enabled ( or not ) state and
* update their settings with any saved non - default values
* /
private async registerProtections() {
for ( const protection of PROTECTIONS ) {
2019-12-04 20:46:29 -05:00
try {
2022-02-02 10:31:11 -05:00
await this . registerProtection ( protection ) ;
2019-12-04 20:46:29 -05:00
} catch ( e ) {
2021-07-01 17:11:27 -04:00
LogService . warn ( "Mjolnir" , extractRequestError ( e ) ) ;
2019-12-04 20:46:29 -05:00
}
}
}
2022-02-02 10:31:11 -05:00
/ *
* Make a list of the names of enabled protections and save them in a state event
* /
private async saveEnabledProtections() {
const protections = this . enabledProtections . map ( p = > p . name ) ;
await this . client . setAccountData ( ENABLED_PROTECTIONS_EVENT_TYPE , { enabled : protections } ) ;
}
/ *
* Enable a protection by name and persist its enable state in to a state event
*
* @param name The name of the protection whose settings we ' re enabling
* /
public async enableProtection ( name : string ) {
const protection = this . protections . get ( name ) ;
if ( protection !== undefined ) {
protection . enabled = true ;
await this . saveEnabledProtections ( ) ;
}
}
/ *
* Disable a protection by name and remove it from the persistent list of enabled protections
*
* @param name The name of the protection whose settings we ' re disabling
* /
public async disableProtection ( name : string ) {
const protection = this . protections . get ( name ) ;
if ( protection !== undefined ) {
protection . enabled = false ;
await this . saveEnabledProtections ( ) ;
}
}
2022-01-25 09:47:50 -05:00
/ *
* Read org . matrix . mjolnir . setting state event , find any saved settings for
* the requested protectionName , then iterate and validate against their parser
2022-02-24 07:43:31 -05:00
* counterparts in Protection . settings and return those which validate
2022-01-25 09:47:50 -05:00
*
* @param protectionName The name of the protection whose settings we ' re reading
* @returns Every saved setting for this protectionName that has a valid value
* /
public async getProtectionSettings ( protectionName : string ) : Promise < { [ setting : string ] : any } > {
let savedSettings : { [ setting : string ] : any } = { }
try {
savedSettings = await this . client . getRoomStateEvent (
this . managementRoomId , 'org.matrix.mjolnir.setting' , protectionName
) ;
} catch {
// setting does not exist, return empty object
2022-02-02 10:31:11 -05:00
return { } ;
2022-01-25 09:47:50 -05:00
}
2022-02-02 10:31:11 -05:00
const settingDefinitions = this . protections . get ( protectionName ) ? . settings ? ? { } ;
2022-01-25 09:47:50 -05:00
const validatedSettings : { [ setting : string ] : any } = { }
for ( let [ key , value ] of Object . entries ( savedSettings ) ) {
if (
// is this a setting name with a known parser?
key in settingDefinitions
// is the datatype of this setting's value what we expect?
&& typeof ( settingDefinitions [ key ] . value ) === typeof ( value )
// is this setting's value valid for the setting?
&& settingDefinitions [ key ] . validate ( value )
) {
validatedSettings [ key ] = value ;
} else {
2022-02-15 10:44:41 -05:00
await this . logMessage (
2022-01-25 09:47:50 -05:00
LogLevel . WARN ,
"getProtectionSetting" ,
` Tried to read ${ protectionName } . ${ key } and got invalid value ${ value } `
) ;
}
}
return validatedSettings ;
}
/ *
* Takes an object of settings we want to change and what their values should be ,
* check that their values are valid , combine them with current saved settings ,
* then save the amalgamation to a state event
*
* @param protectionName Which protection these settings belong to
* @param changedSettings The settings to change and their values
* /
public async setProtectionSettings ( protectionName : string , changedSettings : { [ setting : string ] : any } ) : Promise < any > {
2022-02-02 10:31:11 -05:00
const protection = this . protections . get ( protectionName ) ;
if ( protection === undefined ) {
return ;
}
2022-01-25 09:47:50 -05:00
const validatedSettings : { [ setting : string ] : any } = await this . getProtectionSettings ( protectionName ) ;
for ( let [ key , value ] of Object . entries ( changedSettings ) ) {
2022-02-02 10:31:11 -05:00
if ( ! ( key in protection . settings ) ) {
2022-01-25 09:47:50 -05:00
throw new ProtectionSettingValidationError ( ` Failed to find protection setting by name: ${ key } ` ) ;
}
2022-02-02 10:31:11 -05:00
if ( typeof ( protection . settings [ key ] . value ) !== typeof ( value ) ) {
2022-01-25 09:47:50 -05:00
throw new ProtectionSettingValidationError ( ` Invalid type for protection setting: ${ key } ( ${ typeof ( value ) } ) ` ) ;
}
2022-02-02 10:31:11 -05:00
if ( ! protection . settings [ key ] . validate ( value ) ) {
2022-01-25 09:47:50 -05:00
throw new ProtectionSettingValidationError ( ` Invalid value for protection setting: ${ key } ( ${ value } ) ` ) ;
}
validatedSettings [ key ] = value ;
}
await this . client . sendStateEvent (
this . managementRoomId , 'org.matrix.mjolnir.setting' , protectionName , validatedSettings
) ;
}
2022-02-02 10:31:11 -05:00
/ *
* Given a protection object ; add it to our list of protections , set whether it is enabled
* and update its settings with any saved non - default values .
*
* @param protection The protection object we want to register
* /
2022-02-24 07:43:31 -05:00
public async registerProtection ( protection : Protection ) {
2022-02-02 10:31:11 -05:00
this . protections . set ( protection . name , protection )
2019-12-04 20:46:29 -05:00
2022-02-02 10:31:11 -05:00
let enabledProtections : { enabled : string [ ] } | null = null ;
try {
enabledProtections = await this . client . getAccountData ( ENABLED_PROTECTIONS_EVENT_TYPE ) ;
} catch {
// this setting either doesn't exist, or we failed to read it (bad network?)
// TODO: retry on certain failures?
}
protection . enabled = enabledProtections ? . enabled . includes ( protection . name ) ? ? false ;
2019-12-04 20:46:29 -05:00
2022-02-02 10:31:11 -05:00
const savedSettings = await this . getProtectionSettings ( protection . name ) ;
2022-01-25 09:47:50 -05:00
for ( let [ key , value ] of Object . entries ( savedSettings ) ) {
// this.getProtectionSettings() validates this data for us, so we don't need to
protection . settings [ key ] . setValue ( value ) ;
}
2019-12-04 20:46:29 -05:00
}
2022-02-02 10:31:11 -05:00
/ *
* Given a protection object ; remove it from our list of protections .
*
* @param protection The protection object we want to unregister
* /
public unregisterProtection ( protectionName : string ) {
if ( ! ( protectionName in this . protections ) ) {
throw new Error ( "Failed to find protection by name: " + protectionName ) ;
}
this . protections . delete ( protectionName ) ;
2019-12-04 20:46:29 -05:00
}
2022-02-15 08:51:20 -05:00
/ * *
* Helper for constructing ` BanList ` s and making sure they have the right listeners set up .
* @param roomId The room id for the ` BanList ` .
* @param roomRef A reference ( matrix . to URL ) for the ` BanList ` .
* /
private async addBanList ( roomId : string , roomRef : string ) : Promise < BanList > {
const list = new BanList ( roomId , roomRef , this . client ) ;
this . ruleServer ? . watch ( list ) ;
list . on ( 'BanList.batch' , this . syncWithBanList . bind ( this ) ) ;
await list . updateList ( ) ;
this . banLists . push ( list ) ;
return list ;
}
2022-02-24 07:43:31 -05:00
public getProtection ( protectionName : string ) : Protection | null {
return this . protections . get ( protectionName ) ? ? null ;
}
2021-10-07 08:42:08 -04:00
public async watchList ( roomRef : string ) : Promise < BanList | null > {
2019-10-08 13:25:57 -04:00
const joinedRooms = await this . client . getJoinedRooms ( ) ;
const permalink = Permalinks . parseUrl ( roomRef ) ;
2019-10-08 15:58:31 -04:00
if ( ! permalink . roomIdOrAlias ) return null ;
2019-10-08 13:25:57 -04:00
const roomId = await this . client . resolveRoom ( permalink . roomIdOrAlias ) ;
if ( ! joinedRooms . includes ( roomId ) ) {
await this . client . joinRoom ( permalink . roomIdOrAlias , permalink . viaServers ) ;
}
2019-10-08 15:58:31 -04:00
if ( this . banLists . find ( b = > b . roomId === roomId ) ) return null ;
2019-10-08 13:25:57 -04:00
2022-02-15 08:51:20 -05:00
const list = await this . addBanList ( roomId , roomRef ) ;
2019-10-08 15:58:31 -04:00
await this . client . setAccountData ( WATCHED_LISTS_EVENT_TYPE , {
references : this.banLists.map ( b = > b . roomRef ) ,
} ) ;
2020-02-18 19:06:27 -05:00
await this . warnAboutUnprotectedBanListRoom ( roomId ) ;
2019-10-08 15:58:31 -04:00
return list ;
2019-10-08 13:25:57 -04:00
}
2021-10-07 08:42:08 -04:00
public async unwatchList ( roomRef : string ) : Promise < BanList | null > {
2019-10-08 13:25:57 -04:00
const permalink = Permalinks . parseUrl ( roomRef ) ;
2019-10-08 15:58:31 -04:00
if ( ! permalink . roomIdOrAlias ) return null ;
2019-10-08 13:25:57 -04:00
const roomId = await this . client . resolveRoom ( permalink . roomIdOrAlias ) ;
2021-07-22 02:38:44 -04:00
const list = this . banLists . find ( b = > b . roomId === roomId ) || null ;
2021-10-22 04:47:05 -04:00
if ( list ) {
this . banLists . splice ( this . banLists . indexOf ( list ) , 1 ) ;
this . ruleServer ? . unwatch ( list ) ;
}
2019-10-08 15:58:31 -04:00
await this . client . setAccountData ( WATCHED_LISTS_EVENT_TYPE , {
references : this.banLists.map ( b = > b . roomRef ) ,
} ) ;
return list ;
2019-10-08 13:25:57 -04:00
}
2020-02-18 19:06:27 -05:00
public async warnAboutUnprotectedBanListRoom ( roomId : string ) {
if ( ! config . protectAllJoinedRooms ) return ; // doesn't matter
if ( this . explicitlyProtectedRoomIds . includes ( roomId ) ) return ; // explicitly protected
const createEvent = new CreateEvent ( await this . client . getRoomStateEvent ( roomId , "m.room.create" , "" ) ) ;
if ( createEvent . creator === await this . client . getUserId ( ) ) return ; // we created it
if ( ! this . knownUnprotectedRooms . includes ( roomId ) ) this . knownUnprotectedRooms . push ( roomId ) ;
this . applyUnprotectedRooms ( ) ;
try {
2022-02-02 07:43:05 -05:00
const accountData : { warned : boolean } | null = await this . client . getAccountData ( WARN_UNPROTECTED_ROOM_EVENT_PREFIX + roomId ) ;
if ( accountData && accountData . warned ) return ; // already warned
2020-02-18 19:06:27 -05:00
} catch ( e ) {
// Ignore - probably haven't warned about it yet
}
2022-02-15 10:44:41 -05:00
await this . logMessage ( LogLevel . WARN , "Mjolnir" , ` Not protecting ${ roomId } - it is a ban list that this bot did not create. Add the room as protected if it is supposed to be protected. This warning will not appear again. ` , roomId ) ;
2021-10-07 08:42:08 -04:00
await this . client . setAccountData ( WARN_UNPROTECTED_ROOM_EVENT_PREFIX + roomId , { warned : true } ) ;
2020-02-18 19:06:27 -05:00
}
private applyUnprotectedRooms() {
for ( const roomId of this . knownUnprotectedRooms ) {
delete this . protectedRooms [ roomId ] ;
}
}
2022-02-15 08:51:20 -05:00
private async buildWatchedBanLists() {
this . banLists = [ ] ;
2019-10-08 13:25:57 -04:00
const joinedRooms = await this . client . getJoinedRooms ( ) ;
2019-10-08 15:58:31 -04:00
2022-02-02 07:43:05 -05:00
let watchedListsEvent : { references? : string [ ] } | null = null ;
2019-10-08 15:58:31 -04:00
try {
watchedListsEvent = await this . client . getAccountData ( WATCHED_LISTS_EVENT_TYPE ) ;
} catch ( e ) {
// ignore - not important
}
2022-02-02 07:43:05 -05:00
for ( const roomRef of ( watchedListsEvent ? . references || [ ] ) ) {
2019-10-08 13:25:57 -04:00
const permalink = Permalinks . parseUrl ( roomRef ) ;
if ( ! permalink . roomIdOrAlias ) continue ;
const roomId = await this . client . resolveRoom ( permalink . roomIdOrAlias ) ;
if ( ! joinedRooms . includes ( roomId ) ) {
await this . client . joinRoom ( permalink . roomIdOrAlias , permalink . viaServers ) ;
}
2020-02-18 19:06:27 -05:00
await this . warnAboutUnprotectedBanListRoom ( roomId ) ;
2022-02-15 08:51:20 -05:00
await this . addBanList ( roomId , roomRef ) ;
2019-10-08 13:25:57 -04:00
}
}
2019-11-06 21:17:11 -05:00
public async verifyPermissions ( verbose = true , printRegardless = false ) {
2019-10-04 23:22:34 -04:00
const errors : RoomUpdateError [ ] = [ ] ;
for ( const roomId of Object . keys ( this . protectedRooms ) ) {
2019-10-04 23:36:19 -04:00
errors . push ( . . . ( await this . verifyPermissionsIn ( roomId ) ) ) ;
2019-10-04 23:22:34 -04:00
}
2019-11-06 21:17:11 -05:00
const hadErrors = await this . printActionResult ( errors , "Permission errors in protected rooms:" , printRegardless ) ;
2019-10-04 23:38:50 -04:00
if ( ! hadErrors && verbose ) {
2019-10-04 23:22:34 -04:00
const html = ` <font color="#00cc00">All permissions look OK.</font> ` ;
const text = "All permissions look OK." ;
2022-01-17 11:24:12 -05:00
await this . client . sendMessage ( this . managementRoomId , {
2019-10-04 23:22:34 -04:00
msgtype : "m.notice" ,
body : text ,
format : "org.matrix.custom.html" ,
formatted_body : html ,
} ) ;
}
}
2019-10-04 23:36:19 -04:00
private async verifyPermissionsIn ( roomId : string ) : Promise < RoomUpdateError [ ] > {
const errors : RoomUpdateError [ ] = [ ] ;
try {
const ownUserId = await this . client . getUserId ( ) ;
const powerLevels = await this . client . getRoomStateEvent ( roomId , "m.room.power_levels" , "" ) ;
if ( ! powerLevels ) {
// noinspection ExceptionCaughtLocallyJS
throw new Error ( "Missing power levels state event" ) ;
}
function plDefault ( val : number | undefined | null , def : number ) : number {
if ( ! val && val !== 0 ) return def ;
return val ;
}
const users = powerLevels [ 'users' ] || { } ;
const events = powerLevels [ 'events' ] || { } ;
const usersDefault = plDefault ( powerLevels [ 'users_default' ] , 0 ) ;
const stateDefault = plDefault ( powerLevels [ 'state_default' ] , 50 ) ;
const ban = plDefault ( powerLevels [ 'ban' ] , 50 ) ;
const kick = plDefault ( powerLevels [ 'kick' ] , 50 ) ;
const redact = plDefault ( powerLevels [ 'redact' ] , 50 ) ;
const userLevel = plDefault ( users [ ownUserId ] , usersDefault ) ;
const aclLevel = plDefault ( events [ "m.room.server_acl" ] , stateDefault ) ;
// Wants: ban, kick, redact, m.room.server_acl
if ( userLevel < ban ) {
2019-11-06 21:17:11 -05:00
errors . push ( {
roomId ,
errorMessage : ` Missing power level for bans: ${ userLevel } < ${ ban } ` ,
errorKind : ERROR_KIND_PERMISSION ,
} ) ;
2019-10-04 23:36:19 -04:00
}
if ( userLevel < kick ) {
2019-11-06 21:17:11 -05:00
errors . push ( {
roomId ,
errorMessage : ` Missing power level for kicks: ${ userLevel } < ${ kick } ` ,
errorKind : ERROR_KIND_PERMISSION ,
} ) ;
2019-10-04 23:36:19 -04:00
}
if ( userLevel < redact ) {
2019-11-06 21:17:11 -05:00
errors . push ( {
roomId ,
errorMessage : ` Missing power level for redactions: ${ userLevel } < ${ redact } ` ,
errorKind : ERROR_KIND_PERMISSION ,
} ) ;
2019-10-04 23:36:19 -04:00
}
if ( userLevel < aclLevel ) {
2019-11-06 21:17:11 -05:00
errors . push ( {
roomId ,
errorMessage : ` Missing power level for server ACLs: ${ userLevel } < ${ aclLevel } ` ,
errorKind : ERROR_KIND_PERMISSION ,
} ) ;
2019-10-04 23:36:19 -04:00
}
// Otherwise OK
} catch ( e ) {
2021-07-01 17:11:27 -04:00
LogService . error ( "Mjolnir" , extractRequestError ( e ) ) ;
2019-11-06 21:17:11 -05:00
errors . push ( {
roomId ,
errorMessage : e.message || ( e . body ? e . body . error : '<no message>' ) ,
errorKind : ERROR_KIND_FATAL ,
} ) ;
2019-10-04 23:36:19 -04:00
}
return errors ;
}
2021-09-15 06:06:03 -04:00
/ * *
2021-09-16 06:46:44 -04:00
* Sync all the rooms with all the watched lists , banning and applying any changed ACLS .
2021-09-15 06:06:03 -04:00
* @param verbose Whether to report any errors to the management room .
* /
2019-10-09 06:29:01 -04:00
public async syncLists ( verbose = true ) {
2019-09-27 22:02:03 -04:00
for ( const list of this . banLists ) {
2021-11-12 13:35:44 -05:00
const changes = await list . updateList ( ) ;
await this . printBanlistChanges ( changes , list , true ) ;
2019-09-27 22:02:03 -04:00
}
2019-10-04 22:59:30 -04:00
let hadErrors = false ;
const aclErrors = await applyServerAcls ( this . banLists , Object . keys ( this . protectedRooms ) , this ) ;
const banErrors = await applyUserBans ( this . banLists , Object . keys ( this . protectedRooms ) , this ) ;
2021-09-14 07:17:29 -04:00
const redactionErrors = await this . processRedactionQueue ( ) ;
2019-10-04 22:59:30 -04:00
hadErrors = hadErrors || await this . printActionResult ( aclErrors , "Errors updating server ACLs:" ) ;
hadErrors = hadErrors || await this . printActionResult ( banErrors , "Errors updating member bans:" ) ;
2021-09-14 07:17:29 -04:00
hadErrors = hadErrors || await this . printActionResult ( redactionErrors , "Error updating redactions:" ) ;
2019-10-04 22:59:30 -04:00
2019-10-04 23:38:50 -04:00
if ( ! hadErrors && verbose ) {
2019-10-04 22:59:30 -04:00
const html = ` <font color="#00cc00">Done updating rooms - no errors</font> ` ;
2019-10-04 23:38:50 -04:00
const text = "Done updating rooms - no errors" ;
2022-01-17 11:24:12 -05:00
await this . client . sendMessage ( this . managementRoomId , {
2019-10-04 22:59:30 -04:00
msgtype : "m.notice" ,
body : text ,
format : "org.matrix.custom.html" ,
formatted_body : html ,
} ) ;
}
2019-09-27 22:02:03 -04:00
}
2021-11-12 13:35:44 -05:00
/ * *
* Pulls any changes to the rules that are in a policy room and updates all protected rooms
* with those changes . Does not fail if there are errors updating the room , these are reported to the management room .
2022-02-15 08:51:20 -05:00
* @param banList The ` BanList ` which we will check for changes and apply them to all protected rooms .
2021-11-12 13:35:44 -05:00
* @returns When all of the protected rooms have been updated .
* /
2022-02-15 08:51:20 -05:00
private async syncWithBanList ( banList : BanList ) : Promise < void > {
2021-11-12 13:35:44 -05:00
const changes = await banList . updateList ( ) ;
2019-09-27 22:02:03 -04:00
2019-10-04 22:59:30 -04:00
let hadErrors = false ;
const aclErrors = await applyServerAcls ( this . banLists , Object . keys ( this . protectedRooms ) , this ) ;
const banErrors = await applyUserBans ( this . banLists , Object . keys ( this . protectedRooms ) , this ) ;
2021-09-14 07:17:29 -04:00
const redactionErrors = await this . processRedactionQueue ( ) ;
2019-10-04 22:59:30 -04:00
hadErrors = hadErrors || await this . printActionResult ( aclErrors , "Errors updating server ACLs:" ) ;
hadErrors = hadErrors || await this . printActionResult ( banErrors , "Errors updating member bans:" ) ;
2021-09-14 07:17:29 -04:00
hadErrors = hadErrors || await this . printActionResult ( redactionErrors , "Error updating redactions:" ) ;
2019-10-04 22:59:30 -04:00
if ( ! hadErrors ) {
const html = ` <font color="#00cc00"><b>Done updating rooms - no errors</b></font> ` ;
const text = "Done updating rooms - no errors" ;
2022-01-17 11:24:12 -05:00
await this . client . sendMessage ( this . managementRoomId , {
2019-10-04 22:59:30 -04:00
msgtype : "m.notice" ,
body : text ,
format : "org.matrix.custom.html" ,
formatted_body : html ,
} ) ;
}
2022-02-15 08:51:20 -05:00
// This can fail if the change is very large and it is much less important than applying bans, so do it last.
await this . printBanlistChanges ( changes , banList , true ) ;
2019-09-27 22:02:03 -04:00
}
2019-09-27 17:15:10 -04:00
private async handleEvent ( roomId : string , event : any ) {
2019-11-14 17:59:01 -05:00
// Check for UISI errors
2022-01-17 11:24:12 -05:00
if ( roomId === this . managementRoomId ) {
2019-11-14 17:59:01 -05:00
if ( event [ 'type' ] === 'm.room.message' && event [ 'content' ] && event [ 'content' ] [ 'body' ] ) {
if ( event [ 'content' ] [ 'body' ] === "** Unable to decrypt: The sender's device has not sent us the keys for this message. **" ) {
// UISI
await this . client . unstableApis . addReactionToEvent ( roomId , event [ 'event_id' ] , '⚠' ) ;
await this . client . unstableApis . addReactionToEvent ( roomId , event [ 'event_id' ] , 'UISI' ) ;
await this . client . unstableApis . addReactionToEvent ( roomId , event [ 'event_id' ] , '🚨' ) ;
}
}
}
2019-12-04 21:25:46 -05:00
// Check for updated ban lists before checking protected rooms - the ban lists might be protected
// themselves.
2022-02-15 08:51:20 -05:00
const banList = this . banLists . find ( list = > list . roomId === roomId ) ;
if ( banList !== undefined ) {
if ( ALL_BAN_LIST_RULE_TYPES . includes ( event [ 'type' ] ) || event [ 'type' ] === 'm.room.redaction' ) {
banList . updateForEvent ( event )
2019-12-04 21:25:46 -05:00
}
}
2022-02-24 07:43:31 -05:00
if ( roomId in this . protectedRooms ) {
2019-10-18 11:38:19 -04:00
if ( event [ 'sender' ] === await this . client . getUserId ( ) ) return ; // Ignore ourselves
2019-12-04 20:46:29 -05:00
2022-02-02 10:31:11 -05:00
// Iterate all the enabled protections
for ( const protection of this . enabledProtections ) {
2019-12-04 20:46:29 -05:00
try {
await protection . handleEvent ( this , roomId , event ) ;
} catch ( e ) {
2019-12-09 21:15:51 -05:00
const eventPermalink = Permalinks . forEvent ( roomId , event [ 'event_id' ] ) ;
2019-12-04 20:46:29 -05:00
LogService . error ( "Mjolnir" , "Error handling protection: " + protection . name ) ;
2019-12-09 21:15:51 -05:00
LogService . error ( "Mjolnir" , "Failed event: " + eventPermalink ) ;
2021-07-01 17:11:27 -04:00
LogService . error ( "Mjolnir" , extractRequestError ( e ) ) ;
2022-01-17 11:24:12 -05:00
await this . client . sendNotice ( this . managementRoomId , "There was an error processing an event through a protection - see log for details. Event: " + eventPermalink ) ;
2019-12-04 20:46:29 -05:00
}
}
2019-12-09 21:15:51 -05:00
// Run the event handlers - we always run this after protections so that the protections
// can flag the event for redaction.
2022-02-15 10:44:41 -05:00
await this . unlistedUserRedactionHandler . handleEvent ( roomId , event , this ) ;
2019-12-09 21:15:51 -05:00
2019-10-18 11:38:19 -04:00
if ( event [ 'type' ] === 'm.room.power_levels' && event [ 'state_key' ] === '' ) {
// power levels were updated - recheck permissions
2019-11-06 21:17:11 -05:00
ErrorCache . resetError ( roomId , ERROR_KIND_PERMISSION ) ;
2022-02-15 10:44:41 -05:00
await this . logMessage ( LogLevel . DEBUG , "Mjolnir" , ` Power levels changed in ${ roomId } - checking permissions... ` , roomId ) ;
2019-10-18 11:38:19 -04:00
const errors = await this . verifyPermissionsIn ( roomId ) ;
const hadErrors = await this . printActionResult ( errors ) ;
if ( ! hadErrors ) {
2022-02-15 10:44:41 -05:00
await this . logMessage ( LogLevel . DEBUG , "Mjolnir" , ` All permissions look OK. ` ) ;
2019-10-18 11:38:19 -04:00
}
return ;
} else if ( event [ 'type' ] === "m.room.member" ) {
2021-09-16 06:46:44 -04:00
// The reason we have to apply bans on each member change is because
// we cannot eagerly ban users (that is to ban them when they have never been a member)
// as they can be force joined to a room they might not have known existed.
// Only apply bans and then redactions in the room we are currently looking at.
2021-09-14 07:34:46 -04:00
const banErrors = await applyUserBans ( this . banLists , [ roomId ] , this ) ;
const redactionErrors = await this . processRedactionQueue ( roomId ) ;
await this . printActionResult ( banErrors ) ;
await this . printActionResult ( redactionErrors ) ;
2019-10-04 23:36:19 -04:00
}
}
2019-09-27 17:15:10 -04:00
}
2021-11-12 13:35:44 -05:00
/ * *
* Print the changes to a banlist to the management room .
* @param changes A list of changes that have been made to a particular ban list .
* @param ignoreSelf Whether to exclude changes that have been made by Mjolnir .
* @returns true if the message was sent , false if it wasn ' t ( because there there were no changes to report ) .
* /
private async printBanlistChanges ( changes : ListRuleChange [ ] , list : BanList , ignoreSelf = false ) : Promise < boolean > {
if ( ignoreSelf ) {
const sender = await this . client . getUserId ( ) ;
changes = changes . filter ( change = > change . sender !== sender ) ;
}
if ( changes . length <= 0 ) return false ;
let html = "" ;
let text = "" ;
const changesInfo = ` updated with ${ changes . length } ` + ( changes . length === 1 ? 'change:' : 'changes:' ) ;
const shortcodeInfo = list . listShortcode ? ` (shortcode: ${ htmlEscape ( list . listShortcode ) } ) ` : '' ;
html += ` <a href=" ${ htmlEscape ( list . roomRef ) } "> ${ htmlEscape ( list . roomId ) } </a> ${ shortcodeInfo } ${ changesInfo } <br/><ul> ` ;
text += ` ${ list . roomRef } ${ shortcodeInfo } ${ changesInfo } : \ n ` ;
for ( const change of changes ) {
const rule = change . rule ;
let ruleKind : string = rule . kind ;
if ( ruleKind === RULE_USER ) {
ruleKind = 'user' ;
} else if ( ruleKind === RULE_SERVER ) {
ruleKind = 'server' ;
} else if ( ruleKind === RULE_ROOM ) {
ruleKind = 'room' ;
}
2022-02-02 07:43:05 -05:00
html += ` <li> ${ change . changeType } ${ htmlEscape ( ruleKind ) } (<code> ${ htmlEscape ( rule . recommendation ? ? "" ) } </code>): <code> ${ htmlEscape ( rule . entity ) } </code> ( ${ htmlEscape ( rule . reason ) } )</li> ` ;
2021-11-12 13:35:44 -05:00
text += ` * ${ change . changeType } ${ ruleKind } ( ${ rule . recommendation } ): ${ rule . entity } ( ${ rule . reason } ) \ n ` ;
}
const message = {
msgtype : "m.notice" ,
body : text ,
format : "org.matrix.custom.html" ,
formatted_body : html ,
} ;
2022-01-17 11:24:12 -05:00
await this . client . sendMessage ( this . managementRoomId , message ) ;
2021-11-12 13:35:44 -05:00
return true ;
}
2021-10-07 08:42:08 -04:00
private async printActionResult ( errors : RoomUpdateError [ ] , title : string | null = null , logAnyways = false ) {
2019-10-04 22:59:30 -04:00
if ( errors . length <= 0 ) return false ;
2019-09-27 21:54:13 -04:00
2019-11-06 21:17:11 -05:00
if ( ! logAnyways ) {
errors = errors . filter ( e = > ErrorCache . triggerError ( e . roomId , e . errorKind ) ) ;
if ( errors . length <= 0 ) {
LogService . warn ( "Mjolnir" , "Multiple errors are happening, however they are muted. Please check the management room." ) ;
return true ;
}
}
2019-09-27 17:15:10 -04:00
let html = "" ;
let text = "" ;
2019-10-04 22:59:30 -04:00
const htmlTitle = title ? ` ${ title } <br /> ` : '' ;
const textTitle = title ? ` ${ title } \ n ` : '' ;
html += ` <font color="#ff0000"><b> ${ htmlTitle } ${ errors . length } errors updating protected rooms!</b></font><br /><ul> ` ;
text += ` ${ textTitle } ${ errors . length } errors updating protected rooms! \ n ` ;
2020-04-14 21:39:33 -04:00
const viaServers = [ ( new UserID ( await this . client . getUserId ( ) ) ) . domain ] ;
2019-09-27 21:54:13 -04:00
for ( const error of errors ) {
2020-05-11 23:35:37 -04:00
const alias = ( await this . client . getPublishedAlias ( error . roomId ) ) || error . roomId ;
2020-04-14 21:39:33 -04:00
const url = Permalinks . forRoom ( alias , viaServers ) ;
html += ` <li><a href=" ${ url } "> ${ alias } </a> - ${ error . errorMessage } </li> ` ;
2019-09-27 21:54:13 -04:00
text += ` ${ url } - ${ error . errorMessage } \ n ` ;
2019-09-27 17:15:10 -04:00
}
2019-09-27 21:54:13 -04:00
html += "</ul>" ;
2019-09-27 17:15:10 -04:00
const message = {
msgtype : "m.notice" ,
body : text ,
format : "org.matrix.custom.html" ,
formatted_body : html ,
} ;
2022-01-17 11:24:12 -05:00
await this . client . sendMessage ( this . managementRoomId , message ) ;
2019-10-04 22:59:30 -04:00
return true ;
2019-09-27 17:15:10 -04:00
}
2019-11-14 17:44:13 -05:00
public async isSynapseAdmin ( ) : Promise < boolean > {
try {
const endpoint = ` /_synapse/admin/v1/users/ ${ await this . client . getUserId ( ) } /admin ` ;
const response = await this . client . doRequest ( "GET" , endpoint ) ;
return response [ 'admin' ] ;
} catch ( e ) {
LogService . error ( "Mjolnir" , "Error determining if Mjolnir is a server admin:" ) ;
2021-07-01 17:11:27 -04:00
LogService . error ( "Mjolnir" , extractRequestError ( e ) ) ;
2019-11-14 17:44:13 -05:00
return false ; // assume not
}
}
public async deactivateSynapseUser ( userId : string ) : Promise < any > {
const endpoint = ` /_synapse/admin/v1/deactivate/ ${ userId } ` ;
return await this . client . doRequest ( "POST" , endpoint ) ;
}
2020-02-13 15:56:03 -05:00
2021-07-28 04:56:50 -04:00
public async shutdownSynapseRoom ( roomId : string , message? : string ) : Promise < any > {
const endpoint = ` /_synapse/admin/v1/rooms/ ${ roomId } ` ;
return await this . client . doRequest ( "DELETE" , endpoint , null , {
2020-02-13 15:56:03 -05:00
new_room_user_id : await this . client . getUserId ( ) ,
2021-07-28 04:56:50 -04:00
block : true ,
2021-08-17 11:20:31 -04:00
message : message /* If `undefined`, we'll use Synapse's default message. */
2020-02-13 15:56:03 -05:00
} ) ;
}
2021-09-14 07:17:29 -04:00
2022-03-07 04:14:06 -05:00
/ * *
* Make a user administrator via the Synapse Admin API
* @param roomId the room where the user ( or the bot ) shall be made administrator .
* @param userId optionally specify the user mxID to be made administrator , if not specified the bot mxID will be used .
* @returns The list of errors encountered , for reporting to the management room .
* /
public async makeUserRoomAdmin ( roomId : string , userId? : string ) : Promise < any > {
try {
const endpoint = ` /_synapse/admin/v1/rooms/ ${ roomId } /make_room_admin ` ;
return await this . client . doRequest ( "POST" , endpoint , null , {
user_id : userId || await this . client . getUserId ( ) , /* if not specified make the bot administrator */
} ) ;
} catch ( e ) {
return extractRequestError ( e ) ;
}
}
2021-09-14 07:17:29 -04:00
public queueRedactUserMessagesIn ( userId : string , roomId : string ) {
this . eventRedactionQueue . add ( new RedactUserInRoom ( userId , roomId ) ) ;
}
2021-09-15 06:06:03 -04:00
/ * *
* Process all queued redactions , this is usually called at the end of the sync process ,
* after all users have been banned and ACLs applied .
2021-09-16 06:46:44 -04:00
* If a redaction cannot be processed , the redaction is skipped and removed from the queue .
* We then carry on processing the next redactions .
2021-09-15 06:06:03 -04:00
* @param roomId Limit processing to one room only , otherwise process redactions for all rooms .
2021-09-16 06:46:44 -04:00
* @returns The list of errors encountered , for reporting to the management room .
2021-09-15 06:06:03 -04:00
* /
public async processRedactionQueue ( roomId? : string ) : Promise < RoomUpdateError [ ] > {
2022-02-15 10:44:41 -05:00
return await this . eventRedactionQueue . process ( this , roomId ) ;
2021-09-14 07:17:29 -04:00
}
2022-02-08 08:07:42 -05:00
private async handleReport ( roomId : string , reporterId : string , event : any , reason? : string ) {
for ( const protection of this . enabledProtections ) {
await protection . handleReport ( this , roomId , reporterId , event , reason ) ;
}
}
2019-09-27 17:15:10 -04:00
}