2021-10-07 08:42:08 -04:00
import { strict as assert } from "assert" ;
2022-01-17 11:24:12 -05:00
import { matrixClient } from "./mjolnirSetupUtils" ;
2021-10-07 08:42:08 -04:00
import { newTestUser } from "./clientHelper" ;
2021-11-09 07:15:49 -05:00
import { ReportManager , ABUSE_ACTION_CONFIRMATION_KEY , ABUSE_REPORT_KEY } from "../../src/report/ReportManager" ;
2021-10-07 08:42:08 -04:00
/ * *
* Test the ability to turn abuse reports into room messages .
* /
2021-11-09 07:15:49 -05:00
const REPORT_NOTICE_REGEXPS = {
reporter : /Filed by (?<reporterDisplay>[^ ]*) \((?<reporterId>[^ ]*)\)/ ,
accused : /Against (?<accusedDisplay>[^ ]*) \((?<accusedId>[^ ]*)\)/ ,
room : /Room (?<roomAliasOrId>[^ ]*)/ ,
event : /Event (?<eventId>[^ ]*) Go to event/ ,
content : /Content (?<eventContent>.*)/ ,
comments : /Comments Comments (?<comments>.*)/
} ;
2021-10-07 08:42:08 -04:00
describe ( "Test: Reporting abuse" , async ( ) = > {
2022-10-18 09:48:39 -04:00
// Testing with successive versions of the API.
//
// As of this writing, v3 is the standard, while r0 is deprecated. However,
// both versions are still in use in the wild.
// Note that this version change only affects the actual URL at which reports
// are sent.
for ( let endpoint of [ 'v3' , 'r0' ] ) {
it ( ` Mjölnir intercepts abuse reports with endpoint ${ endpoint } ` , async function ( ) {
this . timeout ( 90000 ) ;
// Listen for any notices that show up.
let notices : any [ ] = [ ] ;
this . mjolnir . client . on ( "room.event" , ( roomId , event ) = > {
if ( roomId = this . mjolnir . managementRoomId ) {
notices . push ( event ) ;
}
} ) ;
2021-10-07 08:42:08 -04:00
2022-10-18 09:48:39 -04:00
// Create a few users and a room.
let goodUser = await newTestUser ( this . config . homeserverUrl , { name : { contains : "reporting-abuse-good-user" } } ) ;
let badUser = await newTestUser ( this . config . homeserverUrl , { name : { contains : "reporting-abuse-bad-user" } } ) ;
let goodUserId = await goodUser . getUserId ( ) ;
let badUserId = await badUser . getUserId ( ) ;
let roomId = await goodUser . createRoom ( { invite : [ await badUser . getUserId ( ) ] } ) ;
await goodUser . inviteUser ( await badUser . getUserId ( ) , roomId ) ;
await badUser . joinRoom ( roomId ) ;
console . log ( "Test: Reporting abuse - send messages" ) ;
// Exchange a few messages.
let goodText = ` GOOD: ${ Math . random ( ) } ` ; // Will NOT be reported.
let badText = ` BAD: ${ Math . random ( ) } ` ; // Will be reported as abuse.
let badText2 = ` BAD: ${ Math . random ( ) } ` ; // Will be reported as abuse.
let badText3 = ` <b>BAD</b>: ${ Math . random ( ) } ` ; // Will be reported as abuse.
let badText4 = [ . . . Array ( 1024 ) ] . map ( _ = > ` ${ Math . random ( ) } ` ) . join ( "" ) ; // Text is too long.
let badText5 = [ . . . Array ( 1024 ) ] . map ( _ = > "ABC" ) . join ( "\n" ) ; // Text has too many lines.
let badEventId = await badUser . sendText ( roomId , badText ) ;
let badEventId2 = await badUser . sendText ( roomId , badText2 ) ;
let badEventId3 = await badUser . sendText ( roomId , badText3 ) ;
let badEventId4 = await badUser . sendText ( roomId , badText4 ) ;
let badEventId5 = await badUser . sendText ( roomId , badText5 ) ;
let badEvent2Comment = ` COMMENT: ${ Math . random ( ) } ` ;
console . log ( "Test: Reporting abuse - send reports" ) ;
let reportsToFind : any [ ] = [ ]
// Time to report, first without a comment, then with one.
try {
await goodUser . doRequest ( "POST" , ` /_matrix/client/ ${ endpoint } /rooms/ ${ encodeURIComponent ( roomId ) } /report/ ${ encodeURIComponent ( badEventId ) } ` ) ;
reportsToFind . push ( {
reporterId : goodUserId ,
accusedId : badUserId ,
eventId : badEventId ,
text : badText ,
comment : null ,
} ) ;
} catch ( e ) {
console . error ( "Could not send first report" , e . body || e ) ;
throw e ;
2021-10-07 08:42:08 -04:00
}
2022-10-18 09:48:39 -04:00
try {
await goodUser . doRequest ( "POST" , ` /_matrix/client/ ${ endpoint } /rooms/ ${ encodeURIComponent ( roomId ) } /report/ ${ encodeURIComponent ( badEventId2 ) } ` , "" , {
reason : badEvent2Comment
} ) ;
reportsToFind . push ( {
reporterId : goodUserId ,
accusedId : badUserId ,
eventId : badEventId2 ,
text : badText2 ,
comment : badEvent2Comment ,
} ) ;
} catch ( e ) {
console . error ( "Could not send second report" , e . body || e ) ;
throw e ;
}
2021-11-09 07:15:49 -05:00
2022-10-18 09:48:39 -04:00
try {
await goodUser . doRequest ( "POST" , ` /_matrix/client/ ${ endpoint } /rooms/ ${ encodeURIComponent ( roomId ) } /report/ ${ encodeURIComponent ( badEventId3 ) } ` , "" ) ;
reportsToFind . push ( {
reporterId : goodUserId ,
accusedId : badUserId ,
eventId : badEventId3 ,
text : badText3 ,
comment : null ,
} ) ;
} catch ( e ) {
console . error ( "Could not send third report" , e . body || e ) ;
throw e ;
}
2021-11-09 07:15:49 -05:00
2022-10-18 09:48:39 -04:00
try {
await goodUser . doRequest ( "POST" , ` /_matrix/client/ ${ endpoint } /rooms/ ${ encodeURIComponent ( roomId ) } /report/ ${ encodeURIComponent ( badEventId4 ) } ` , "" ) ;
reportsToFind . push ( {
reporterId : goodUserId ,
accusedId : badUserId ,
eventId : badEventId4 ,
text : null ,
textPrefix : badText4.substring ( 0 , 256 ) ,
comment : null ,
} ) ;
} catch ( e ) {
console . error ( "Could not send fourth report" , e . body || e ) ;
throw e ;
}
2021-11-09 07:15:49 -05:00
2022-10-18 09:48:39 -04:00
try {
await goodUser . doRequest ( "POST" , ` /_matrix/client/ ${ endpoint } /rooms/ ${ encodeURIComponent ( roomId ) } /report/ ${ encodeURIComponent ( badEventId5 ) } ` , "" ) ;
reportsToFind . push ( {
reporterId : goodUserId ,
accusedId : badUserId ,
eventId : badEventId5 ,
text : null ,
textPrefix : badText5.substring ( 0 , 256 ) . split ( "\n" ) . join ( " " ) ,
comment : null ,
} ) ;
} catch ( e ) {
console . error ( "Could not send fifth report" , e . body || e ) ;
throw e ;
}
2021-10-07 08:42:08 -04:00
2022-10-18 09:48:39 -04:00
console . log ( "Test: Reporting abuse - wait" ) ;
await new Promise ( resolve = > setTimeout ( resolve , 1000 ) ) ;
let found : any [ ] = [ ] ;
for ( let toFind of reportsToFind ) {
for ( let event of notices ) {
if ( "content" in event && "body" in event . content ) {
if ( ! ( ABUSE_REPORT_KEY in event . content ) || event . content [ ABUSE_REPORT_KEY ] . event_id != toFind . eventId ) {
// Not a report or not our report.
continue ;
}
let report = event . content [ ABUSE_REPORT_KEY ] ;
let body = event . content . body as string ;
let matches : Map < string , RegExpMatchArray > | null = new Map ( ) ;
for ( let key of Object . keys ( REPORT_NOTICE_REGEXPS ) ) {
let match = body . match ( REPORT_NOTICE_REGEXPS [ key ] ) ;
if ( match ) {
console . debug ( "We have a match" , key , REPORT_NOTICE_REGEXPS [ key ] , match . groups ) ;
} else {
console . debug ( "Not a match" , key , REPORT_NOTICE_REGEXPS [ key ] ) ;
// Not a report, skipping.
matches = null ;
break ;
}
matches . set ( key , match ) ;
}
if ( ! matches ) {
2021-11-09 07:15:49 -05:00
// Not a report, skipping.
2022-10-18 09:48:39 -04:00
continue ;
2021-11-09 07:15:49 -05:00
}
2022-10-18 09:48:39 -04:00
assert ( body . length < 3000 , ` The report shouldn't be too long ${ body . length } ` ) ;
assert ( body . split ( "\n" ) . length < 200 , "The report shouldn't have too many newlines." ) ;
2021-11-09 07:15:49 -05:00
2022-10-18 09:48:39 -04:00
assert . equal ( matches . get ( "event" ) ! . groups ! . eventId , toFind . eventId , "The report should specify the correct event id" ) ; ;
2021-11-09 07:15:49 -05:00
2022-10-18 09:48:39 -04:00
assert . equal ( matches . get ( "reporter" ) ! . groups ! . reporterId , toFind . reporterId , "The report should specify the correct reporter" ) ;
assert . equal ( report . reporter_id , toFind . reporterId , "The embedded report should specify the correct reporter" ) ;
assert . ok ( toFind . reporterId . includes ( matches . get ( "reporter" ) ! . groups ! . reporterDisplay ) , "The report should display the correct reporter" ) ;
2021-11-09 07:15:49 -05:00
2022-10-18 09:48:39 -04:00
assert . equal ( matches . get ( "accused" ) ! . groups ! . accusedId , toFind . accusedId , "The report should specify the correct accused" ) ;
assert . equal ( report . accused_id , toFind . accusedId , "The embedded report should specify the correct accused" ) ;
assert . ok ( toFind . accusedId . includes ( matches . get ( "accused" ) ! . groups ! . accusedDisplay ) , "The report should display the correct reporter" ) ;
2021-11-09 07:15:49 -05:00
2022-10-18 09:48:39 -04:00
if ( toFind . text ) {
assert . equal ( matches . get ( "content" ) ! . groups ! . eventContent , toFind . text , "The report should contain the text we inserted in the event" ) ;
}
if ( toFind . textPrefix ) {
assert . ok ( matches . get ( "content" ) ! . groups ! . eventContent . startsWith ( toFind . textPrefix ) , ` The report should contain a prefix of the long text we inserted in the event: ${ toFind . textPrefix } in? ${ matches . get ( "content" ) ! . groups ! . eventContent } ` ) ;
}
if ( toFind . comment ) {
assert . equal ( matches . get ( "comments" ) ! . groups ! . comments , toFind . comment , "The report should contain the comment we added" ) ;
}
assert . equal ( matches . get ( "room" ) ! . groups ! . roomAliasOrId , roomId , "The report should specify the correct room" ) ;
assert . equal ( report . room_id , roomId , "The embedded report should specify the correct room" ) ;
found . push ( toFind ) ;
break ;
2021-10-07 08:42:08 -04:00
}
}
}
2022-10-18 09:48:39 -04:00
assert . deepEqual ( found , reportsToFind ) ;
2021-11-09 07:15:49 -05:00
2022-10-18 09:48:39 -04:00
// Since Mjölnir is not a member of the room, the only buttons we should find
// are `help` and `ignore`.
for ( let event of notices ) {
if ( event . content && event . content [ "m.relates_to" ] && event . content [ "m.relates_to" ] [ "key" ] ) {
let regexp = /\/([[^]]*)\]/ ;
let matches = event . content [ "m.relates_to" ] [ "key" ] . match ( regexp ) ;
if ( ! matches ) {
2021-11-09 07:15:49 -05:00
continue ;
2022-10-18 09:48:39 -04:00
}
switch ( matches [ 1 ] ) {
case "bad-report" :
case "help" :
continue ;
default :
throw new Error ( ` Didn't expect label ${ matches [ 1 ] } ` ) ;
}
2021-11-09 07:15:49 -05:00
}
}
2022-10-18 09:48:39 -04:00
} ) ;
}
2021-11-09 07:15:49 -05:00
it ( 'The redact action works' , async function ( ) {
2022-01-06 06:19:19 -05:00
this . timeout ( 60000 ) ;
2021-11-09 07:15:49 -05:00
// Listen for any notices that show up.
2022-10-18 09:48:39 -04:00
let notices : any [ ] = [ ] ;
this . mjolnir . client . on ( "room.event" , ( roomId , event ) = > {
2022-01-17 11:24:12 -05:00
if ( roomId = this . mjolnir . managementRoomId ) {
2021-11-09 07:15:49 -05:00
notices . push ( event ) ;
}
} ) ;
// Create a moderator.
2022-08-16 10:51:18 -04:00
let moderatorUser = await newTestUser ( this . config . homeserverUrl , { name : { contains : "reporting-abuse-moderator-user" } } ) ;
2022-10-18 09:48:39 -04:00
this . mjolnir . client . inviteUser ( await moderatorUser . getUserId ( ) , this . mjolnir . managementRoomId ) ;
2022-01-17 11:24:12 -05:00
await moderatorUser . joinRoom ( this . mjolnir . managementRoomId ) ;
2021-11-09 07:15:49 -05:00
// Create a few users and a room.
2022-08-16 10:51:18 -04:00
let goodUser = await newTestUser ( this . config . homeserverUrl , { name : { contains : "reacting-abuse-good-user" } } ) ;
let badUser = await newTestUser ( this . config . homeserverUrl , { name : { contains : "reacting-abuse-bad-user" } } ) ;
2021-11-09 07:15:49 -05:00
let goodUserId = await goodUser . getUserId ( ) ;
let badUserId = await badUser . getUserId ( ) ;
let roomId = await moderatorUser . createRoom ( { invite : [ await badUser . getUserId ( ) ] } ) ;
await moderatorUser . inviteUser ( await goodUser . getUserId ( ) , roomId ) ;
await moderatorUser . inviteUser ( await badUser . getUserId ( ) , roomId ) ;
await badUser . joinRoom ( roomId ) ;
await goodUser . joinRoom ( roomId ) ;
// Setup Mjölnir as moderator for our room.
2022-10-18 09:48:39 -04:00
await moderatorUser . inviteUser ( await this . mjolnir . client . getUserId ( ) , roomId ) ;
await moderatorUser . setUserPowerLevel ( await this . mjolnir . client . getUserId ( ) , roomId , 100 ) ;
2021-11-09 07:15:49 -05:00
console . log ( "Test: Reporting abuse - send messages" ) ;
// Exchange a few messages.
let goodText = ` GOOD: ${ Math . random ( ) } ` ; // Will NOT be reported.
let badText = ` BAD: ${ Math . random ( ) } ` ; // Will be reported as abuse.
let goodEventId = await goodUser . sendText ( roomId , goodText ) ;
let badEventId = await badUser . sendText ( roomId , badText ) ;
let goodEventId2 = await goodUser . sendText ( roomId , goodText ) ;
console . log ( "Test: Reporting abuse - send reports" ) ;
// Time to report.
let reportToFind = {
reporterId : goodUserId ,
accusedId : badUserId ,
eventId : badEventId ,
text : badText ,
comment : null ,
} ;
try {
await goodUser . doRequest ( "POST" , ` /_matrix/client/r0/rooms/ ${ encodeURIComponent ( roomId ) } /report/ ${ encodeURIComponent ( badEventId ) } ` ) ;
} catch ( e ) {
console . error ( "Could not send first report" , e . body || e ) ;
throw e ;
}
console . log ( "Test: Reporting abuse - wait" ) ;
await new Promise ( resolve = > setTimeout ( resolve , 1000 ) ) ;
2022-10-18 09:48:39 -04:00
let mjolnirRooms = new Set ( await this . mjolnir . client . getJoinedRooms ( ) ) ;
2021-11-09 07:15:49 -05:00
assert . ok ( mjolnirRooms . has ( roomId ) , "Mjölnir should be a member of the room" ) ;
// Find the notice
let noticeId ;
for ( let event of notices ) {
if ( "content" in event && ABUSE_REPORT_KEY in event . content ) {
if ( ! ( ABUSE_REPORT_KEY in event . content ) || event . content [ ABUSE_REPORT_KEY ] . event_id != badEventId ) {
// Not a report or not our report.
continue ;
}
noticeId = event . event_id ;
break ;
}
}
assert . ok ( noticeId , "We should have found our notice" ) ;
// Find the buttons.
2022-10-18 09:48:39 -04:00
let buttons : any [ ] = [ ] ;
2021-11-09 07:15:49 -05:00
for ( let event of notices ) {
if ( event [ "type" ] != "m.reaction" ) {
continue ;
}
if ( event [ "content" ] [ "m.relates_to" ] [ "rel_type" ] != "m.annotation" ) {
continue ;
}
if ( event [ "content" ] [ "m.relates_to" ] [ "event_id" ] != noticeId ) {
continue ;
}
buttons . push ( event ) ;
}
// Find the redact button... and click it.
let redactButtonId = null ;
for ( let button of buttons ) {
if ( button [ "content" ] [ "m.relates_to" ] [ "key" ] . includes ( "[redact-message]" ) ) {
redactButtonId = button [ "event_id" ] ;
2022-01-17 11:24:12 -05:00
await moderatorUser . sendEvent ( this . mjolnir . managementRoomId , "m.reaction" , button [ "content" ] ) ;
2021-11-09 07:15:49 -05:00
break ;
}
}
assert . ok ( redactButtonId , "We should have found the redact button" ) ;
await new Promise ( resolve = > setTimeout ( resolve , 1000 ) ) ;
// This should have triggered a confirmation request, with more buttons!
let confirmEventId = null ;
for ( let event of notices ) {
console . debug ( "Is this the confirm button?" , event ) ;
if ( ! event [ "content" ] [ "m.relates_to" ] ) {
console . debug ( "Not a reaction" ) ;
continue ;
}
if ( ! event [ "content" ] [ "m.relates_to" ] [ "key" ] . includes ( "[confirm]" ) ) {
console . debug ( "Not confirm" ) ;
continue ;
}
if ( ! event [ "content" ] [ "m.relates_to" ] [ "event_id" ] == redactButtonId ) {
console . debug ( "Not reaction to redact button" ) ;
continue ;
}
// It's the confirm button, click it!
confirmEventId = event [ "event_id" ] ;
2022-01-17 11:24:12 -05:00
await moderatorUser . sendEvent ( this . mjolnir . managementRoomId , "m.reaction" , event [ "content" ] ) ;
2021-11-09 07:15:49 -05:00
break ;
}
assert . ok ( confirmEventId , "We should have found the confirm button" ) ;
await new Promise ( resolve = > setTimeout ( resolve , 1000 ) ) ;
// This should have redacted the message.
2022-10-18 09:48:39 -04:00
let newBadEvent = await this . mjolnir . client . getEvent ( roomId , badEventId ) ;
2021-11-09 07:15:49 -05:00
assert . deepEqual ( Object . keys ( newBadEvent . content ) , [ ] , "Redaction should have removed the content of the offending event" ) ;
} ) ;
2021-10-07 08:42:08 -04:00
} ) ;