import { IRemoteRoom , IMessageEvent , ISendingUser , IFileEvent , Util , Log , IPresenceEvent } from "mx-puppet-bridge" ; import { App , IDiscordSendFile , MAXFILESIZE , AVATAR_SETTINGS } from "../app" ; import * as Discord from "better-discord.js" ; import { TextEncoder , TextDecoder } from "util" ; import { TextGuildChannel } from "../discord/DiscordUtil" ; const log = new Log ( "DiscordPuppet:MatrixEventHandler" ) ; export class MatrixEventHandler { public constructor ( private readonly app ) { } public async handleMatrixMessage ( room , data , asUser , event ) { const p = this . app . puppets [ room . puppetId ] ; if ( ! p ) { return ; } const chan = await this . app . discord . getDiscordChan ( room ) ; if ( ! chan ) { log . warn ( "Channel not found" , room ) ; return ; } if ( asUser ) { const MAX_NAME_LENGTH = 80 ; const displayname = ( new TextEncoder ( ) . encode ( asUser . displayname ) ) ; asUser . displayname = ( new TextDecoder ( ) . decode ( displayname . slice ( 0 , MAX_NAME_LENGTH ) ) ) ; } const sendMsg = await this . app . matrix . parseMatrixMessage ( room . puppetId , event . content ) ; const lockKey = `${ room . puppetId };${ chan . id }` ; this . app . messageDeduplicator . lock ( lockKey , p . client . user ! . id , sendMsg ) ; try { const reply = await this . app . discord . sendToDiscord ( chan , sendMsg , asUser ) ; await this . app . matrix . insertNewEventId ( room , data . eventId ! , reply ) ; } catch ( err ) { log . warn ( "Couldn't send message" , err ) ; this . app . messageDeduplicator . unlock ( lockKey ) ; await this . app . matrix . sendMessageFail ( room ) ; } } public async handleMatrixFile ( room , data , asUser , event ) { const p = this . app . puppets [ room . puppetId ] ; if ( ! p ) { return ; } const chan = await this . app . discord . getDiscordChan ( room ) ; if ( ! chan ) { log . warn ( "Channel not found" , room ) ; return ; } let size = data . info ? data . info . size || 0 : 0 ; const mimetype = data . info ? data . info . mimetype || "" : "" ; const lockKey = `${ room . puppetId };${ chan . id }` ; const isImage = Boolean ( mimetype && mimetype . split ( "/" ) [ 0 ] === "image" ) ; if ( size < MAXFILESIZE ) { const buffer = await Util . DownloadFile ( data . url ) ; size = buffer . byteLength ; if ( size < MAXFILESIZE ) { const filename = this . app . getFilenameForMedia ( data . filename , mimetype ) ; this . app . messageDeduplicator . lock ( lockKey , p . client . user ! . id , `file:${ filename }` ) ; try { const sendFile = { buffer , filename , url : data . url , isImage , } ; const reply = await this . app . discord . sendToDiscord ( chan , sendFile , asUser ) ; await this . app . matrix . insertNewEventId ( room , data . eventId ! , reply ) ; return ; } catch ( err ) { this . app . messageDeduplicator . unlock ( lockKey ) ; log . warn ( "Couldn't send media message, retrying as embed/url" , err ) ; } } } try { const filename = await this . app . discord . discordEscape ( data . filename ) ; const msg = `Uploaded a file \`${ filename }\`: ${ data . url }` ; this . app . messageDeduplicator . lock ( lockKey , p . client . user ! . id , msg ) ; const reply = await this . app . discord . sendToDiscord ( chan , msg , asUser ) ; await this . app . matrix . insertNewEventId ( room , data . eventId ! , reply ) ; } catch ( err ) { log . warn ( "Couldn't send media message" , err ) ; this . app . messageDeduplicator . unlock ( lockKey ) ; await this . app . matrix . sendMessageFail ( room ) ; } } public async handleMatrixRedact ( room , eventId , asUser , event ) { const p = this . app . puppets [ room . puppetId ] ; if ( ! p ) { return ; } const chan = await this . app . discord . getDiscordChan ( room ) ; if ( ! chan ) { log . warn ( "Channel not found" , room ) ; return ; } log . verbose ( `Deleting message with ID ${ eventId }...` ) ; const msg = await chan . messages . fetch ( eventId ) ; if ( ! msg ) { return ; } try { p . deletedMessages . add ( msg . id ) ; const hook = msg . webhookID && this . app . discord . isTextGuildChannel ( chan ) ? await this . app . discord . getOrCreateWebhook ( chan as TextGuildChannel ) : null ; if ( hook && msg . webhookID === hook . id ) { await hook . deleteMessage ( msg ) ; } else { await msg . delete ( ) ; } await this . app . puppet . eventSync . remove ( room , msg . id ) ; } catch ( err ) { log . warn ( "Couldn't delete message" , err ) ; } } public async handleMatrixEdit ( room , eventId , data , asUser , event ) { const p = this . app . puppets [ room . puppetId ] ; if ( ! p ) { return ; } const chan = await this . app . discord . getDiscordChan ( room ) ; if ( ! chan ) { log . warn ( "Channel not found" , room ) ; return ; } log . verbose ( `Editing message with ID ${ eventId }...` ) ; const msg = await chan . messages . fetch ( eventId ) ; if ( ! msg ) { return ; } let sendMsg = await this . app . matrix . parseMatrixMessage ( room . puppetId , event . content [ "m.new_content" ] ) ; if ( msg . content . startsWith ( "> <" ) ) { const matches = msg . content . match ( /^((> [^\n]+\n)+)/ ) ; if ( matches ) { sendMsg = `${ matches [ 1 ] }${ sendMsg }` ; } } const lockKey = `${ room . puppetId };${ chan . id }` ; this . app . messageDeduplicator . lock ( lockKey , p . client . user ! . id , sendMsg ) ; try { let reply ; let matrixEventId = data . eventId ! ; if ( asUser ) { const hook = this . app . discord . isTextGuildChannel ( chan ) ? await this . app . discord . getOrCreateWebhook ( chan as TextGuildChannel ) : null ; if ( hook && msg . webhookID === hook . id ) { reply = await hook . editMessage ( msg , sendMsg , { username : asUser . displayname , avatarURL : asUser . avatarUrl || undefined , } ) ; } else if ( eventId === this . app . lastEventIds [ chan . id ] ) { try { p . deletedMessages . add ( msg . id ) ; const matrixEvents = await this . app . puppet . eventSync . getMatrix ( room , msg . id ) ; if ( matrixEvents . length > 0 ) { matrixEventId = matrixEvents [ 0 ] ; } await msg . delete ( ) ; await this . app . puppet . eventSync . remove ( room , msg . id ) ; } catch ( err ) { log . warn ( "Couldn't delete old message" , err ) ; } reply = await this . app . discord . sendToDiscord ( chan , sendMsg , asUser ) ; } else { sendMsg = `**EDIT:** ${ sendMsg }` ; reply = await this . app . discord . sendToDiscord ( chan , sendMsg , asUser ) ; } } else { reply = await msg . edit ( sendMsg ) ; } await this . app . matrix . insertNewEventId ( room , matrixEventId , reply ) ; } catch ( err ) { log . warn ( "Couldn't edit message" , err ) ; this . app . messageDeduplicator . unlock ( lockKey ) ; await this . app . matrix . sendMessageFail ( room ) ; } } public async handleMatrixReply ( room , eventId , data , asUser , event ) { const p = this . app . puppets [ room . puppetId ] ; if ( ! p ) { return ; } const chan = await this . app . discord . getDiscordChan ( room ) ; if ( ! chan ) { log . warn ( "Channel not found" , room ) ; return ; } log . verbose ( `Replying to message with ID ${ eventId }...` ) ; const msg = await chan . messages . fetch ( eventId ) ; if ( ! msg ) { return ; } let sendMsg = await this . app . matrix . parseMatrixMessage ( room . puppetId , event . content ) ; let content = msg . content ; if ( ! content && msg . embeds . length > 0 && msg . embeds [ 0 ] . description ) { content = msg . embeds [ 0 ] . description ; } const quoteParts = content . split ( "\n" ) ; quoteParts [ 0 ] = `<@${ msg . author . id }>: ${ quoteParts [ 0 ] }` ; const quote = quoteParts . map ( ( s ) => `> ${ s }` ) . join ( "\n" ) ; sendMsg = `${ quote }\n${ sendMsg }` ; const lockKey = `${ room . puppetId };${ chan . id }` ; try { this . app . messageDeduplicator . lock ( lockKey , p . client . user ! . id , sendMsg ) ; const reply = await this . app . discord . sendToDiscord ( chan , sendMsg , asUser ) ; await this . app . matrix . insertNewEventId ( room , data . eventId ! , reply ) ; } catch ( err ) { log . warn ( "Couldn't send reply" , err ) ; this . app . messageDeduplicator . unlock ( lockKey ) ; await this . app . matrix . sendMessageFail ( room ) ; } } public async handleMatrixReaction ( room , eventId , reaction , asUser , event ) { const p = this . app . puppets [ room . puppetId ] ; if ( ! p || asUser ) { return ; } const chan = await this . app . discord . getDiscordChan ( room ) ; if ( ! chan ) { log . warn ( "Channel not found" , room ) ; return ; } log . verbose ( `Reacting to ${ eventId } with ${ reaction }...` ) ; const msg = await chan . messages . fetch ( eventId ) ; if ( ! msg ) { return ; } if ( reaction . startsWith ( "mxc://" ) ) { const emoji = await this . app . discord . getDiscordEmoji ( room . puppetId , reaction ) ; if ( emoji ) { await msg . react ( emoji ) ; } } else { await msg . react ( reaction ) ; } } public async handleMatrixRemoveReaction ( room , eventId , reaction , asUser , event ) { const p = this . app . puppets [ room . puppetId ] ; if ( ! p || asUser ) { return ; } const chan = await this . app . discord . getDiscordChan ( room ) ; if ( ! chan ) { log . warn ( "Channel not found" , room ) ; return ; } log . verbose ( `Removing reaction to ${ eventId } with ${ reaction }...` ) ; const msg = await chan . messages . fetch ( eventId ) ; if ( ! msg ) { return ; } let emoji = null ; if ( reaction . startsWith ( "mxc://" ) ) { emoji = await this . app . discord . getDiscordEmoji ( room . puppetId , reaction ) ; } for ( const r of msg . reactions . cache . array ( ) ) { if ( r . emoji . name === reaction ) { await r . remove ( ) ; break ; } if ( emoji && emoji . id === r . emoji . id ) { await r . remove ( ) ; break ; } } } public async handleMatrixTyping ( room , typing , asUser , event ) { const p = this . app . puppets [ room . puppetId ] ; if ( ! p || asUser ) { return ; } const chan = await this . app . discord . getDiscordChan ( room ) ; if ( ! chan ) { log . warn ( "Channel not found" , room ) ; return ; } if ( typing ) { await chan . startTyping ( ) ; } else { chan . stopTyping ( true ) ; } } public async handleMatrixPresence ( puppetId , presence , asUser , event ) { const p = this . app . puppets [ puppetId ] ; if ( ! p || asUser ) { return ; } const presenceObj = { status : { online : "online" , offline : "invisible" , unavailable : "idle" , } [ presence . presence ] as "online" | "invisible" | "idle" | undefined , } ; if ( presence . statusMsg ) { presenceObj . activity = { name : presence . statusMsg , type : "PLAYING" , } ; } await p . client . user ! . setPresence ( presenceObj ) ; } }