2023-04-25 20:06:08 +00:00
// @ts-check
2023-06-28 12:06:56 +00:00
const assert = require ( "assert" ) . strict
2024-07-20 13:30:07 +00:00
const markdown = require ( "@cloudrac3r/discord-markdown" )
2023-07-13 05:36:20 +00:00
const pb = require ( "prettier-bytes" )
2023-07-11 04:51:30 +00:00
const DiscordTypes = require ( "discord-api-types/v10" )
2024-07-20 13:30:07 +00:00
const { tag } = require ( "@cloudrac3r/html-template-tag" )
2023-04-25 20:06:08 +00:00
2023-05-12 05:35:37 +00:00
const passthrough = require ( "../../passthrough" )
2023-09-14 00:32:27 +00:00
const { sync , db , discord , select , from } = passthrough
2023-05-12 05:35:37 +00:00
/** @type {import("../../matrix/file")} */
const file = sync . require ( "../../matrix/file" )
2023-10-07 07:58:46 +00:00
/** @type {import("./emoji-to-key")} */
const emojiToKey = sync . require ( "./emoji-to-key" )
2024-01-18 03:54:09 +00:00
/** @type {import("../actions/lottie")} */
const lottie = sync . require ( "../actions/lottie" )
2023-10-27 11:24:42 +00:00
/** @type {import("../../m2d/converters/utils")} */
const mxUtils = sync . require ( "../../m2d/converters/utils" )
2023-11-23 03:11:46 +00:00
/** @type {import("../../discord/utils")} */
const dUtils = sync . require ( "../../discord/utils" )
2023-07-11 05:27:40 +00:00
const reg = require ( "../../matrix/read-registration" )
const userRegex = reg . namespaces . users . map ( u => new RegExp ( u . regex ) )
2023-05-12 05:35:37 +00:00
2023-10-13 13:15:21 +00:00
/ * *
* @ param { DiscordTypes . APIMessage } message
* @ param { DiscordTypes . APIGuild } guild
* @ param { boolean } useHTML
* /
function getDiscordParseCallbacks ( message , guild , useHTML ) {
2023-07-07 05:31:39 +00:00
return {
2023-08-16 05:03:05 +00:00
/** @param {{id: string, type: "discordUser"}} node */
2023-07-07 05:31:39 +00:00
user : node => {
2023-10-05 23:31:10 +00:00
const mxid = select ( "sim" , "mxid" , { user _id : node . id } ) . pluck ( ) . get ( )
2024-08-27 14:05:40 +00:00
const interaction = message . interaction _metadata || message . interaction
const username = message . mentions . find ( ment => ment . id === node . id ) ? . username
|| ( interaction ? . user . id === node . id ? interaction . user . username : null )
|| node . id
2023-07-07 05:31:39 +00:00
if ( mxid && useHTML ) {
return ` <a href="https://matrix.to/#/ ${ mxid } ">@ ${ username } </a> `
} else {
return ` @ ${ username } : `
}
} ,
2024-02-13 03:52:21 +00:00
/** @param {{id: string, type: "discordChannel", row: {room_id: string, name: string, nick: string?}?, via: string}} node */
2023-07-07 05:31:39 +00:00
channel : node => {
2024-02-13 22:57:01 +00:00
if ( ! node . row ) { // fallback for when this channel is not bridged
const channel = discord . channels . get ( node . id )
if ( channel ) {
return ` # ${ channel . name } [channel not bridged] `
} else {
return ` #unknown-channel [channel from an unbridged server] `
}
2023-08-20 20:07:05 +00:00
} else if ( useHTML ) {
2024-02-13 03:52:21 +00:00
return ` <a href="https://matrix.to/#/ ${ node . row . room _id } ? ${ node . via } "># ${ node . row . nick || node . row . name } </a> `
2023-07-07 05:31:39 +00:00
} else {
2024-02-12 10:07:55 +00:00
return ` # ${ node . row . nick || node . row . name } `
2023-07-07 05:31:39 +00:00
}
} ,
2023-08-16 05:03:05 +00:00
/** @param {{animated: boolean, name: string, id: string, type: "discordEmoji"}} node */
emoji : node => {
if ( useHTML ) {
2023-10-05 23:31:10 +00:00
const mxc = select ( "emoji" , "mxc_url" , { emoji _id : node . id } ) . pluck ( ) . get ( )
2024-08-16 00:04:09 +00:00
assert ( mxc , ` Emoji consistency assertion failed for ${ node . name } : ${ node . id } ` ) // All emojis should have been added ahead of time in the messageToEvent function.
2023-11-23 03:11:46 +00:00
return ` <img data-mx-emoticon height="32" src=" ${ mxc } " title=": ${ node . name } :" alt=": ${ node . name } :"> `
2023-08-16 05:03:05 +00:00
} else {
return ` : ${ node . name } : `
}
} ,
2023-10-13 13:15:21 +00:00
role : node => {
const role = guild . roles . find ( r => r . id === node . id )
if ( ! role ) {
2023-11-23 02:52:41 +00:00
// This fallback should only trigger if somebody manually writes a silly message, or if the cache breaks (hasn't happened yet).
// If the cache breaks, fix discord-packets.js to store role info properly.
return "@&" + node . id
2023-10-13 13:15:21 +00:00
} else if ( useHTML && role . color ) {
return ` <font color="# ${ role . color . toString ( 16 ) } ">@ ${ role . name } </font> `
} else if ( useHTML ) {
return ` <span data-mx-color="#ffffff" data-mx-bg-color="#414eef">@ ${ role . name } </span> `
} else {
return ` @ ${ role . name } : `
}
} ,
2024-03-04 04:02:38 +00:00
everyone : ( ) => {
if ( message . mention _everyone ) return "@room"
return "@everyone"
} ,
here : ( ) => {
if ( message . mention _everyone ) return "@room"
return "@here"
}
2023-07-07 05:31:39 +00:00
}
}
2023-10-27 11:24:42 +00:00
const embedTitleParser = markdown . markdownEngine . parserFor ( {
... markdown . rules ,
autolink : undefined ,
link : undefined
} )
2023-11-23 00:37:09 +00:00
/ * *
* @ param { { room ? : boolean , user _ids ? : string [ ] } } mentions
* @ param { DiscordTypes . APIAttachment } attachment
* /
async function attachmentToEvent ( mentions , attachment ) {
const emoji =
attachment . content _type ? . startsWith ( "image/jp" ) ? "📸"
: attachment . content _type ? . startsWith ( "image/" ) ? "🖼️"
: attachment . content _type ? . startsWith ( "video/" ) ? "🎞️"
: attachment . content _type ? . startsWith ( "text/" ) ? "📝"
: attachment . content _type ? . startsWith ( "audio/" ) ? "🎶"
: "📄"
// no native media spoilers in Element, so we'll post a link instead, forcing it to not preview using a blockquote
if ( attachment . filename . startsWith ( "SPOILER_" ) ) {
return {
$type : "m.room.message" ,
"m.mentions" : mentions ,
msgtype : "m.text" ,
body : ` ${ emoji } Uploaded SPOILER file: ${ attachment . url } ( ${ pb ( attachment . size ) } ) ` ,
format : "org.matrix.custom.html" ,
2023-11-25 09:08:29 +00:00
formatted _body : ` <blockquote> ${ emoji } Uploaded SPOILER file: <a href=" ${ attachment . url } "> ${ attachment . url } </a> ( ${ pb ( attachment . size ) } )</blockquote> `
2023-11-23 00:37:09 +00:00
}
}
// for large files, always link them instead of uploading so I don't use up all the space in the content repo
else if ( attachment . size > reg . ooye . max _file _size ) {
return {
$type : "m.room.message" ,
"m.mentions" : mentions ,
msgtype : "m.text" ,
body : ` ${ emoji } Uploaded file: ${ attachment . url } ( ${ pb ( attachment . size ) } ) ` ,
format : "org.matrix.custom.html" ,
formatted _body : ` ${ emoji } Uploaded file: <a href=" ${ attachment . url } "> ${ attachment . filename } </a> ( ${ pb ( attachment . size ) } ) `
}
} else if ( attachment . content _type ? . startsWith ( "image/" ) && attachment . width && attachment . height ) {
return {
$type : "m.room.message" ,
"m.mentions" : mentions ,
msgtype : "m.image" ,
url : await file . uploadDiscordFileToMxc ( attachment . url ) ,
external _url : attachment . url ,
2024-01-06 06:10:52 +00:00
body : attachment . description || attachment . filename ,
2023-11-23 00:37:09 +00:00
filename : attachment . filename ,
info : {
mimetype : attachment . content _type ,
w : attachment . width ,
h : attachment . height ,
size : attachment . size
}
}
} else if ( attachment . content _type ? . startsWith ( "video/" ) && attachment . width && attachment . height ) {
return {
$type : "m.room.message" ,
"m.mentions" : mentions ,
msgtype : "m.video" ,
url : await file . uploadDiscordFileToMxc ( attachment . url ) ,
external _url : attachment . url ,
body : attachment . description || attachment . filename ,
filename : attachment . filename ,
info : {
mimetype : attachment . content _type ,
w : attachment . width ,
h : attachment . height ,
size : attachment . size
}
}
} else if ( attachment . content _type ? . startsWith ( "audio/" ) ) {
return {
$type : "m.room.message" ,
"m.mentions" : mentions ,
msgtype : "m.audio" ,
url : await file . uploadDiscordFileToMxc ( attachment . url ) ,
external _url : attachment . url ,
body : attachment . description || attachment . filename ,
filename : attachment . filename ,
info : {
mimetype : attachment . content _type ,
size : attachment . size ,
duration : attachment . duration _secs ? attachment . duration _secs * 1000 : undefined
}
}
} else {
return {
$type : "m.room.message" ,
"m.mentions" : mentions ,
msgtype : "m.file" ,
url : await file . uploadDiscordFileToMxc ( attachment . url ) ,
external _url : attachment . url ,
2024-01-06 06:10:52 +00:00
body : attachment . description || attachment . filename ,
2023-11-23 00:37:09 +00:00
filename : attachment . filename ,
info : {
mimetype : attachment . content _type ,
size : attachment . size
}
}
}
}
2023-04-25 20:06:08 +00:00
/ * *
* @ param { import ( "discord-api-types/v10" ) . APIMessage } message
2023-06-28 12:06:56 +00:00
* @ param { import ( "discord-api-types/v10" ) . APIGuild } guild
2023-08-16 08:44:38 +00:00
* @ param { { includeReplyFallback ? : boolean , includeEditFallbackStar ? : boolean } } options default values :
* - includeReplyFallback : true
* - includeEditFallbackStar : false
* @ param { { api : import ( "../../matrix/api" ) } } di simple - as - nails dependency injection for the matrix API
2023-04-25 20:06:08 +00:00
* /
2023-08-16 08:44:38 +00:00
async function messageToEvent ( message , guild , options = { } , di ) {
2023-05-12 05:35:37 +00:00
const events = [ ]
2024-03-04 04:02:38 +00:00
/* c8 ignore next 7 */
2023-08-21 11:31:40 +00:00
if ( message . type === DiscordTypes . MessageType . ThreadCreated ) {
// This is the kind of message that appears when somebody makes a thread which isn't close enough to the message it's based off.
// It lacks the lines and the pill, so it looks kind of like a member join message, and it says:
// [#] NICKNAME started a thread: __THREAD NAME__. __See all threads__
// We're already bridging the THREAD_CREATED gateway event to make a comparable message, so drop this one.
return [ ]
}
if ( message . type === DiscordTypes . MessageType . ThreadStarterMessage ) {
// This is the message that appears at the top of a thread when the thread was based off an existing message.
// It's just a message reference, no content.
const ref = message . message _reference
assert ( ref )
assert ( ref . message _id )
2023-10-05 23:31:10 +00:00
const eventID = select ( "event_message" , "event_id" , { message _id : ref . message _id } ) . pluck ( ) . get ( )
const roomID = select ( "channel_room" , "room_id" , { channel _id : ref . channel _id } ) . pluck ( ) . get ( )
2023-08-28 05:32:55 +00:00
if ( ! eventID || ! roomID ) return [ ]
const event = await di . api . getEvent ( roomID , eventID )
2023-08-21 11:31:40 +00:00
return [ {
... event . content ,
2023-10-14 04:23:55 +00:00
$type : event . type ,
$sender : null
2023-08-21 11:31:40 +00:00
} ]
}
2024-08-27 14:05:40 +00:00
const interaction = message . interaction _metadata || message . interaction
if ( message . type === DiscordTypes . MessageType . ChatInputCommand && interaction && "name" in interaction ) {
2024-08-27 12:50:48 +00:00
// Commands are sent by the responding bot. Need to attach the metadata of the person using the command at the top.
2024-08-27 14:05:40 +00:00
if ( message . content ) message . content = ` \n ${ message . content } `
message . content = ` > ↪️ <@ ${ interaction . user . id } > used \` / ${ interaction . name } \` ${ message . content } `
2024-08-27 12:50:48 +00:00
}
2023-07-11 04:51:30 +00:00
/ * *
@ type { { room ? : boolean , user _ids ? : string [ ] } }
We should consider the following scenarios for mentions :
2023-07-11 05:28:42 +00:00
1. A discord user rich - replies to a matrix user with a text post
2023-07-11 04:51:30 +00:00
+ The matrix user needs to be m . mentioned in the text event
+ The matrix user needs to have their name / mxid / link in the text event ( notification fallback )
- So prepend their ` @name: ` to the start of the plaintext body
2023-07-11 05:28:42 +00:00
2. A discord user rich - replies to a matrix user with an image event only
2023-07-11 04:51:30 +00:00
+ The matrix user needs to be m . mentioned in the image event
2023-07-11 05:28:42 +00:00
+ TODO The matrix user needs to have their name / mxid in the image event ' s body field , alongside the filename ( notification fallback )
2023-07-11 04:51:30 +00:00
- So append their name to the filename body , I guess ! ! !
2023-07-11 05:28:42 +00:00
3. A discord user ` @ ` s a matrix user in the text body of their text box
2023-07-11 04:51:30 +00:00
+ The matrix user needs to be m . mentioned in the text event
+ No change needed to the text event content : it already has their name
- So make sure we don ' t do anything in this case .
* /
const mentions = { }
2023-09-14 00:32:27 +00:00
let repliedToEventRow = null
2023-07-11 04:51:30 +00:00
let repliedToEventSenderMxid = null
2024-03-05 22:56:21 +00:00
if ( message . mention _everyone ) mentions . room = true
2023-07-11 04:51:30 +00:00
function addMention ( mxid ) {
if ( ! mentions . user _ids ) mentions . user _ids = [ ]
2023-07-11 05:27:40 +00:00
if ( ! mentions . user _ids . includes ( mxid ) ) mentions . user _ids . push ( mxid )
2023-07-11 04:51:30 +00:00
}
// Mentions scenarios 1 and 2, part A. i.e. translate relevant message.mentions to m.mentions
// (Still need to do scenarios 1 and 2 part B, and scenario 3.)
if ( message . type === DiscordTypes . MessageType . Reply && message . message _reference ? . message _id ) {
2023-09-14 00:32:27 +00:00
const row = from ( "event_message" ) . join ( "message_channel" , "message_id" ) . join ( "channel_room" , "channel_id" ) . select ( "event_id" , "room_id" , "source" ) . and ( "WHERE message_id = ? AND part = 0" ) . get ( message . message _reference . message _id )
2023-07-11 04:51:30 +00:00
if ( row ) {
2023-09-14 00:32:27 +00:00
repliedToEventRow = row
2023-07-11 04:51:30 +00:00
}
2024-02-15 21:06:15 +00:00
} else if ( dUtils . isWebhookMessage ( message ) && message . embeds [ 0 ] ? . author ? . name ? . endsWith ( "↩️" ) ) {
// It could be a PluralKit emulated reply, let's see if it has a message link
const isEmulatedReplyToText = message . embeds [ 0 ] . description ? . startsWith ( "**[Reply to:]" )
const isEmulatedReplyToAttachment = message . embeds [ 0 ] . description ? . startsWith ( "*[(click to see attachment" )
if ( isEmulatedReplyToText || isEmulatedReplyToAttachment ) {
assert ( message . embeds [ 0 ] . description )
const match = message . embeds [ 0 ] . description . match ( /\/channels\/[0-9]*\/[0-9]*\/([0-9]{2,})/ )
if ( match ) {
const row = from ( "event_message" ) . join ( "message_channel" , "message_id" ) . join ( "channel_room" , "channel_id" ) . select ( "event_id" , "room_id" , "source" ) . and ( "WHERE message_id = ? AND part = 0" ) . get ( match [ 1 ] )
if ( row ) {
/ *
we generate a partial referenced _message based on what PK provided . we don ' t need everything , since this will only be used for further message - to - event converting .
the following properties are necessary :
- content : used for generating the reply fallback
- author : used for the top of the reply fallback ( only used for discord authors . for matrix authors , repliedToEventSenderMxid is set . )
* /
const emulatedMessageContent =
( isEmulatedReplyToAttachment ? "[Media]"
: message . embeds [ 0 ] . description . replace ( /^.*?\)\*\*\s*/ , "" ) )
message . referenced _message = {
content : emulatedMessageContent ,
// @ts-ignore
author : {
username : message . embeds [ 0 ] . author . name . replace ( /\s*↩️\s*$/ , "" )
}
2024-02-13 01:38:41 +00:00
}
2024-02-15 21:06:15 +00:00
message . embeds . shift ( )
repliedToEventRow = row
2024-02-02 02:55:02 +00:00
}
2024-02-01 09:23:08 +00:00
}
}
2023-07-11 04:51:30 +00:00
}
2023-09-14 00:32:27 +00:00
if ( repliedToEventRow && repliedToEventRow . source === 0 ) { // reply was originally from Matrix
2023-07-11 04:51:30 +00:00
// Need to figure out who sent that event...
2023-09-14 00:32:27 +00:00
const event = await di . api . getEvent ( repliedToEventRow . room _id , repliedToEventRow . event _id )
2023-07-11 04:51:30 +00:00
repliedToEventSenderMxid = event . sender
// Need to add the sender to m.mentions
addMention ( repliedToEventSenderMxid )
}
2024-02-13 03:52:21 +00:00
/** @type {Map<string, Promise<string>>} */
const viaMemo = new Map ( )
/ * *
* @ param { string } roomID
* @ returns { Promise < string > } string encoded URLSearchParams
* /
function getViaServersMemo ( roomID ) {
// @ts-ignore
if ( viaMemo . has ( roomID ) ) return viaMemo . get ( roomID )
const promise = mxUtils . getViaServersQuery ( roomID , di . api ) . then ( p => p . toString ( ) )
viaMemo . set ( roomID , promise )
return promise
}
2023-10-27 11:24:42 +00:00
/ * *
* Translate Discord message links to Matrix event links .
2023-11-23 03:11:46 +00:00
* If OOYE has handled this message in the past , this is an instant database lookup .
* Otherwise , if OOYE knows the channel , this is a multi - second request to / timestamp _to _event to approximate .
2023-10-27 11:24:42 +00:00
* @ param { string } content Partial or complete Discord message content
* /
2023-11-23 03:11:46 +00:00
async function transformContentMessageLinks ( content ) {
2023-11-23 03:41:32 +00:00
let offset = 0
2023-11-30 03:27:40 +00:00
for ( const match of [ ... content . matchAll ( /https:\/\/(?:ptb\.|canary\.|www\.)?discord(?:app)?\.com\/channels\/[0-9]+\/([0-9]+)\/([0-9]+)/g ) ] ) {
2023-11-23 03:11:46 +00:00
assert ( typeof match . index === "number" )
2023-11-30 03:27:40 +00:00
const [ _ , channelID , messageID ] = match
2023-11-23 03:11:46 +00:00
let result
2023-11-30 03:27:40 +00:00
const roomID = select ( "channel_room" , "room_id" , { channel _id : channelID } ) . pluck ( ) . get ( )
2023-11-23 03:11:46 +00:00
if ( roomID ) {
const eventID = select ( "event_message" , "event_id" , { message _id : messageID } ) . pluck ( ) . get ( )
2024-02-13 03:52:21 +00:00
const via = await getViaServersMemo ( roomID )
2023-11-23 03:11:46 +00:00
if ( eventID && roomID ) {
2024-02-13 03:52:21 +00:00
result = ` https://matrix.to/#/ ${ roomID } / ${ eventID } ? ${ via } `
2023-11-23 03:11:46 +00:00
} else {
const ts = dUtils . snowflakeToTimestampExact ( messageID )
const { event _id } = await di . api . getEventForTimestamp ( roomID , ts )
2024-02-13 03:52:21 +00:00
result = ` https://matrix.to/#/ ${ roomID } / ${ event _id } ? ${ via } `
2023-11-23 03:11:46 +00:00
}
2023-07-10 20:01:11 +00:00
} else {
2023-11-23 03:11:46 +00:00
result = ` ${ match [ 0 ] } [event is from another server] `
2023-07-10 20:01:11 +00:00
}
2023-11-30 03:27:40 +00:00
2023-11-23 03:41:32 +00:00
content = content . slice ( 0 , match . index + offset ) + result + content . slice ( match . index + match [ 0 ] . length + offset )
offset += result . length - match [ 0 ] . length
2023-11-23 03:11:46 +00:00
}
return content
2023-10-27 11:24:42 +00:00
}
/ * *
* Translate links and emojis and mentions and stuff . Give back the text and HTML so they can be combined into bigger events .
* @ param { string } content Partial or complete Discord message content
* @ param { any } customOptions
* @ param { any } customParser
* @ param { any } customHtmlOutput
* /
async function transformContent ( content , customOptions = { } , customParser = null , customHtmlOutput = null ) {
2023-11-23 03:11:46 +00:00
content = await transformContentMessageLinks ( content )
2023-07-10 20:01:11 +00:00
2023-09-20 13:39:09 +00:00
// Handling emojis that we don't know about. The emoji has to be present in the DB for it to be picked up in the emoji markdown converter.
// So we scan the message ahead of time for all its emojis and ensure they are in the DB.
2024-08-27 13:31:57 +00:00
const emojiMatches = [ ... content . matchAll ( /<(a?):([^:>]{1,64}):([0-9]+)>/g ) ]
2023-10-07 07:58:46 +00:00
await Promise . all ( emojiMatches . map ( match => {
2023-09-20 13:39:09 +00:00
const id = match [ 3 ]
const name = match [ 2 ]
2023-10-27 11:24:42 +00:00
const animated = ! ! match [ 1 ]
2023-10-07 07:58:46 +00:00
return emojiToKey . emojiToKey ( { id , name , animated } ) // Register the custom emoji if needed
} ) )
2023-09-20 13:39:09 +00:00
2024-02-12 10:07:55 +00:00
async function transformParsedVia ( parsed ) {
for ( const node of parsed ) {
if ( node . type === "discordChannel" ) {
node . row = select ( "channel_room" , [ "room_id" , "name" , "nick" ] , { channel _id : node . id } ) . get ( )
if ( node . row ? . room _id ) {
2024-02-13 03:52:21 +00:00
node . via = await getViaServersMemo ( node . row . room _id )
2024-02-12 10:07:55 +00:00
}
}
if ( Array . isArray ( node . content ) ) {
await transformParsedVia ( node . content )
}
}
return parsed
}
let html = await markdown . toHtmlWithPostParser ( content , transformParsedVia , {
2023-10-27 11:24:42 +00:00
discordCallback : getDiscordParseCallbacks ( message , guild , true ) ,
... customOptions
} , customParser , customHtmlOutput )
2023-07-07 05:31:39 +00:00
2024-02-12 10:07:55 +00:00
let body = await markdown . toHtmlWithPostParser ( content , transformParsedVia , {
2023-10-13 13:15:21 +00:00
discordCallback : getDiscordParseCallbacks ( message , guild , false ) ,
2023-07-07 05:31:39 +00:00
discordOnly : true ,
escapeHTML : false ,
2023-10-27 11:24:42 +00:00
... customOptions
2024-07-20 13:30:07 +00:00
} )
2023-07-07 05:31:39 +00:00
2023-10-27 11:24:42 +00:00
return { body , html }
}
2023-07-11 05:27:40 +00:00
2024-02-12 10:07:55 +00:00
// FIXME: What was the scanMentions parameter supposed to activate? It's unused.
2023-10-27 11:24:42 +00:00
async function addTextEvent ( body , html , msgtype , { scanMentions } ) {
2023-08-16 08:44:38 +00:00
// Star * prefix for fallback edits
if ( options . includeEditFallbackStar ) {
body = "* " + body
html = "* " + html
}
2023-10-09 22:29:27 +00:00
const flags = message . flags || 0
if ( flags & 2 ) {
body = ` [🔀 ${ message . author . username } ] \n ` + body
html = ` 🔀 <strong> ${ message . author . username } </strong><br> ` + html
}
2023-07-11 04:51:30 +00:00
// Fallback body/formatted_body for replies
2023-08-16 08:44:38 +00:00
// This branch is optional - do NOT change anything apart from the reply fallback, since it may not be run
2023-09-14 00:32:27 +00:00
if ( repliedToEventRow && options . includeReplyFallback !== false ) {
2023-07-11 04:51:30 +00:00
let repliedToDisplayName
let repliedToUserHtml
2023-09-14 00:32:27 +00:00
if ( repliedToEventRow ? . source === 0 && repliedToEventSenderMxid ) {
2023-07-11 04:51:30 +00:00
const match = repliedToEventSenderMxid . match ( /^@([^:]*)/ )
assert ( match )
2024-02-15 21:06:15 +00:00
repliedToDisplayName = message . referenced _message ? . author . username || match [ 1 ] || "a Matrix user" // grab the localpart as the display name, whatever
2023-07-11 04:51:30 +00:00
repliedToUserHtml = ` <a href="https://matrix.to/#/ ${ repliedToEventSenderMxid } "> ${ repliedToDisplayName } </a> `
} else {
repliedToDisplayName = message . referenced _message ? . author . global _name || message . referenced _message ? . author . username || "a Discord user"
repliedToUserHtml = repliedToDisplayName
}
2023-07-12 02:33:38 +00:00
let repliedToContent = message . referenced _message ? . content
2024-08-16 00:04:09 +00:00
if ( repliedToContent ? . match ( /^(-# )?> (-# )?<:L1:/ ) ) {
2023-10-12 13:05:44 +00:00
// If the Discord user is replying to a Matrix user's reply, the fallback is going to contain the emojis and stuff from the bridged rep of the Matrix user's reply quote.
// Need to remove that previous reply rep from this fallback body. The fallbody body should only contain the Matrix user's actual message.
2024-08-16 00:04:09 +00:00
// ┌──────A─────┐ A reply rep starting with >quote or -#smalltext >quote. Match until the end of the line.
// ┆ ┆┌─B─┐ There may be up to 2 reply rep lines in a row if it was created in the old format. Match all lines.
repliedToContent = repliedToContent . replace ( /^((-# )?> .*\n){1,2}/ , "" )
2023-10-12 13:05:44 +00:00
}
2023-07-12 02:33:38 +00:00
if ( repliedToContent == "" ) repliedToContent = "[Media]"
else if ( ! repliedToContent ) repliedToContent = "[Replied-to message content wasn't provided by Discord]"
2023-07-11 04:51:30 +00:00
const repliedToHtml = markdown . toHTML ( repliedToContent , {
2023-10-13 13:15:21 +00:00
discordCallback : getDiscordParseCallbacks ( message , guild , true )
2024-07-20 13:30:07 +00:00
} )
2023-07-11 04:51:30 +00:00
const repliedToBody = markdown . toHTML ( repliedToContent , {
2023-10-13 13:15:21 +00:00
discordCallback : getDiscordParseCallbacks ( message , guild , false ) ,
2023-07-11 04:51:30 +00:00
discordOnly : true ,
escapeHTML : false ,
2024-07-20 13:30:07 +00:00
} )
2023-09-14 00:32:27 +00:00
html = ` <mx-reply><blockquote><a href="https://matrix.to/#/ ${ repliedToEventRow . room _id } / ${ repliedToEventRow . event _id } ">In reply to</a> ${ repliedToUserHtml } `
2023-07-11 04:51:30 +00:00
+ ` <br> ${ repliedToHtml } </blockquote></mx-reply> `
+ html
body = ( ` ${ repliedToDisplayName } : ` // scenario 1 part B for mentions
+ repliedToBody ) . split ( "\n" ) . map ( line => "> " + line ) . join ( "\n" )
+ "\n\n" + body
}
const newTextMessageEvent = {
$type : "m.room.message" ,
"m.mentions" : mentions ,
2023-08-19 10:54:23 +00:00
msgtype ,
2023-07-11 04:51:30 +00:00
body : body
}
2023-07-01 13:41:31 +00:00
const isPlaintext = body === html
2023-07-07 05:31:39 +00:00
2023-07-11 04:51:30 +00:00
if ( ! isPlaintext ) {
Object . assign ( newTextMessageEvent , {
2023-07-01 13:41:31 +00:00
format : "org.matrix.custom.html" ,
formatted _body : html
} )
2023-05-12 05:35:37 +00:00
}
2023-07-11 04:51:30 +00:00
events . push ( newTextMessageEvent )
2023-04-25 20:06:08 +00:00
}
2023-05-12 05:35:37 +00:00
2023-10-01 10:55:42 +00:00
let msgtype = "m.text"
// Handle message type 4, channel name changed
if ( message . type === DiscordTypes . MessageType . ChannelNameChange ) {
msgtype = "m.emote"
message . content = "changed the channel name to **" + message . content + "**"
}
2024-03-15 02:54:13 +00:00
if ( message . content ) {
// Mentions scenario 3: scan the message content for written @mentions of matrix users. Allows for up to one space between @ and mention.
const matches = [ ... message . content . matchAll ( /@ ?([a-z0-9._]+)\b/gi ) ]
if ( matches . length && matches . some ( m => m [ 1 ] . match ( /[a-z]/i ) && m [ 1 ] !== "everyone" && m [ 1 ] !== "here" ) ) {
const writtenMentionsText = matches . map ( m => m [ 1 ] . toLowerCase ( ) )
const roomID = select ( "channel_room" , "room_id" , { channel _id : message . channel _id } ) . pluck ( ) . get ( )
assert ( roomID )
const { joined } = await di . api . getJoinedMembers ( roomID )
for ( const [ mxid , member ] of Object . entries ( joined ) ) {
if ( ! userRegex . some ( rx => mxid . match ( rx ) ) ) {
const localpart = mxid . match ( /@([^:]*)/ )
assert ( localpart )
const displayName = member . display _name || localpart [ 1 ]
if ( writtenMentionsText . includes ( localpart [ 1 ] . toLowerCase ( ) ) || writtenMentionsText . includes ( displayName . toLowerCase ( ) ) ) addMention ( mxid )
}
2023-10-27 11:24:42 +00:00
}
}
2024-03-15 02:54:13 +00:00
// Text content appears first
2023-10-27 11:24:42 +00:00
const { body , html } = await transformContent ( message . content )
await addTextEvent ( body , html , msgtype , { scanMentions : true } )
2023-10-01 10:55:42 +00:00
}
2023-05-12 05:35:37 +00:00
// Then attachments
2024-03-15 02:54:13 +00:00
if ( message . attachments ) {
const attachmentEvents = await Promise . all ( message . attachments . map ( attachmentToEvent . bind ( null , mentions ) ) )
events . push ( ... attachmentEvents )
}
2023-05-12 05:35:37 +00:00
2023-10-01 10:55:42 +00:00
// Then embeds
for ( const embed of message . embeds || [ ] ) {
if ( embed . type === "image" ) {
2024-01-16 03:00:33 +00:00
continue // Matrix's own URL previews are fine for images.
2023-10-01 10:55:42 +00:00
}
2024-03-25 05:05:19 +00:00
if ( embed . url ? . startsWith ( "https://discord.com/" ) ) {
continue // If discord creates an embed preview for a discord channel link, don't copy that embed
}
2023-10-01 10:55:42 +00:00
// Start building up a replica ("rep") of the embed in Discord-markdown format, which we will convert into both plaintext and formatted body at once
2023-10-27 11:24:42 +00:00
const rep = new mxUtils . MatrixStringBuilder ( )
2023-10-01 10:55:42 +00:00
2024-03-05 22:38:46 +00:00
// Provider
if ( embed . provider ? . name ) {
if ( embed . provider . url ) {
rep . addParagraph ( ` via ${ embed . provider . name } ${ embed . provider . url } ` , tag ` <sub><a href=" ${ embed . provider . url } "> ${ embed . provider . name } </a></sub> ` )
} else {
rep . addParagraph ( ` via ${ embed . provider . name } ` , tag ` <sub> ${ embed . provider . name } </sub> ` )
}
}
2023-10-27 11:24:42 +00:00
// Author and URL into a paragraph
2023-10-01 10:55:42 +00:00
let authorNameText = embed . author ? . name || ""
2023-10-27 11:24:42 +00:00
if ( authorNameText && embed . author ? . icon _url ) authorNameText = ` ⏺️ ${ authorNameText } ` // using the emoji instead of an image
2024-03-05 20:34:46 +00:00
if ( authorNameText ) {
2023-10-27 11:24:42 +00:00
if ( embed . author ? . url ) {
2023-11-23 03:11:46 +00:00
const authorURL = await transformContentMessageLinks ( embed . author . url )
2023-10-27 11:24:42 +00:00
rep . addParagraph ( ` ## ${ authorNameText } ${ authorURL } ` , tag ` <strong><a href=" ${ authorURL } "> ${ authorNameText } </a></strong> ` )
} else {
rep . addParagraph ( ` ## ${ authorNameText } ` , tag ` <strong> ${ authorNameText } </strong> ` )
}
}
2023-10-01 10:55:42 +00:00
2023-10-27 11:24:42 +00:00
// Title and URL into a paragraph
if ( embed . title ) {
const { body , html } = await transformContent ( embed . title , { } , embedTitleParser , markdown . htmlOutput )
if ( embed . url ) {
rep . addParagraph ( ` ## ${ body } ${ embed . url } ` , tag ` <strong><a href=" ${ embed . url } "> $ ${ html } </a></strong> ` )
} else {
rep . addParagraph ( ` ## ${ body } ` , ` <strong> ${ html } </strong> ` )
}
}
2023-10-01 10:55:42 +00:00
2024-03-05 22:38:46 +00:00
let embedTypeShouldShowDescription = embed . type !== "video" // Discord doesn't display descriptions for videos
if ( embed . provider ? . name === "YouTube" ) embedTypeShouldShowDescription = true // But I personally like showing the descriptions for YouTube videos specifically
if ( embed . description && embedTypeShouldShowDescription ) {
2023-10-27 11:24:42 +00:00
const { body , html } = await transformContent ( embed . description )
rep . addParagraph ( body , html )
}
2023-10-02 09:42:32 +00:00
2023-10-01 10:55:42 +00:00
for ( const field of embed . fields || [ ] ) {
2023-10-27 11:24:42 +00:00
const name = field . name . match ( /^[\s ]*$/ ) ? { body : "" , html : "" } : await transformContent ( field . name , { } , embedTitleParser , markdown . htmlOutput )
const value = await transformContent ( field . value )
const fieldRep = new mxUtils . MatrixStringBuilder ( )
. addLine ( ` ### ${ name . body } ` , ` <strong> ${ name . html } </strong> ` , name . body )
. addLine ( value . body , value . html , ! ! value . body )
rep . addParagraph ( fieldRep . get ( ) . body , fieldRep . get ( ) . formatted _body )
2023-10-01 10:55:42 +00:00
}
2023-10-27 11:24:42 +00:00
2024-03-05 22:38:46 +00:00
let chosenImage = embed . image ? . url
// the thumbnail seems to be used for "article" type but displayed big at the bottom by discord
if ( embed . type === "article" && embed . thumbnail ? . url && ! chosenImage ) chosenImage = embed . thumbnail . url
if ( chosenImage ) rep . addParagraph ( ` 📸 ${ chosenImage } ` )
2023-10-27 11:24:42 +00:00
if ( embed . video ? . url ) rep . addParagraph ( ` 🎞️ ${ embed . video . url } ` )
if ( embed . footer ? . text ) rep . addLine ( ` — ${ embed . footer . text } ` , tag ` — ${ embed . footer . text } ` )
let { body , formatted _body : html } = rep . get ( )
2024-02-12 02:39:04 +00:00
body = body . split ( "\n" ) . map ( l => "| " + l ) . join ( "\n" )
2023-10-27 11:24:42 +00:00
html = ` <blockquote> ${ html } </blockquote> `
2023-10-01 10:55:42 +00:00
// Send as m.notice to apply the usual automated/subtle appearance, showing this wasn't actually typed by the person
2023-10-27 11:24:42 +00:00
await addTextEvent ( body , html , "m.notice" , { scanMentions : false } )
2023-10-01 10:55:42 +00:00
}
2023-05-12 05:35:37 +00:00
// Then stickers
2023-06-28 12:06:56 +00:00
if ( message . sticker _items ) {
const stickerEvents = await Promise . all ( message . sticker _items . map ( async stickerItem => {
const format = file . stickerFormat . get ( stickerItem . format _type )
2024-01-18 23:38:25 +00:00
assert ( format ? . mime )
2023-09-10 09:35:51 +00:00
if ( format ? . mime === "lottie" ) {
2024-01-18 23:38:25 +00:00
const { mxc _url , info } = await lottie . convert ( stickerItem )
return {
$type : "m.sticker" ,
"m.mentions" : mentions ,
body : stickerItem . name ,
info ,
url : mxc _url
2023-09-10 09:35:51 +00:00
}
2024-01-18 23:38:25 +00:00
} else {
2023-06-28 12:06:56 +00:00
let body = stickerItem . name
const sticker = guild . stickers . find ( sticker => sticker . id === stickerItem . id )
if ( sticker && sticker . description ) body += ` - ${ sticker . description } `
return {
$type : "m.sticker" ,
2023-07-11 04:51:30 +00:00
"m.mentions" : mentions ,
2023-06-28 12:06:56 +00:00
body ,
info : {
mimetype : format . mime
} ,
url : await file . uploadDiscordFileToMxc ( file . sticker ( stickerItem ) )
}
2023-09-10 09:35:51 +00:00
}
2023-06-28 12:06:56 +00:00
} ) )
events . push ( ... stickerEvents )
}
2023-05-12 05:35:37 +00:00
2023-07-11 04:51:30 +00:00
// Rich replies
2023-09-14 00:32:27 +00:00
if ( repliedToEventRow ) {
2023-07-11 04:51:30 +00:00
Object . assign ( events [ 0 ] , {
"m.relates_to" : {
"m.in_reply_to" : {
2023-09-14 00:32:27 +00:00
event _id : repliedToEventRow . event _id
2023-07-11 04:51:30 +00:00
}
}
} )
}
2023-05-12 05:35:37 +00:00
return events
2023-04-25 20:06:08 +00:00
}
2023-05-05 05:29:08 +00:00
2023-05-08 11:37:51 +00:00
module . exports . messageToEvent = messageToEvent