import { PuppetBridge , Log , Util , IRetList , MessageDeduplicator , IRemoteRoom , } from "mx-puppet-bridge" ; import * as Discord from "better-discord.js" ; import { DiscordMessageParser , MatrixMessageParser , } from "matrix-discord-parser" ; import * as path from "path" ; import * as mime from "mime" ; import { DiscordStore } from "./store" ; import { DiscordUtil , TextGuildChannel , TextChannel , BridgeableGuildChannel , BridgeableChannel , } from "./discord/DiscordUtil" ; import { MatrixUtil } from "./matrix/MatrixUtil" ; import { Commands } from "./Commands" ; import ExpireSet from "expire-set" ; import * as Emoji from "node-emoji" ; const log = new Log ( "DiscordPuppet:App" ) ; export const AVATAR_SETTINGS = { format : "png" , size : 2048 , dynamic : true } ; export const MAXFILESIZE = 8000000 ; export interface IDiscordPuppet { client : Discord . Client ; data : any ; deletedMessages : ExpireSet < string > ; } export interface IDiscordPuppets { [ puppetId ] : IDiscordPuppet ; } export interface IDiscordSendFile { buffer : Buffer ; filename : string ; url : string ; isImage : boolean ; } export class App { public puppets : IDiscordPuppets = { } ; public discordMsgParser : DiscordMessageParser ; public matrixMsgParser : MatrixMessageParser ; public messageDeduplicator : MessageDeduplicator ; public store : DiscordStore ; public lastEventIds : { [ chan ] : string ; } = { } ; public readonly discord : DiscordUtil ; public readonly matrix : MatrixUtil ; public readonly commands : Commands ; constructor ( public puppet ) { this . discordMsgParser = new DiscordMessageParser ( ) ; this . matrixMsgParser = new MatrixMessageParser ( ) ; this . messageDeduplicator = new MessageDeduplicator ( ) ; this . store = new DiscordStore ( puppet . store ) ; this . discord = new DiscordUtil ( this ) ; this . matrix = new MatrixUtil ( this ) ; this . commands = new Commands ( this ) ; } public async init ( ) { await this . store . init ( ) ; } public async handlePuppetName ( puppetId , name ) { const p = this . puppets [ puppetId ] ; if ( ! p || ! p . data . syncProfile || ! p . client . user ! . bot ) { return ; } try { await p . client . user ! . setUsername ( name ) ; } catch ( err ) { log . warn ( `Couldn't set name for ${ puppetId }` , err ) ; } } public async handlePuppetAvatar ( puppetId , url , mxc ) { const p = this . puppets [ puppetId ] ; if ( ! p || ! p . data . syncProfile ) { return ; } try { const AVATAR_SIZE = 800 ; const realUrl = this . puppet . getUrlFromMxc ( mxc , AVATAR_SIZE , AVATAR_SIZE , "scale" ) ; const buffer = await Util . DownloadFile ( realUrl ) ; await p . client . user ! . setAvatar ( buffer ) ; } catch ( err ) { log . warn ( `Couldn't set avatar for ${ puppetId }` , err ) ; } } public async newPuppet ( puppetId , data ) { log . info ( `Adding new Puppet: puppetId=${ puppetId }` ) ; if ( this . puppets [ puppetId ] ) { await this . deletePuppet ( puppetId ) ; } let client ; if ( data . bot || false ) { client = new Discord . Client ( { ws : { intents : Discord . Intents . NON_PRIVILEGED } } ) ; } else { client = new Discord . Client ( ) ; } client . on ( "ready" , async ( ) => { const d = this . puppets [ puppetId ] . data ; d . username = client . user ! . tag ; d . id = client . user ! . id ; d . bot = client . user ! . bot ; await this . puppet . setUserId ( puppetId , client . user ! . id ) ; await this . puppet . setPuppetData ( puppetId , d ) ; await this . puppet . sendStatusMessage ( puppetId , "connected" ) ; await this . updateUserInfo ( puppetId ) ; this . puppet . trackConnectionStatus ( puppetId , true ) ; for ( const user of client . users . cache . array ( ) ) { await this . discord . updatePresence ( puppetId , user . presence ) ; } } ) ; client . on ( "message" , async ( msg ) => { try { await this . discord . events . handleDiscordMessage ( puppetId , msg ) ; } catch ( err ) { log . error ( "Error handling discord message event" , err . error || err . body || err ) ; } } ) ; client . on ( "messageUpdate" , async ( msg1 , msg2 ) => { try { await this . discord . events . handleDiscordMessageUpdate ( puppetId , msg1 , msg2 ) ; } catch ( err ) { log . error ( "Error handling discord messageUpdate event" , err . error || err . body || err ) ; } } ) ; client . on ( "messageDelete" , async ( msg ) => { try { await this . discord . events . handleDiscordMessageDelete ( puppetId , msg ) ; } catch ( err ) { log . error ( "Error handling discord messageDelete event" , err . error || err . body || err ) ; } } ) ; client . on ( "messageDeleteBulk" , async ( msgs ) => { for ( const msg of msgs . array ( ) ) { try { await this . discord . events . handleDiscordMessageDelete ( puppetId , msg ) ; } catch ( err ) { log . error ( "Error handling one discord messageDeleteBulk event" , err . error || err . body || err ) ; } } } ) ; client . on ( "typingStart" , async ( chan , user ) => { try { if ( ! this . discord . isBridgeableChannel ( chan ) ) { return ; } const params = this . matrix . getSendParams ( puppetId , chan as BridgeableChannel , user ) ; await this . puppet . setUserTyping ( params , true ) ; } catch ( err ) { log . error ( "Error handling discord typingStart event" , err . error || err . body || err ) ; } } ) ; client . on ( "presenceUpdate" , async ( _ , presence ) => { try { await this . discord . updatePresence ( puppetId , presence ) ; } catch ( err ) { log . error ( "Error handling discord presenceUpdate event" , err . error || err . body || err ) ; } } ) ; client . on ( "messageReactionAdd" , async ( reaction , user ) => { try { const chan = reaction . message . channel ; if ( ! await this . bridgeRoom ( puppetId , chan ) ) { return ; } const params = this . matrix . getSendParams ( puppetId , chan , user ) ; if ( reaction . emoji . id ) { const mxc = await this . matrix . getEmojiMxc ( puppetId , reaction . emoji . name , reaction . emoji . animated , reaction . emoji . id ) ; await this . puppet . sendReaction ( params , reaction . message . id , mxc || reaction . emoji . name ) ; } else { await this . puppet . sendReaction ( params , reaction . message . id , this . emojiAddVariant ( reaction . emoji . name ) ) ; } } catch ( err ) { log . error ( "Error handling discord messageReactionAdd event" , err . error || err . body || err ) ; } } ) ; client . on ( "messageReactionRemove" , async ( reaction , user ) => { try { const chan = reaction . message . channel ; if ( ! await this . bridgeRoom ( puppetId , chan ) ) { return ; } const params = this . matrix . getSendParams ( puppetId , chan , user ) ; if ( reaction . emoji . id ) { const mxc = await this . matrix . getEmojiMxc ( puppetId , reaction . emoji . name , reaction . emoji . animated , reaction . emoji . id ) ; await this . puppet . removeReaction ( params , reaction . message . id , mxc || reaction . emoji . name ) ; } else { await this . puppet . removeReaction ( params , reaction . message . id , this . emojiAddVariant ( reaction . emoji . name ) ) ; } } catch ( err ) { log . error ( "Error handling discord messageReactionRemove event" , err . error || err . body || err ) ; } } ) ; client . on ( "messageReactionRemoveAll" , async ( message ) => { try { const chan = message . channel ; if ( ! await this . bridgeRoom ( puppetId , chan ) ) { return ; } let user ; if ( this . discord . isBridgeableGuildChannel ( chan ) ) { const gchan = chan as BridgeableGuildChannel ; user = gchan . guild . owner ? gchan . guild . owner . user : client . user ! ; } else if ( chan instanceof Discord . DMChannel ) { user = chan . recipient ; } else if ( chan instanceof Discord . GroupDMChannel ) { user = chan . owner ; } else { user = client . user ! ; } const params = this . matrix . getSendParams ( puppetId , chan , user ) ; await this . puppet . removeAllReactions ( params , message . id ) ; } catch ( err ) { log . error ( "Error handling discord messageReactionRemoveAll event" , err . error || err . body || err ) ; } } ) ; client . on ( "channelUpdate" , async ( _ , chan ) => { if ( ! this . discord . isBridgeableChannel ( chan ) ) { return ; } const remoteChan = this . matrix . getRemoteRoom ( puppetId , chan as BridgeableChannel ) ; await this . puppet . updateRoom ( remoteChan ) ; } ) ; client . on ( "guildMemberUpdate" , async ( oldMember , newMember ) => { const promiseList = [ ] ; if ( oldMember . displayName !== newMember . displayName ) { promiseList . push ( ( async ( ) => { const remoteUser = this . matrix . getRemoteUser ( puppetId , newMember ) ; await this . puppet . updateUser ( remoteUser ) ; } ) ( ) ) ; } const leaveRooms = new Set < BridgeableGuildChannel > ( ) ; const joinRooms = new Set < BridgeableGuildChannel > ( ) ; for ( const chan of newMember . guild . channels . cache . array ( ) ) { if ( ! this . discord . isBridgeableGuildChannel ( chan ) ) { continue ; } const gchan = chan as BridgeableGuildChannel ; if ( gchan . members . has ( newMember . id ) ) { joinRooms . add ( gchan ) ; } else { leaveRooms . add ( gchan ) ; } } for ( const chan of leaveRooms ) { promiseList . push ( ( async ( ) => { const params = this . matrix . getSendParams ( puppetId , chan , newMember ) ; await this . puppet . removeUser ( params ) ; } ) ( ) ) ; } for ( const chan of joinRooms ) { promiseList . push ( ( async ( ) => { const params = this . matrix . getSendParams ( puppetId , chan , newMember ) ; await this . puppet . addUser ( params ) ; } ) ( ) ) ; } await Promise . all ( promiseList ) ; } ) ; client . on ( "userUpdate" , async ( _ , user ) => { const remoteUser = this . matrix . getRemoteUser ( puppetId , user ) ; await this . puppet . updateUser ( remoteUser ) ; } ) ; client . on ( "guildUpdate" , async ( _ , guild ) => { try { const remoteGroup = await this . matrix . getRemoteGroup ( puppetId , guild ) ; await this . puppet . updateGroup ( remoteGroup ) ; for ( const chan of guild . channels . cache . array ( ) ) { if ( ! this . discord . isBridgeableGuildChannel ( chan ) ) { return ; } const remoteChan = this . matrix . getRemoteRoom ( puppetId , chan as BridgeableGuildChannel ) ; await this . puppet . updateRoom ( remoteChan ) ; } } catch ( err ) { log . error ( "Error handling discord guildUpdate event" , err . error || err . body || err ) ; } } ) ; client . on ( "relationshipAdd" , async ( _ , relationship ) => { if ( relationship . type === "incoming" ) { const msg = `New incoming friends request from ${ relationship . user . username }!

Type \`addfriend ${ puppetId } ${ relationship . user . id }\` to accept it.` ; await this . puppet . sendStatusMessage ( puppetId , msg ) ; } } ) ; client . on ( "guildMemberAdd" , async ( member ) => { const promiseList = [ ] ; for ( const chan of member . guild . channels . cache . array ( ) ) { if ( ( await this . bridgeRoom ( puppetId , chan ) ) && chan . members . has ( member . id ) ) { promiseList . push ( ( async ( ) => { const params = this . matrix . getSendParams ( puppetId , chan as BridgeableGuildChannel , member ) ; await this . puppet . addUser ( params ) ; } ) ( ) ) ; } } await Promise . all ( promiseList ) ; } ) ; client . on ( "guildMemberRemove" , async ( member ) => { const promiseList = [ ] ; for ( const chan of member . guild . channels . cache . array ( ) ) { if ( this . discord . isBridgeableGuildChannel ( chan ) ) { promiseList . push ( ( async ( ) => { const params = this . matrix . getSendParams ( puppetId , chan as BridgeableGuildChannel , member ) ; await this . puppet . removeUser ( params ) ; } ) ( ) ) ; } } await Promise . all ( promiseList ) ; } ) ; const TWO_MIN = 120000 ; this . puppets [ puppetId ] = { client , data , deletedMessages : new ExpireSet ( TWO_MIN ) , } ; try { await client . login ( data . token , data . bot || false ) ; } catch ( e ) { log . error ( `Failed to log in puppetId ${ puppetId }:` , e ) ; await this . puppet . sendStatusMessage ( puppetId , "Failed to connect: " + e ) ; this . puppet . trackConnectionStatus ( puppetId , false ) ; } } public async deletePuppet ( puppetId ) { log . info ( `Got signal to quit Puppet: puppetId=${ puppetId }` ) ; const p = this . puppets [ puppetId ] ; if ( ! p ) { return ; } p . client . destroy ( ) ; delete this . puppet [ puppetId ] ; } public async listUsers ( puppetId ) { const retUsers = [ ] ; const retGuilds = [ ] ; const p = this . puppets [ puppetId ] ; if ( ! p ) { return [ ] ; } const blacklistedIds = [ p . client . user ! . id , "1" ] ; for ( const guild of p . client . guilds . cache . array ( ) ) { retGuilds . push ( { category : true , name : guild . name , } ) ; for ( const member of guild . members . cache . array ( ) ) { if ( ! blacklistedIds . includes ( member . user . id ) ) { retGuilds . push ( { name : member . user . username , id : member . user . id , } ) ; } } } for ( const user of p . client . users . cache . array ( ) ) { const found = retGuilds . find ( ( element ) => element . id === user . id ) ; if ( ! found && ! blacklistedIds . includes ( user . id ) ) { retUsers . push ( { name : user . username , id : user . id , } ) ; } } return retUsers . concat ( retGuilds ) ; } public async getUserIdsInRoom ( room ) { const chan = await this . discord . getDiscordChan ( room ) ; if ( ! chan ) { return null ; } const users = new Set < string > ( ) ; if ( chan instanceof Discord . DMChannel ) { users . add ( chan . recipient . id ) ; return users ; } if ( chan instanceof Discord . GroupDMChannel ) { for ( const recipient of chan . recipients . array ( ) ) { users . add ( recipient . id ) ; } return users ; } if ( this . discord . isBridgeableGuildChannel ( chan ) ) { const gchan = chan as BridgeableGuildChannel ; for ( const member of gchan . members . array ( ) ) { users . add ( member . id ) ; } return users ; } return null ; } public async updateUserInfo ( puppetId ) { const p = this . puppets [ puppetId ] ; if ( ! p || ! p . data . syncProfile ) { return ; } const userInfo = await this . puppet . getPuppetMxidInfo ( puppetId ) ; if ( userInfo ) { if ( userInfo . name ) { await this . handlePuppetName ( puppetId , userInfo . name ) ; } if ( userInfo . avatarUrl ) { await this . handlePuppetAvatar ( puppetId , userInfo . avatarUrl , userInfo . avatarMxc as string ) ; } } } public async bridgeRoom ( puppetId , chan ) { if ( [ "dm" , "group" ] . includes ( chan . type ) ) { return true ; } if ( ! this . discord . isBridgeableChannel ( chan ) ) { return false ; } if ( this . puppets [ puppetId ] && this . puppets [ puppetId ] . data . bridgeAll ) { return true ; } if ( this . discord . isBridgeableGuildChannel ( chan ) ) { const gchan = chan as BridgeableGuildChannel ; if ( await this . store . isGuildBridged ( puppetId , gchan . guild . id ) ) { return true ; } return await this . store . isChannelBridged ( puppetId , gchan . id ) ; } return false ; } public getFilenameForMedia ( filename , mimetype ) { let ext = "" ; const mimeExt = mime . getExtension ( mimetype ) ; if ( mimeExt ) { ext = "." + ( { oga : "ogg" , } [ mimeExt ] || mimeExt ) ; } if ( filename ) { if ( path . extname ( filename ) !== "" ) { return filename ; } return path . basename ( filename ) + ext ; } return "matrix-media" + ext ; } public emojiAddVariant ( s ) { if ( Emoji . find ( s + "\ufe0f" ) ) { return s + "\ufe0f" ; } return s ; } }