2019-09-27 20:26:57 +00:00
/ *
2021-07-01 21:11:27 +00:00
Copyright 2019 - 2021 The Matrix . org Foundation C . I . C .
2019-09-27 20:26:57 +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 .
* /
2020-04-15 00:46:39 +00:00
import {
2021-07-01 21:11:27 +00:00
extractRequestError ,
2020-04-15 00:46:39 +00:00
LogLevel ,
LogService ,
MatrixClient ,
MatrixGlob ,
MessageType ,
Permalinks ,
TextualMessageEventContent ,
UserID
} from "matrix-bot-sdk" ;
2019-12-10 02:56:12 +00:00
import { logMessage } from "./LogProxy" ;
import config from "./config" ;
2020-04-15 00:46:39 +00:00
import * as htmlEscape from "escape-html" ;
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 ) {
2020-04-15 00:46:39 +00:00
await logMessage ( LogLevel . DEBUG , "utils#redactUserMessagesIn" , ` Fetching sent messages for ${ userIdOrGlob } in ${ targetRoomId } to redact... ` , targetRoomId ) ;
2019-12-10 02:56:12 +00:00
2020-06-12 14:15:48 +00:00
await getMessagesByUserIn ( client , userIdOrGlob , targetRoomId , limit , async ( eventsToRedact ) = > {
for ( const victimEvent of eventsToRedact ) {
await logMessage ( LogLevel . DEBUG , "utils#redactUserMessagesIn" , ` Redacting ${ victimEvent [ 'event_id' ] } in ${ targetRoomId } ` , 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 ` , targetRoomId ) ;
}
2019-12-10 02:56:12 +00:00
}
2020-06-12 14:15:48 +00:00
} ) ;
2019-12-10 02:56:12 +00:00
}
}
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 .
2020-06-12 14:15:48 +00:00
* @param { function } cb Callback function to handle the events as they are received .
* @returns { Promise < any > } Resolves when complete .
2019-12-10 02:43:41 +00:00
* /
2020-06-12 14:15:48 +00:00
export async function getMessagesByUserIn ( client : MatrixClient , sender : string , roomId : string , limit : number , cb : ( events : any [ ] ) = > void ) : 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 ;
}
}
2021-09-16 17:10:46 +00:00
/ * *
* Note : ` rooms/initialSync ` is deprecated . However , there is no replacement for this API for the time being .
* While previous versions of this function used ` /sync ` , experience shows that it can grow extremely
* slow ( 4 - 5 minutes long ) when we need to sync many large rooms , which leads to timeouts and
* breakage in Mjolnir , see https : //github.com/matrix-org/synapse/issues/10842.
* /
function roomInitialSync() {
return client . doRequest ( "GET" , ` /_matrix/client/r0/rooms/ ${ encodeURIComponent ( roomId ) } /initialSync ` ) ;
2019-10-09 14:53:37 +00:00
}
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
2021-09-16 17:10:46 +00:00
const response = await roomInitialSync ( ) ;
2019-10-09 14:53:37 +00:00
if ( ! response ) return [ ] ;
2019-12-10 02:43:41 +00:00
let processed = 0 ;
2019-10-09 14:53:37 +00:00
2021-09-16 17:10:46 +00:00
const timeline = ( response [ 'messages' ] || { } )
const syncedMessages = timeline [ 'chunk' ] || [ ] ;
// The start of the chunk has the oldest events.
let token = timeline [ 'start' ] ;
2019-12-10 02:43:41 +00:00
let bfMessages = { chunk : syncedMessages , end : token } ;
do {
2021-07-22 06:38:44 +00:00
const messages : any [ ] = [ ] ;
2019-12-10 02:43:41 +00:00
for ( const event of ( bfMessages [ 'chunk' ] || [ ] ) ) {
2020-06-12 14:15:48 +00:00
if ( processed >= limit ) return ; // we're done even if we don't want to be
2019-12-10 02:43:41 +00:00
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" ) ;
2020-06-12 14:15:48 +00:00
cb ( messages ) ;
return ;
2019-10-09 14:53:37 +00:00
}
}
2020-06-12 14:15:48 +00:00
cb ( messages ) ;
} while ( token ) ;
2019-10-09 14:53:37 +00:00
}
2020-04-15 00:46:39 +00:00
export async function replaceRoomIdsWithPills ( client : MatrixClient , text : string , roomIds : string [ ] | string , msgtype : MessageType = "m.text" ) : Promise < TextualMessageEventContent > {
if ( ! Array . isArray ( roomIds ) ) roomIds = [ roomIds ] ;
const content : TextualMessageEventContent = {
body : text ,
formatted_body : htmlEscape ( text ) ,
msgtype : msgtype ,
format : "org.matrix.custom.html" ,
} ;
const escapeRegex = ( v : string ) : string = > {
return v . replace ( /[-\/\\^$*+?.()|[\]{}]/g , '\\$&' ) ;
} ;
const viaServers = [ ( new UserID ( await client . getUserId ( ) ) ) . domain ] ;
for ( const roomId of roomIds ) {
2020-05-12 03:30:22 +00:00
let alias = roomId ;
try {
2020-05-12 03:31:47 +00:00
alias = ( await client . getPublishedAlias ( roomId ) ) || roomId ;
2020-05-12 03:30:22 +00:00
} catch ( e ) {
// This is a recursive call, so tell the function not to try and call us
await logMessage ( LogLevel . WARN , "utils" , ` Failed to resolve room alias for ${ roomId } - see console for details ` , null , true ) ;
2021-07-01 21:11:27 +00:00
LogService . warn ( "utils" , extractRequestError ( e ) ) ;
2020-05-12 03:30:22 +00:00
}
2020-04-15 00:46:39 +00:00
const regexRoomId = new RegExp ( escapeRegex ( roomId ) , "g" ) ;
content . body = content . body . replace ( regexRoomId , alias ) ;
2021-07-22 06:38:44 +00:00
if ( content . formatted_body ) {
content . formatted_body = content . formatted_body . replace ( regexRoomId , ` <a href=" ${ Permalinks . forRoom ( alias , viaServers ) } "> ${ alias } </a> ` ) ;
}
2020-04-15 00:46:39 +00:00
}
return content ;
}