2023-04-25 20:06:08 +00:00
// @ts-check
2023-06-28 12:06:56 +00:00
const assert = require ( "assert" ) . strict
2023-04-25 20:06:08 +00:00
const markdown = require ( "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" )
2023-04-25 20:06:08 +00:00
2023-05-12 05:35:37 +00:00
const passthrough = require ( "../../passthrough" )
2023-06-28 12:06:56 +00:00
const { sync , db , discord } = passthrough
2023-05-12 05:35:37 +00:00
/** @type {import("../../matrix/file")} */
const file = sync . require ( "../../matrix/file" )
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-07-07 05:31:39 +00:00
function getDiscordParseCallbacks ( message , useHTML ) {
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 => {
const mxid = db . prepare ( "SELECT mxid FROM sim WHERE discord_id = ?" ) . pluck ( ) . get ( node . id )
const username = message . mentions . find ( ment => ment . id === node . id ) ? . username || node . id
if ( mxid && useHTML ) {
return ` <a href="https://matrix.to/#/ ${ mxid } ">@ ${ username } </a> `
} else {
return ` @ ${ username } : `
}
} ,
2023-08-16 05:03:05 +00:00
/** @param {{id: string, type: "discordChannel"}} node */
2023-07-07 05:31:39 +00:00
channel : node => {
2023-07-10 22:10:47 +00:00
const { room _id , name , nick } = db . prepare ( "SELECT room_id, name, nick FROM channel_room WHERE channel_id = ?" ) . get ( node . id )
if ( room _id && useHTML ) {
return ` <a href="https://matrix.to/#/ ${ room _id } "># ${ nick || name } </a> `
2023-07-07 05:31:39 +00:00
} else {
2023-07-10 22:10:47 +00:00
return ` # ${ nick || 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 ) {
// TODO: upload the emoji and actually use the right mxc!!
return ` <img src="mxc://cadence.moe/ ${ node . id } " data-mx-emoticon alt=": ${ node . name } :" title=": ${ node . name } :" height="24"> `
} else {
return ` : ${ node . name } : `
}
} ,
2023-07-07 05:31:39 +00:00
role : node =>
"@&" + node . id ,
everyone : node =>
"@room" ,
here : node =>
"@here"
}
}
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 = [ ]
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 = { }
let repliedToEventId = null
let repliedToEventRoomId = null
let repliedToEventSenderMxid = null
let repliedToEventOriginallyFromMatrix = false
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 ) {
const row = db . prepare ( "SELECT event_id, room_id, source FROM event_message INNER JOIN channel_room USING (channel_id) WHERE message_id = ? AND part = 0" ) . get ( message . message _reference . message _id )
if ( row ) {
repliedToEventId = row . event _id
repliedToEventRoomId = row . room _id
repliedToEventOriginallyFromMatrix = row . source === 0 // source 0 = matrix
}
}
if ( repliedToEventOriginallyFromMatrix ) {
// Need to figure out who sent that event...
2023-08-16 08:44:38 +00:00
const event = await di . api . getEvent ( repliedToEventRoomId , repliedToEventId )
2023-07-11 04:51:30 +00:00
repliedToEventSenderMxid = event . sender
// Need to add the sender to m.mentions
addMention ( repliedToEventSenderMxid )
}
2023-05-12 05:35:37 +00:00
// Text content appears first
2023-07-01 13:41:31 +00:00
if ( message . content ) {
2023-07-10 20:01:11 +00:00
let content = message . content
content = content . replace ( /https:\/\/(?:ptb\.|canary\.|www\.)?discord(?:app)?\.com\/channels\/([0-9]+)\/([0-9]+)\/([0-9]+)/ , ( whole , guildID , channelID , messageID ) => {
const row = db . prepare ( "SELECT room_id, event_id FROM event_message INNER JOIN channel_room USING (channel_id) WHERE channel_id = ? AND message_id = ? AND part = 0" ) . get ( channelID , messageID )
if ( row ) {
return ` https://matrix.to/#/ ${ row . room _id } / ${ row . event _id } `
} else {
return ` ${ whole } [event not found] `
}
} )
2023-07-11 04:51:30 +00:00
let html = markdown . toHTML ( content , {
2023-07-07 05:31:39 +00:00
discordCallback : getDiscordParseCallbacks ( message , true )
2023-07-01 13:41:31 +00:00
} , null , null )
2023-07-07 05:31:39 +00:00
2023-07-11 05:27:40 +00:00
// TODO: add a string return type to my discord-markdown library
2023-07-11 04:51:30 +00:00
let body = markdown . toHTML ( content , {
2023-07-10 20:01:11 +00:00
discordCallback : getDiscordParseCallbacks ( message , false ) ,
2023-07-07 05:31:39 +00:00
discordOnly : true ,
escapeHTML : false ,
} , null , null )
2023-07-11 05:27:40 +00:00
// Mentions scenario 3: scan the message content for written @mentions of matrix users
const matches = [ ... content . matchAll ( /@([a-z0-9._]+)\b/gi ) ]
if ( matches . length && matches . some ( m => m [ 1 ] . match ( /[a-z]/i ) ) ) {
const writtenMentionsText = matches . map ( m => m [ 1 ] . toLowerCase ( ) )
const roomID = db . prepare ( "SELECT room_id FROM channel_room WHERE channel_id = ?" ) . pluck ( ) . get ( message . channel _id )
2023-08-16 08:44:38 +00:00
const { joined } = await di . api . getJoinedMembers ( roomID )
2023-07-11 05:27:40 +00:00
for ( const [ mxid , member ] of Object . entries ( joined ) ) {
if ( ! userRegex . some ( rx => mxid . match ( rx ) ) ) {
const localpart = mxid . match ( /@([^:]*)/ )
assert ( localpart )
if ( writtenMentionsText . includes ( localpart [ 1 ] . toLowerCase ( ) ) || writtenMentionsText . includes ( member . display _name . toLowerCase ( ) ) ) addMention ( mxid )
}
}
}
2023-08-16 08:44:38 +00:00
// Star * prefix for fallback edits
if ( options . includeEditFallbackStar ) {
body = "* " + body
html = "* " + 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
if ( repliedToEventId && options . includeReplyFallback !== false ) {
2023-07-11 04:51:30 +00:00
let repliedToDisplayName
let repliedToUserHtml
if ( repliedToEventOriginallyFromMatrix && repliedToEventSenderMxid ) {
const match = repliedToEventSenderMxid . match ( /^@([^:]*)/ )
assert ( match )
repliedToDisplayName = match [ 1 ] || "a Matrix user" // grab the localpart as the display name, whatever
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
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 , {
discordCallback : getDiscordParseCallbacks ( message , true )
} , null , null )
const repliedToBody = markdown . toHTML ( repliedToContent , {
discordCallback : getDiscordParseCallbacks ( message , false ) ,
discordOnly : true ,
escapeHTML : false ,
} , null , null )
html = ` <mx-reply><blockquote><a href="https://matrix.to/#/ ${ repliedToEventRoomId } / ${ repliedToEventId } ">In reply to</a> ${ repliedToUserHtml } `
+ ` <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 ,
msgtype : "m.text" ,
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
// Then attachments
const attachmentEvents = await Promise . all ( message . attachments . map ( async attachment => {
2023-07-13 05:36:20 +00:00
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/" ) ? "🎶"
: "📄"
// for large files, always link them instead of uploading so I don't use up all the space in the content repo
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 ) {
2023-05-12 05:35:37 +00:00
return {
$type : "m.room.message" ,
2023-07-11 04:51:30 +00:00
"m.mentions" : mentions ,
2023-05-12 05:35:37 +00:00
msgtype : "m.image" ,
url : await file . uploadDiscordFileToMxc ( attachment . url ) ,
external _url : attachment . url ,
body : attachment . filename ,
// TODO: filename: attachment.filename and then use body as the caption
info : {
mimetype : attachment . content _type ,
w : attachment . width ,
h : attachment . height ,
size : attachment . size
}
}
} else {
return {
$type : "m.room.message" ,
2023-07-11 04:51:30 +00:00
"m.mentions" : mentions ,
2023-05-12 05:35:37 +00:00
msgtype : "m.text" ,
2023-07-13 05:36:20 +00:00
body : ` Unsupported attachment: \n ${ JSON . stringify ( attachment , null , 2 ) } \n ${ attachment . url } `
2023-05-12 05:35:37 +00:00
}
}
} ) )
events . push ( ... attachmentEvents )
// 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 )
if ( format ? . mime ) {
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 ) )
}
} else {
return {
$type : "m.room.message" ,
2023-07-11 04:51:30 +00:00
"m.mentions" : mentions ,
2023-06-28 12:06:56 +00:00
msgtype : "m.text" ,
body : "Unsupported sticker format. Name: " + stickerItem . name
}
}
} ) )
events . push ( ... stickerEvents )
}
2023-05-12 05:35:37 +00:00
2023-07-11 04:51:30 +00:00
// Rich replies
if ( repliedToEventId ) {
Object . assign ( events [ 0 ] , {
"m.relates_to" : {
"m.in_reply_to" : {
event _id : repliedToEventId
}
}
} )
}
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