2019-12-05 02:07:04 +00:00
/ *
2020-04-15 00:46:39 +00:00
Copyright 2019 , 2020 The Matrix . org Foundation C . I . C .
2019-12-05 02:07:04 +00: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 .
* /
2022-02-02 15:31:11 +00:00
import { Protection } from "./IProtection" ;
2022-01-25 14:47:50 +00:00
import { NumberProtectionSetting } from "./ProtectionSettings" ;
2019-12-05 02:07:04 +00:00
import { Mjolnir } from "../Mjolnir" ;
import { LogLevel , LogService } from "matrix-bot-sdk" ;
2019-12-10 02:20:47 +00:00
import config from "../config" ;
2019-12-05 02:07:04 +00:00
2022-01-25 14:47:50 +00:00
// if this is exceeded, we'll ban the user for spam and redact their messages
export const DEFAULT_MAX_PER_MINUTE = 10 ;
2019-12-05 02:07:04 +00:00
const TIMESTAMP_THRESHOLD = 30000 ; // 30s out of phase
2022-02-02 15:31:11 +00:00
export class BasicFlooding extends Protection {
2019-12-05 02:07:04 +00:00
2019-12-10 02:15:51 +00:00
private lastEvents : { [ roomId : string ] : { [ userId : string ] : { originServerTs : number , eventId : string } [ ] } } = { } ;
private recentlyBanned : string [ ] = [ ] ;
2019-12-05 02:07:04 +00:00
2022-02-02 12:43:05 +00:00
settings = {
maxPerMinute : new NumberProtectionSetting ( DEFAULT_MAX_PER_MINUTE )
} ;
2022-01-25 14:47:50 +00:00
2019-12-05 02:07:04 +00:00
public get name ( ) : string {
return 'BasicFloodingProtection' ;
}
2022-02-02 15:31:11 +00:00
public get description ( ) : string {
return "If a user posts more than " + DEFAULT_MAX_PER_MINUTE + " messages in 60s they'll be " +
"banned for spam. This does not publish the ban to any of your ban lists." ;
}
2019-12-05 02:07:04 +00:00
public async handleEvent ( mjolnir : Mjolnir , roomId : string , event : any ) : Promise < any > {
if ( ! this . lastEvents [ roomId ] ) this . lastEvents [ roomId ] = { } ;
const forRoom = this . lastEvents [ roomId ] ;
if ( ! forRoom [ event [ 'sender' ] ] ) forRoom [ event [ 'sender' ] ] = [ ] ;
let forUser = forRoom [ event [ 'sender' ] ] ;
if ( ( new Date ( ) ) . getTime ( ) - event [ 'origin_server_ts' ] > TIMESTAMP_THRESHOLD ) {
LogService . warn ( "BasicFlooding" , ` ${ event [ 'event_id' ] } is more than ${ TIMESTAMP_THRESHOLD } ms out of phase - rewriting event time to be 'now' ` ) ;
event [ 'origin_server_ts' ] = ( new Date ( ) ) . getTime ( ) ;
}
forUser . push ( { originServerTs : event [ 'origin_server_ts' ] , eventId : event [ 'event_id' ] } ) ;
// Do some math to see if the user is spamming
let messageCount = 0 ;
for ( const prevEvent of forUser ) {
if ( ( new Date ( ) ) . getTime ( ) - prevEvent . originServerTs > 60000 ) continue ; // not important
messageCount ++ ;
}
2022-02-02 12:43:05 +00:00
if ( messageCount >= this . settings . maxPerMinute . value ) {
2022-02-15 15:44:41 +00:00
await mjolnir . logMessage ( LogLevel . WARN , "BasicFlooding" , ` Banning ${ event [ 'sender' ] } in ${ roomId } for flooding ( ${ messageCount } messages in the last minute) ` , roomId ) ;
2020-06-12 14:15:48 +00:00
if ( ! config . noop ) {
await mjolnir . client . banUser ( event [ 'sender' ] , roomId , "spam" ) ;
} else {
2022-02-15 15:44:41 +00:00
await mjolnir . logMessage ( LogLevel . WARN , "BasicFlooding" , ` Tried to ban ${ event [ 'sender' ] } in ${ roomId } but Mjolnir is running in no-op mode ` , roomId ) ;
2020-06-12 14:15:48 +00:00
}
2019-12-10 02:15:51 +00:00
if ( this . recentlyBanned . includes ( event [ 'sender' ] ) ) return ; // already handled (will be redacted)
2021-09-14 13:36:53 +00:00
mjolnir . unlistedUserRedactionHandler . addUser ( event [ 'sender' ] ) ;
2019-12-10 02:15:51 +00:00
this . recentlyBanned . push ( event [ 'sender' ] ) ; // flag to reduce spam
2019-12-05 02:07:04 +00:00
// Redact all the things the user said too
2019-12-10 02:20:47 +00:00
if ( ! config . noop ) {
for ( const eventId of forUser . map ( e = > e . eventId ) ) {
await mjolnir . client . redactEvent ( roomId , eventId , "spam" ) ;
}
} else {
2022-02-15 15:44:41 +00:00
await mjolnir . logMessage ( LogLevel . WARN , "BasicFlooding" , ` Tried to redact messages for ${ event [ 'sender' ] } in ${ roomId } but Mjolnir is running in no-op mode ` , roomId ) ;
2019-12-05 02:07:04 +00:00
}
2019-12-10 02:15:51 +00:00
// Free up some memory now that we're ready to handle it elsewhere
2019-12-05 02:07:04 +00:00
forUser = forRoom [ event [ 'sender' ] ] = [ ] ; // reset the user's list
}
// Trim the oldest messages off the user's history if it's getting large
2022-02-02 12:43:05 +00:00
if ( forUser . length > this . settings . maxPerMinute . value * 2 ) {
forUser . splice ( 0 , forUser . length - ( this . settings . maxPerMinute . value * 2 ) - 1 ) ;
2019-12-05 02:07:04 +00:00
}
}
}