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 ,
Permalinks ,
UserID
} from "matrix-bot-sdk" ;
2019-09-27 17:15:10 -04:00
import BanList , { ALL_RULE_TYPES } from "./models/BanList" ;
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 20:46:49 -05:00
import { logMessage } from "./LogProxy" ;
2019-11-06 21:17:11 -05:00
import ErrorCache , { ERROR_KIND_FATAL , ERROR_KIND_PERMISSION } from "./ErrorCache" ;
2019-12-04 20:46:29 -05:00
import { IProtection } from "./protections/IProtection" ;
import { PROTECTIONS } from "./protections/protections" ;
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" ;
2019-09-27 17:15:10 -04:00
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 ;
2019-12-04 20:46:29 -05:00
private protections : IProtection [ ] = [ ] ;
2021-09-14 09:36:53 -04:00
private unlistedUserRedactionQueue = new UnlistedUserRedactionQueue ( ) ;
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 [ ] = [ ] ;
2019-09-27 18:04:08 -04:00
2019-09-27 17:15:10 -04:00
constructor (
public readonly client : MatrixClient ,
public readonly protectedRooms : { [ roomId : string ] : string } ,
2019-10-08 13:25:57 -04:00
private banLists : BanList [ ] ,
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 ( ) ) ) ;
}
2019-09-27 17:15:10 -04:00
client . on ( "room.event" , this . handleEvent . bind ( this ) ) ;
client . on ( "room.message" , async ( roomId , event ) = > {
2019-11-06 20:46:49 -05:00
if ( roomId !== config . managementRoom ) 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
} ) ;
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 ;
}
2019-12-04 20:46:29 -05:00
public get enabledProtections ( ) : IProtection [ ] {
return this . protections ;
}
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 ;
}
2019-09-27 17:15:10 -04:00
public start() {
2019-10-04 23:22:34 -04:00
return this . client . start ( ) . then ( async ( ) = > {
this . currentState = STATE_CHECKING_PERMISSIONS ;
2020-02-18 19:06:27 -05:00
2020-01-21 15:43:36 -05:00
await 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 {
2021-07-22 02:38:44 -04:00
const data : Object | 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 ) {
await logMessage ( LogLevel . INFO , "Mjolnir@startup" , "Checking permissions..." ) ;
await this . verifyPermissions ( config . verboseLogging ) ;
}
} ) . then ( async ( ) = > {
this . currentState = STATE_SYNCING ;
2019-10-04 23:02:37 -04:00
if ( config . syncOnStartup ) {
2019-11-06 20:46:49 -05:00
await logMessage ( LogLevel . INFO , "Mjolnir@startup" , "Syncing lists..." ) ;
2019-10-04 23:38:50 -04:00
await this . syncLists ( config . verboseLogging ) ;
2019-12-04 20:46:29 -05:00
await this . enableProtections ( ) ;
2019-10-04 23:02:37 -04:00
}
2019-10-04 23:22:34 -04:00
} ) . then ( async ( ) = > {
this . currentState = STATE_RUNNING ;
2020-06-12 10:03:08 -04:00
Healthz . isHealthy = true ;
2019-11-06 20:46:49 -05:00
await logMessage ( LogLevel . INFO , "Mjolnir@startup" , "Startup complete. Now monitoring rooms." ) ;
2020-06-12 10:03:08 -04:00
} ) . catch ( async err = > {
try {
LogService . error ( "Mjolnir" , "Error during startup:" ) ;
2021-07-01 17:11:27 -04:00
LogService . error ( "Mjolnir" , extractRequestError ( err ) ) ;
2020-06-12 10:03:08 -04:00
await logMessage ( LogLevel . ERROR , "Mjolnir@startup" , "Startup failed due to error - see console" ) ;
} catch ( e ) {
// If we failed to handle the error, just crash
console . error ( e ) ;
process . exit ( 1 ) ;
}
2019-10-04 23:02:37 -04:00
} ) ;
2019-09-27 17:15:10 -04:00
}
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 ) ;
2020-01-21 15:43:36 -05:00
let additionalProtectedRooms ;
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
}
if ( ! additionalProtectedRooms || ! additionalProtectedRooms [ 'rooms' ] ) additionalProtectedRooms = { rooms : [ ] } ;
additionalProtectedRooms . rooms . push ( roomId ) ;
await this . client . setAccountData ( PROTECTED_ROOMS_EVENT_TYPE , additionalProtectedRooms ) ;
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 ) ;
2020-01-21 15:43:36 -05:00
let additionalProtectedRooms ;
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
}
if ( ! additionalProtectedRooms || ! additionalProtectedRooms [ 'rooms' ] ) additionalProtectedRooms = { rooms : [ ] } ;
additionalProtectedRooms . rooms = additionalProtectedRooms . rooms . filter ( r = > r !== roomId ) ;
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 ;
const joinedRoomIds = ( await this . client . getJoinedRooms ( ) ) . filter ( r = > r !== config . managementRoom ) ;
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 ) ;
}
}
2019-12-04 20:46:29 -05:00
private async getEnabledProtections() {
let enabled : string [ ] = [ ] ;
try {
2021-07-22 02:38:44 -04:00
const protections : Object | null = await this . client . getAccountData ( ENABLED_PROTECTIONS_EVENT_TYPE ) ;
2019-12-04 20:46:29 -05:00
if ( protections && protections [ 'enabled' ] ) {
for ( const protection of protections [ 'enabled' ] ) {
enabled . push ( protection ) ;
}
}
} catch ( e ) {
2021-07-01 17:11:27 -04:00
LogService . warn ( "Mjolnir" , extractRequestError ( e ) ) ;
2019-12-04 20:46:29 -05:00
}
return enabled ;
}
private async enableProtections() {
for ( const protection of await this . getEnabledProtections ( ) ) {
try {
this . enableProtection ( protection , false ) ;
} catch ( e ) {
2021-07-01 17:11:27 -04:00
LogService . warn ( "Mjolnir" , extractRequestError ( e ) ) ;
2019-12-04 20:46:29 -05:00
}
}
}
public async enableProtection ( protectionName : string , persist = true ) : Promise < any > {
const definition = PROTECTIONS [ protectionName ] ;
if ( ! definition ) throw new Error ( "Failed to find protection by name: " + protectionName ) ;
const protection = definition . factory ( ) ;
this . protections . push ( protection ) ;
if ( persist ) {
const existing = this . protections . map ( p = > p . name ) ;
await this . client . setAccountData ( ENABLED_PROTECTIONS_EVENT_TYPE , { enabled : existing } ) ;
}
}
public async disableProtection ( protectionName : string ) : Promise < any > {
const idx = this . protections . findIndex ( p = > p . name === protectionName ) ;
if ( idx >= 0 ) this . protections . splice ( idx , 1 ) ;
const existing = this . protections . map ( p = > p . name ) ;
await this . client . setAccountData ( ENABLED_PROTECTIONS_EVENT_TYPE , { enabled : existing } ) ;
}
2021-07-22 02:38:44 -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
const list = new BanList ( roomId , roomRef , this . client ) ;
await list . updateList ( ) ;
this . banLists . push ( list ) ;
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-07-22 02:38:44 -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 ;
2019-10-08 13:25:57 -04:00
if ( list ) this . banLists . splice ( this . banLists . indexOf ( list ) , 1 ) ;
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 {
2021-07-22 02:38:44 -04:00
const accountData : Object | null = await this . client . getAccountData ( WARN_UNPROTECTED_ROOM_EVENT_PREFIX + roomId ) ;
2020-02-18 19:06:27 -05:00
if ( accountData && accountData [ 'warned' ] ) return ; // already warned
} catch ( e ) {
// Ignore - probably haven't warned about it yet
}
2020-04-14 20:46:39 -04:00
await 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 ) ;
2020-02-18 19:06:27 -05:00
await this . client . setAccountData ( WARN_UNPROTECTED_ROOM_EVENT_PREFIX + roomId , { warned : true } ) ;
}
private applyUnprotectedRooms() {
for ( const roomId of this . knownUnprotectedRooms ) {
delete this . protectedRooms [ roomId ] ;
}
}
2019-10-08 13:25:57 -04:00
public async buildWatchedBanLists() {
const banLists : BanList [ ] = [ ] ;
const joinedRooms = await this . client . getJoinedRooms ( ) ;
2019-10-08 15:58:31 -04:00
let watchedListsEvent = { } ;
try {
watchedListsEvent = await this . client . getAccountData ( WATCHED_LISTS_EVENT_TYPE ) ;
} catch ( e ) {
// ignore - not important
}
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 ) ;
2019-10-08 13:25:57 -04:00
const list = new BanList ( roomId , roomRef , this . client ) ;
await list . updateList ( ) ;
banLists . push ( list ) ;
}
this . banLists = banLists ;
}
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." ;
2019-11-06 20:46:49 -05:00
await this . client . sendMessage ( config . managementRoom , {
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 ;
}
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 ) {
await list . updateList ( ) ;
}
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" ;
2019-11-06 20:46:49 -05:00
await this . client . sendMessage ( config . managementRoom , {
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
}
public async syncListForRoom ( roomId : string ) {
let updated = false ;
for ( const list of this . banLists ) {
if ( list . roomId !== roomId ) continue ;
await list . updateList ( ) ;
updated = true ;
}
if ( ! updated ) return ;
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" ;
2019-11-06 20:46:49 -05:00
await this . client . sendMessage ( config . managementRoom , {
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
}
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
if ( roomId === config . managementRoom ) {
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.
if ( this . banLists . map ( b = > b . roomId ) . includes ( roomId ) ) {
if ( ALL_RULE_TYPES . includes ( event [ 'type' ] ) ) {
await this . syncListForRoom ( roomId ) ;
}
}
2019-10-18 11:38:19 -04:00
if ( Object . keys ( this . protectedRooms ) . includes ( roomId ) ) {
if ( event [ 'sender' ] === await this . client . getUserId ( ) ) return ; // Ignore ourselves
2019-12-04 20:46:29 -05:00
// Iterate all the protections
for ( const protection of this . protections ) {
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 ) ) ;
2019-12-09 21:15:51 -05:00
await this . client . sendNotice ( config . managementRoom , "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.
2021-09-14 09:36:53 -04:00
await this . unlistedUserRedactionHandler . handleEvent ( roomId , event , this . client ) ;
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 ) ;
2020-04-14 20:46:39 -04:00
await 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 ) {
2020-02-12 17:42:10 -05:00
await logMessage ( LogLevel . DEBUG , "Mjolnir" , ` All permissions look OK. ` ) ;
2019-10-18 11:38:19 -04:00
}
return ;
} else if ( event [ 'type' ] === "m.room.member" ) {
2019-11-07 13:00:41 -05:00
// Only apply bans in the room we're 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-07-22 02:38:44 -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 ,
} ;
2019-11-06 20:46:49 -05:00
await this . client . sendMessage ( config . managementRoom , 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
public queueRedactUserMessagesIn ( userId : string , roomId : string ) {
this . eventRedactionQueue . add ( new RedactUserInRoom ( userId , roomId ) ) ;
}
2021-09-14 07:34:46 -04:00
public async processRedactionQueue ( roomId? : string ) {
return await this . eventRedactionQueue . process ( this . client , roomId ) ;
2021-09-14 07:17:29 -04:00
}
2019-09-27 17:15:10 -04:00
}