import { App , AVATAR_SETTINGS } from "../app" ; import * as Discord from "better-discord.js" ; import { IStringFormatterVars , IRemoteUser , IRemoteRoom , IRemoteGroup , IRemoteUserRoomOverride , IReceiveParams , IRetList , Log , } from "mx-puppet-bridge" ; import * as escapeHtml from "escape-html" ; import { IMatrixMessageParserOpts } from "matrix-discord-parser" ; import { MatrixEventHandler } from "./MatrixEventHandler" ; import { TextGuildChannel , TextChannel , BridgeableGuildChannel , BridgeableChannel } from "../discord/DiscordUtil" ; const log = new Log ( "DiscordPuppet:MatrixUtil" ) ; export class MatrixUtil { public readonly events : MatrixEventHandler ; public constructor ( private readonly app ) { this . events = new MatrixEventHandler ( app ) ; } public async getDmRoom ( user ) { const p = this . app . puppets [ user . puppetId ] ; if ( ! p ) { return null ; } const u = await this . app . discord . getUserById ( p . client , user . userId ) ; if ( ! u ) { return null ; } return `dm-${ user . puppetId }-${ u . id }` ; } public async getEmojiMxc ( puppetId , name , animated , id ) { const emoji = await this . app . puppet . emoteSync . get ( { puppetId , emoteId : id , } ) ; if ( emoji && emoji . avatarMxc ) { return emoji . avatarMxc ; } const { emote } = await this . app . puppet . emoteSync . set ( { puppetId , emoteId : id , avatarUrl : `https://cdn.discordapp.com/emojis/${ id }${ animated ? ".gif" : ".png" }` , name , data : { animated , name , } , } ) ; return emote . avatarMxc || null ; } public getSendParams ( puppetId , msgOrChannel , user ? ) { let channel ; let eventId ; let externalUrl ; let isWebhook = false ; let guildChannel ; if ( ! user ) { const msg = msgOrChannel as Discord . Message ; channel = msg . channel ; user = msg . member || msg . author ; eventId = msg . id ; isWebhook = msg . webhookID ? true : false ; if ( this . app . discord . isBridgeableGuildChannel ( channel ) ) { guildChannel = channel as BridgeableGuildChannel ; externalUrl = `https://discordapp.com/channels/${ guildChannel . guild . id }/${ guildChannel . id }/${ eventId }` ; } else if ( [ "group" , "dm" ] . includes ( channel . type ) ) { externalUrl = `https://discordapp.com/channels/@me/${ channel . id }/${ eventId }` ; } } else { channel = msgOrChannel as BridgeableChannel ; } return { room : this . getRemoteRoom ( puppetId , channel ) , user : this . getRemoteUser ( puppetId , user , isWebhook , guildChannel ) , eventId , externalUrl , } ; } public getRemoteUserRoomOverride ( member , chan ) { const nameVars = { name : member . user . username , discriminator : member . user . discriminator , displayname : member . displayName , channel : chan . name , guild : chan . guild . name , } ; return { nameVars , } ; } public getRemoteUser ( puppetId , userOrMember , isWebhook = false , chan ? ) { let user ; let member = null ; if ( userOrMember instanceof Discord . GuildMember ) { member = userOrMember ; user = member . user ; } else { user = userOrMember ; } const nameVars = { name : user . username , discriminator : user . discriminator , } ; const response = { userId : isWebhook ? `webhook-${ user . id }-${ user . username }` : user . id , puppetId , avatarUrl : user . avatarURL ( AVATAR_SETTINGS ) , nameVars , } ; if ( member ) { response . roomOverrides = { } ; if ( chan ) { response . roomOverrides [ chan . id ] = this . getRemoteUserRoomOverride ( member , chan ) ; } else { for ( const gchan of member . guild . channels . cache . array ( ) ) { if ( this . app . discord . isBridgeableGuildChannel ( gchan ) ) { response . roomOverrides [ gchan . id ] = this . getRemoteUserRoomOverride ( member , gchan as BridgeableGuildChannel ) ; } } } } return response ; } public getRemoteRoom ( puppetId , channel ) { let roomId = channel . id ; if ( channel instanceof Discord . DMChannel ) { roomId = `dm-${ puppetId }-${ channel . recipient . id }` ; } const ret = { roomId , puppetId , isDirect : channel . type === "dm" , } ; if ( channel instanceof Discord . GroupDMChannel ) { ret . nameVars = { name : channel . name , } ; ret . avatarUrl = channel . iconURL ( AVATAR_SETTINGS ) ; } if ( this . app . discord . isBridgeableGuildChannel ( channel ) ) { const gchan = channel as BridgeableGuildChannel ; ret . nameVars = { name : gchan . name , guild : gchan . guild . name , category : gchan . parent ?. name , } ; ret . avatarUrl = gchan . guild . iconURL ( AVATAR_SETTINGS ) ; ret . groupId = gchan . guild . id ; ret . topic = gchan . topic ; ret . emotes = gchan . guild . emojis . cache . map ( ( e ) => { return { emoteId : e . id , name : e . name , avatarUrl : e . url , data : { animated : e . animated , name : e . name , } , } ; } ) ; } return ret ; } public async getRemoteRoomById ( room ) { const chan = await this . app . discord . getDiscordChan ( room ) ; if ( ! chan ) { return null ; } if ( ! await this . app . bridgeRoom ( room . puppetId , chan ) ) { return null ; } return this . getRemoteRoom ( room . puppetId , chan ) ; } public async getRemoteGroup ( puppetId , guild ) { const roomIds = [ ] ; let description = `<h1>${ escapeHtml ( guild . name ) }</h1>` ; description += `<h2>Channels:</h2><ul>` ; await this . app . discord . iterateGuildStructure ( puppetId , guild , async ( cat ) => { const name = escapeHtml ( cat . name ) ; description += `</ul><h3>${ name }</h3><ul>` ; } , async ( chan ) => { roomIds . push ( chan . id ) ; const mxid = await this . app . puppet . getMxidForRoom ( { puppetId , roomId : chan . id , } ) ; const url = "https://matrix.to/#/" + mxid ; const name = escapeHtml ( chan . name ) ; description += `<li>${ name }: <a href="${ url }">${ name }</a></li>` ; } ) ; description += "</ul>" ; return { puppetId , groupId : guild . id , nameVars : { name : guild . name , } , avatarUrl : guild . iconURL ( AVATAR_SETTINGS ) , roomIds , longDescription : description , } ; } public async insertNewEventId ( room , matrixId , msgs ) { const p = this . app . puppets [ room . puppetId ] ; if ( ! Array . isArray ( msgs ) ) { msgs = [ msgs ] ; } for ( const m of msgs ) { const lockKey = `${ room . puppetId };${ m . channel . id }` ; await this . app . puppet . eventSync . insert ( room , matrixId , m . id ) ; this . app . messageDeduplicator . unlock ( lockKey , p . client . user ! . id , m . id ) ; this . app . lastEventIds [ m . channel . id ] = m . id ; } } public async createRoom ( chan ) { return await this . getRemoteRoomById ( chan ) ; } public async createUser ( user ) { const p = this . app . puppets [ user . puppetId ] ; if ( ! p ) { return null ; } if ( user . userId . startsWith ( "webhook-" ) ) { return null ; } const u = await this . app . discord . getUserById ( p . client , user . userId ) ; if ( ! u ) { return null ; } const remoteUser = this . getRemoteUser ( user . puppetId , u ) ; remoteUser . roomOverrides = { } ; for ( const guild of p . client . guilds . cache . array ( ) ) { const member = guild . members . resolve ( u . id ) ; if ( member ) { for ( const chan of guild . channels . cache . array ( ) ) { if ( this . app . discord . isBridgeableGuildChannel ( chan ) ) { remoteUser . roomOverrides [ chan . id ] = this . getRemoteUserRoomOverride ( member , chan as BridgeableGuildChannel ) ; } } } } return remoteUser ; } public async createGroup ( group ) { const p = this . app . puppets [ group . puppetId ] ; if ( ! p ) { return null ; } const guild = p . client . guilds . resolve ( group . groupId ) ; if ( ! guild ) { return null ; } return await this . getRemoteGroup ( group . puppetId , guild ) ; } public async listRooms ( puppetId ) { const retGroups = [ ] ; const retGuilds = [ ] ; const p = this . app . puppets [ puppetId ] ; if ( ! p ) { return [ ] ; } for ( const guild of p . client . guilds . cache . array ( ) ) { let didGuild = false ; let didCat = false ; await this . app . discord . iterateGuildStructure ( puppetId , guild , async ( cat ) => { didCat = true ; retGuilds . push ( { category : true , name : `${ guild . name } - ${ cat . name }` , } ) ; } , async ( chan ) => { if ( ! didGuild && ! didCat ) { didGuild = true ; retGuilds . push ( { category : true , name : guild . name , } ) ; } retGuilds . push ( { name : chan . name , id : chan . id , } ) ; } ) ; } for ( const chan of p . client . channels . cache . array ( ) ) { if ( chan instanceof Discord . GroupDMChannel ) { const found = retGuilds . find ( ( element ) => element . id === chan . id ) ; if ( ! found ) { retGroups . push ( { name : chan . name || "" , id : chan . id , } ) ; } } } return retGroups . concat ( retGuilds ) ; } public async parseMatrixMessage ( puppetId , eventContent ) { const opts = { displayname : "" , callbacks : { canNotifyRoom : async ( ) => true , getUserId : async ( mxid ) => { const parts = this . app . puppet . userSync . getPartsFromMxid ( mxid ) ; if ( ! parts || ( parts . puppetId !== puppetId && parts . puppetId !== - 1 ) ) { return null ; } return parts . userId ; } , getChannelId : async ( mxid ) => { const parts = await this . app . puppet . roomSync . getPartsFromMxid ( mxid ) ; if ( ! parts || ( parts . puppetId !== puppetId && parts . puppetId !== - 1 ) ) { return null ; } return parts . roomId ; } , getEmoji : async ( mxc , name ) => { const emote = await this . app . puppet . emoteSync . getByMxc ( puppetId , mxc ) ; log . info ( "Found emoji" , emote ) ; if ( ! emote ) { return null ; } return { animated : Boolean ( emote . data && emote . data . animated ) , name : ( ( emote . data && emote . data . name ) || emote . name ) as string , id : emote . emoteId , } ; } , mxcUrlToHttp : ( mxc ) => this . app . puppet . getUrlFromMxc ( mxc ) , } , determineCodeLanguage : true , } ; const msg = await this . app . matrixMsgParser . FormatMessage ( opts , eventContent ) ; return msg ; } public async sendMessageFail ( room ) { const chan = await this . app . discord . getDiscordChan ( room ) ; if ( ! chan ) { return ; } let msg = "" ; if ( chan instanceof Discord . DMChannel ) { msg = `Failed to send message to DM with user ${ chan . recipient . username }` ; } else if ( chan instanceof Discord . GroupDMChannel ) { let name = chan . name ; if ( ! name ) { const names = [ ] ; for ( const user of chan . recipients . array ( ) ) { names . push ( user . username ) ; } name = names . join ( ", " ) ; } msg = `Failed to send message into Group DM ${ name }` ; } else if ( this . app . discord . isBridgeableGuildChannel ( chan ) ) { const gchan = chan as BridgeableGuildChannel ; msg = `Failed to send message into channel ${ gchan . name } of guild ${ gchan . guild . name }` ; } else { msg = `Failed to send message into channel with id \`${ chan . id }\`` ; } await this . app . puppet . sendStatusMessage ( room , msg ) ; } }