2019-09-27 20:26:57 +00:00
/ *
Copyright 2019 The Matrix . org Foundation C . I . C .
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 .
* /
2019-12-10 02:56:12 +00:00
import { LogLevel , LogService , MatrixClient , MatrixGlob } from "matrix-bot-sdk" ;
import { logMessage } from "./LogProxy" ;
import config from "./config" ;
2019-10-09 14:53:37 +00:00
2019-09-27 20:26:57 +00:00
export function setToArray < T > ( set : Set < T > ) : T [ ] {
const arr : T [ ] = [ ] ;
for ( const v of set ) {
arr . push ( v ) ;
}
return arr ;
}
2019-10-09 14:53:37 +00:00
2019-12-10 02:43:41 +00:00
export function isTrueJoinEvent ( event : any ) : boolean {
const membership = event [ 'content' ] [ 'membership' ] || 'join' ;
let prevMembership = "leave" ;
if ( event [ 'unsigned' ] && event [ 'unsigned' ] [ 'prev_content' ] ) {
prevMembership = event [ 'unsigned' ] [ 'prev_content' ] [ 'membership' ] || 'leave' ;
}
// We look at the previous membership to filter out profile changes
return membership === 'join' && prevMembership !== "join" ;
}
2020-04-14 22:44:31 +00:00
export async function redactUserMessagesIn ( client : MatrixClient , userIdOrGlob : string , targetRoomIds : string [ ] , limit = 1000 ) {
2019-12-10 02:56:12 +00:00
for ( const targetRoomId of targetRoomIds ) {
await logMessage ( LogLevel . DEBUG , "utils#redactUserMessagesIn" , ` Fetching sent messages for ${ userIdOrGlob } in ${ targetRoomId } to redact... ` ) ;
2020-04-14 22:44:31 +00:00
const eventsToRedact = await getMessagesByUserIn ( client , userIdOrGlob , targetRoomId , limit ) ;
2019-12-10 02:56:12 +00:00
for ( const victimEvent of eventsToRedact ) {
await logMessage ( LogLevel . DEBUG , "utils#redactUserMessagesIn" , ` Redacting ${ victimEvent [ 'event_id' ] } in ${ targetRoomId } ` ) ;
if ( ! config . noop ) {
await client . redactEvent ( targetRoomId , victimEvent [ 'event_id' ] ) ;
} else {
await logMessage ( LogLevel . WARN , "utils#redactUserMessagesIn" , ` Tried to redact ${ victimEvent [ 'event_id' ] } in ${ targetRoomId } but Mjolnir is running in no-op mode ` ) ;
}
}
}
}
2019-12-10 02:43:41 +00:00
/ * *
* Gets all the events sent by a user ( or users if using wildcards ) in a given room ID , since
* the time they joined .
* @param { MatrixClient } client The client to use .
* @param { string } sender The sender . Can include wildcards to match multiple people .
* @param { string } roomId The room ID to search in .
2020-04-14 22:44:31 +00:00
* @param { number } limit The maximum number of messages to search . Defaults to 1000 .
2019-12-10 02:43:41 +00:00
* @returns { Promise < any > } Resolves to the events sent by the user ( s ) prior to join .
* /
2020-04-14 22:44:31 +00:00
export async function getMessagesByUserIn ( client : MatrixClient , sender : string , roomId : string , limit : number ) : Promise < any [ ] > {
2019-10-09 14:53:37 +00:00
const filter = {
room : {
rooms : [ roomId ] ,
state : {
2020-04-14 22:44:31 +00:00
// types: ["m.room.member"], // We'll redact all types of events
2019-10-09 14:53:37 +00:00
rooms : [ roomId ] ,
} ,
timeline : {
rooms : [ roomId ] ,
2020-04-14 22:44:31 +00:00
// types: ["m.room.message"], // We'll redact all types of events
2019-10-09 14:53:37 +00:00
} ,
ephemeral : {
limit : 0 ,
types : [ ] ,
} ,
account_data : {
limit : 0 ,
types : [ ] ,
} ,
} ,
presence : {
limit : 0 ,
types : [ ] ,
} ,
account_data : {
limit : 0 ,
types : [ ] ,
} ,
} ;
2019-12-10 02:43:41 +00:00
let isGlob = true ;
if ( ! sender . includes ( "*" ) ) {
isGlob = false ;
filter . room . timeline [ 'senders' ] = [ sender ] ;
}
const matcher = new MatrixGlob ( sender ) ;
function testUser ( userId : string ) : boolean {
if ( isGlob ) {
return matcher . test ( userId ) ;
} else {
return userId === sender ;
}
}
2019-10-09 14:53:37 +00:00
function initialSync() {
const qs = {
filter : JSON.stringify ( filter ) ,
} ;
return client . doRequest ( "GET" , "/_matrix/client/r0/sync" , qs ) ;
}
function backfill ( from : string ) {
const qs = {
filter : JSON.stringify ( filter ) ,
from : from ,
dir : "b" ,
} ;
2019-12-10 02:43:41 +00:00
LogService . info ( "utils" , "Backfilling with token: " + token ) ;
2019-10-09 14:53:37 +00:00
return client . doRequest ( "GET" , ` /_matrix/client/r0/rooms/ ${ encodeURIComponent ( roomId ) } /messages ` , qs ) ;
}
// Do an initial sync first to get the batch token
const response = await initialSync ( ) ;
if ( ! response ) return [ ] ;
const messages = [ ] ;
2019-12-10 02:43:41 +00:00
let processed = 0 ;
2019-10-09 14:53:37 +00:00
const timeline = ( ( ( response [ 'rooms' ] || { } ) [ 'join' ] || { } ) [ roomId ] || { } ) [ 'timeline' ] || { } ;
const syncedMessages = timeline [ 'events' ] || [ ] ;
2019-12-10 02:43:41 +00:00
let token = timeline [ 'prev_batch' ] || response [ 'next_batch' ] ;
let bfMessages = { chunk : syncedMessages , end : token } ;
do {
for ( const event of ( bfMessages [ 'chunk' ] || [ ] ) ) {
if ( processed >= limit ) return messages ; // we're done even if we don't want to be
processed ++ ;
if ( testUser ( event [ 'sender' ] ) ) messages . push ( event ) ;
2019-10-09 14:53:37 +00:00
}
2019-12-10 02:43:41 +00:00
if ( token ) {
bfMessages = await backfill ( token ) ;
let lastToken = token ;
token = bfMessages [ 'end' ] ;
if ( lastToken === token ) {
LogService . warn ( "utils" , "Backfill returned same end token - returning" ) ;
return messages ;
2019-10-09 14:53:37 +00:00
}
}
2019-12-10 02:43:41 +00:00
} while ( token ) ;
2019-10-09 14:53:37 +00:00
return messages ;
}