2023-07-02 13:06:05 +00:00
// @ts-check
2023-07-04 05:19:17 +00:00
const Ty = require ( "../../types" )
2023-07-02 13:06:05 +00:00
const DiscordTypes = require ( "discord-api-types/v10" )
2023-08-25 13:43:17 +00:00
const chunk = require ( "chunk-text" )
const TurndownService = require ( "turndown" )
2023-09-02 11:28:41 +00:00
const assert = require ( "assert" ) . strict
2023-07-02 13:06:05 +00:00
const passthrough = require ( "../../passthrough" )
2023-09-18 10:51:59 +00:00
const { sync , db , discord , select , from } = passthrough
2023-07-02 13:06:05 +00:00
/** @type {import("../../matrix/file")} */
const file = sync . require ( "../../matrix/file" )
2023-08-26 10:22:54 +00:00
/** @type {import("../converters/utils")} */
const utils = sync . require ( "../converters/utils" )
2023-07-02 13:06:05 +00:00
2023-08-25 13:43:17 +00:00
const BLOCK _ELEMENTS = [
"ADDRESS" , "ARTICLE" , "ASIDE" , "AUDIO" , "BLOCKQUOTE" , "BODY" , "CANVAS" ,
"CENTER" , "DD" , "DETAILS" , "DIR" , "DIV" , "DL" , "DT" , "FIELDSET" , "FIGCAPTION" , "FIGURE" ,
"FOOTER" , "FORM" , "FRAMESET" , "H1" , "H2" , "H3" , "H4" , "H5" , "H6" , "HEADER" ,
"HGROUP" , "HR" , "HTML" , "ISINDEX" , "LI" , "MAIN" , "MENU" , "NAV" , "NOFRAMES" ,
"NOSCRIPT" , "OL" , "OUTPUT" , "P" , "PRE" , "SECTION" , "SUMMARY" , "TABLE" , "TBODY" , "TD" ,
"TFOOT" , "TH" , "THEAD" , "TR" , "UL"
]
2023-09-03 13:37:33 +00:00
/** @type {[RegExp, string][]} */
const markdownEscapes = [
[ /\\/g , '\\\\' ] ,
[ /\*/g , '\\*' ] ,
[ /^-/g , '\\-' ] ,
[ /^\+ /g , '\\+ ' ] ,
[ /^(=+)/g , '\\$1' ] ,
[ /^(#{1,6}) /g , '\\$1 ' ] ,
[ /`/g , '\\`' ] ,
[ /^~~~/g , '\\~~~' ] ,
[ /\[/g , '\\[' ] ,
[ /\]/g , '\\]' ] ,
[ /^>/g , '\\>' ] ,
[ /_/g , '\\_' ] ,
[ /^(\d+)\. /g , '$1\\. ' ]
]
2023-08-25 13:43:17 +00:00
const turndownService = new TurndownService ( {
2023-08-25 14:04:49 +00:00
hr : "----" ,
headingStyle : "atx" ,
preformattedCode : true ,
2023-09-03 13:37:33 +00:00
codeBlockStyle : "fenced" ,
2023-08-25 13:43:17 +00:00
} )
2023-09-03 13:37:33 +00:00
/ * *
* Markdown characters in the HTML content need to be escaped , though take care not to escape the middle of bare links
* @ param { string } string
* /
// @ts-ignore bad type from turndown
turndownService . escape = function ( string ) {
const escapedWords = string . split ( " " ) . map ( word => {
if ( word . match ( /^https?:\/\// ) ) {
return word
} else {
return markdownEscapes . reduce ( function ( accumulator , escape ) {
return accumulator . replace ( escape [ 0 ] , escape [ 1 ] )
} , word )
}
} )
return escapedWords . join ( " " )
}
2023-08-26 08:30:22 +00:00
turndownService . remove ( "mx-reply" )
2023-08-25 13:43:17 +00:00
turndownService . addRule ( "strikethrough" , {
2023-08-30 03:20:39 +00:00
filter : [ "del" , "s" ] ,
2023-08-25 13:43:17 +00:00
replacement : function ( content ) {
return "~~" + content + "~~"
}
} )
2023-08-26 10:59:22 +00:00
turndownService . addRule ( "underline" , {
filter : [ "u" ] ,
replacement : function ( content ) {
return "__" + content + "__"
}
} )
2023-08-26 07:07:19 +00:00
turndownService . addRule ( "blockquote" , {
filter : "blockquote" ,
replacement : function ( content ) {
content = content . replace ( /^\n+|\n+$/g , "" )
content = content . replace ( /^/gm , "> " )
return content
}
} )
2023-08-28 13:36:15 +00:00
turndownService . addRule ( "spoiler" , {
filter : function ( node , options ) {
return node . hasAttribute ( "data-mx-spoiler" )
} ,
replacement : function ( content , node ) {
return "||" + content + "||"
}
} )
2023-08-26 11:22:23 +00:00
turndownService . addRule ( "inlineLink" , {
filter : function ( node , options ) {
2023-08-30 03:20:39 +00:00
return (
2023-08-26 11:22:23 +00:00
node . nodeName === "A" &&
node . getAttribute ( "href" )
2023-08-30 03:20:39 +00:00
)
2023-08-26 11:22:23 +00:00
} ,
replacement : function ( content , node ) {
if ( node . getAttribute ( "data-user-id" ) ) return ` <@ ${ node . getAttribute ( "data-user-id" ) } > `
if ( node . getAttribute ( "data-channel-id" ) ) return ` <# ${ node . getAttribute ( "data-channel-id" ) } > `
const href = node . getAttribute ( "href" )
let brackets = [ "" , "" ]
if ( href . startsWith ( "https://matrix.to" ) ) brackets = [ "<" , ">" ]
2023-08-30 01:29:16 +00:00
return "[" + content + "](" + brackets [ 0 ] + href + brackets [ 1 ] + ")"
2023-08-26 11:22:23 +00:00
}
} )
2023-09-19 07:59:58 +00:00
turndownService . addRule ( "emoji" , {
filter : function ( node , options ) {
if ( node . nodeName !== "IMG" || ! node . hasAttribute ( "data-mx-emoticon" ) || ! node . getAttribute ( "src" ) ) return false
2023-09-19 12:37:15 +00:00
let row = select ( "emoji" , [ "id" , "name" , "animated" ] , "WHERE mxc_url = ?" ) . get ( node . getAttribute ( "src" ) )
if ( ! row ) {
// We don't know what this is... but maybe we can guess based on the name?
const guessedName = node . getAttribute ( "title" ) ? . replace ? . ( /^:|:$/g , "" )
if ( ! guessedName ) return false
for ( const guild of discord . guilds . values ( ) ) {
/** @type {{name: string, id: string, animated: number}[]} */
// @ts-ignore
const emojis = guild . emojis
const match = emojis . find ( e => e . name === guessedName ) || emojis . find ( e => e . name ? . toLowerCase ( ) === guessedName . toLowerCase ( ) )
if ( match ) {
row = match
break
}
}
}
2023-09-19 07:59:58 +00:00
if ( ! row ) return false
2023-09-19 12:37:15 +00:00
node . setAttribute ( "data-emoji-id" , row . id )
node . setAttribute ( "data-emoji-name" , row . name )
2023-09-19 07:59:58 +00:00
node . setAttribute ( "data-emoji-animated-char" , row . animated ? "a" : "" )
return true
} ,
replacement : function ( content , node ) {
/** @type {string} */
const id = node . getAttribute ( "data-emoji-id" )
/** @type {string} */
const animatedChar = node . getAttribute ( "data-emoji-animated-char" )
/** @type {string} */
2023-09-19 12:37:15 +00:00
const name = node . getAttribute ( "data-emoji-name" )
2023-09-19 07:59:58 +00:00
return ` < ${ animatedChar } : ${ name } : ${ id } > `
}
} )
2023-08-26 07:07:19 +00:00
turndownService . addRule ( "fencedCodeBlock" , {
filter : function ( node , options ) {
return (
options . codeBlockStyle === "fenced" &&
node . nodeName === "PRE" &&
node . firstChild &&
node . firstChild . nodeName === "CODE"
)
} ,
replacement : function ( content , node , options ) {
const className = node . firstChild . getAttribute ( "class" ) || ""
const language = ( className . match ( /language-(\S+)/ ) || [ null , "" ] ) [ 1 ]
const code = node . firstChild
const visibleCode = code . childNodes . map ( c => c . nodeName === "BR" ? "\n" : c . textContent ) . join ( "" ) . replace ( /\n*$/g , "" )
var fence = "```"
return (
fence + language + "\n" +
visibleCode +
"\n" + fence
)
}
} )
2023-08-26 10:22:54 +00:00
/ * *
* @ param { string } roomID
* @ param { string } mxid
* @ returns { Promise < { displayname ? : string ? , avatar _url ? : string ? } > }
* /
async function getMemberFromCacheOrHomeserver ( roomID , mxid , api ) {
2023-09-18 10:51:59 +00:00
const row = select ( "member_cache" , [ "displayname" , "avatar_url" ] , "WHERE room_id = ? AND mxid = ?" ) . get ( roomID , mxid )
2023-08-26 10:22:54 +00:00
if ( row ) return row
return api . getStateEvent ( roomID , "m.room.member" , mxid ) . then ( event => {
2023-08-26 10:50:54 +00:00
db . prepare ( "REPLACE INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES (?, ?, ?, ?)" ) . run ( roomID , mxid , event ? . displayname || null , event ? . avatar _url || null )
2023-08-26 10:22:54 +00:00
return event
} ) . catch ( ( ) => {
return { displayname : null , avatar _url : null }
} )
}
2023-09-10 02:59:31 +00:00
2023-09-10 01:47:24 +00:00
/ * *
* Splits a display name into one chunk containing <= 80 characters , and another chunk containing the rest of the characters . Splits on
* whitespace if possible .
* These chunks , respectively , go in the display name , and at the top of the message .
* If the second part isn 't empty, it' ll also contain boldening markdown and a line break at the end , so that regardless of its value it
* can be prepended to the message content as - is .
* @ summary Splits too - long Matrix names into a display name chunk and a message content chunk .
* @ param { string } displayName - The Matrix side display name to chop up .
2023-09-10 02:59:31 +00:00
* @ returns { [ string , string ] } [ shortened display name , display name runoff ]
2023-09-10 01:47:24 +00:00
* /
function splitDisplayName ( displayName ) {
/** @type {string[]} */
let displayNameChunks = chunk ( displayName , 80 )
if ( displayNameChunks . length === 1 ) {
return [ displayName , "" ]
} else {
const displayNamePreRunoff = displayNameChunks [ 0 ]
2023-09-10 02:59:31 +00:00
// displayNameRunoff is a slice of the original rather than a concatenation of the rest of the chunks in order to preserve whatever whitespace it was broken on.
const displayNameRunoff = ` ** ${ displayName . slice ( displayNamePreRunoff . length + 1 ) } ** \n `
2023-09-10 01:47:24 +00:00
return [ displayNamePreRunoff , displayNameRunoff ]
}
}
2023-08-26 10:22:54 +00:00
2023-07-02 13:06:05 +00:00
/ * *
2023-09-03 03:40:25 +00:00
* @ param { Ty . Event . Outer _M _Room _Message | Ty . Event . Outer _M _Room _Message _File | Ty . Event . Outer _M _Sticker | Ty . Event . Outer _M _Room _Message _Encrypted _File } event
2023-08-26 08:30:22 +00:00
* @ param { import ( "discord-api-types/v10" ) . APIGuild } guild
* @ param { { api : import ( "../../matrix/api" ) } } di simple - as - nails dependency injection for the matrix API
2023-07-02 13:06:05 +00:00
* /
2023-08-26 08:30:22 +00:00
async function eventToMessage ( event , guild , di ) {
2023-07-02 13:06:05 +00:00
/** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]})[]} */
2023-08-25 13:43:17 +00:00
let messages = [ ]
2023-07-02 13:06:05 +00:00
2023-08-19 06:37:34 +00:00
let displayName = event . sender
let avatarURL = undefined
2023-08-27 13:30:07 +00:00
/** @type {string[]} */
let messageIDsToEdit = [ ]
2023-08-26 08:30:22 +00:00
let replyLine = ""
2023-08-26 10:22:54 +00:00
// Extract a basic display name from the sender
2023-08-19 06:37:34 +00:00
const match = event . sender . match ( /^@(.*?):/ )
2023-08-26 10:22:54 +00:00
if ( match ) displayName = match [ 1 ]
// Try to extract an accurate display name and avatar URL from the member event
const member = await getMemberFromCacheOrHomeserver ( event . room _id , event . sender , di ? . api )
if ( member . displayname ) displayName = member . displayname
2023-09-02 11:28:41 +00:00
if ( member . avatar _url ) avatarURL = utils . getPublicUrlForMxc ( member . avatar _url ) || undefined
2023-09-10 02:59:31 +00:00
// If the display name is too long to be put into the webhook (80 characters is the maximum),
// put the excess characters into displayNameRunoff, later to be put at the top of the message
let [ displayNameShortened , displayNameRunoff ] = splitDisplayName ( displayName )
2023-09-10 01:47:24 +00:00
// If the message type is m.emote, the full name is already included at the start of the message, so remove any runoff
if ( event . type === "m.room.message" && event . content . msgtype === "m.emote" ) {
displayNameRunoff = ""
}
2023-08-19 06:37:34 +00:00
2023-08-25 13:43:17 +00:00
let content = event . content . body // ultimate fallback
2023-09-02 11:28:41 +00:00
const attachments = [ ]
2023-09-03 03:40:25 +00:00
/** @type {({name: string, url: string} | {name: string, url: string, key: string, iv: string})[]} */
2023-09-02 11:28:41 +00:00
const pendingFiles = [ ]
2023-08-25 13:43:17 +00:00
2023-08-28 13:31:52 +00:00
// Convert content depending on what the message is
2023-09-02 11:28:41 +00:00
if ( event . type === "m.room.message" && ( event . content . msgtype === "m.text" || event . content . msgtype === "m.emote" ) ) {
2023-08-27 13:30:07 +00:00
// Handling edits. If the edit was an edit of a reply, edits do not include the reply reference, so we need to fetch up to 2 more events.
// this event ---is an edit of--> original event ---is a reply to--> past event
await ( async ( ) => {
if ( ! event . content [ "m.new_content" ] ) return
const relatesTo = event . content [ "m.relates_to" ]
if ( ! relatesTo ) return
// Check if we have a pointer to what was edited
const relType = relatesTo . rel _type
if ( relType !== "m.replace" ) return
const originalEventId = relatesTo . event _id
if ( ! originalEventId ) return
2023-09-18 10:51:59 +00:00
messageIDsToEdit = select ( "event_message" , "message_id" , "WHERE event_id = ? ORDER BY part" ) . pluck ( ) . all ( originalEventId )
2023-08-27 13:30:07 +00:00
if ( ! messageIDsToEdit . length ) return
2023-08-28 13:31:52 +00:00
// Ok, it's an edit.
event . content = event . content [ "m.new_content" ]
// Is it editing a reply? We need special handling if it is.
2023-08-27 13:30:07 +00:00
// Get the original event, then check if it was a reply
const originalEvent = await di . api . getEvent ( event . room _id , originalEventId )
if ( ! originalEvent ) return
const repliedToEventId = originalEvent . content [ "m.relates_to" ] ? . [ "m.in_reply_to" ] ? . event _id
if ( ! repliedToEventId ) return
2023-08-28 13:31:52 +00:00
2023-08-27 13:30:07 +00:00
// After all that, it's an edit of a reply.
2023-08-28 13:31:52 +00:00
// We'll be sneaky and prepare the message data so that the next steps can handle it just like original messages.
Object . assign ( event . content , {
"m.relates_to" : {
"m.in_reply_to" : {
event _id : repliedToEventId
}
}
} )
2023-08-27 13:30:07 +00:00
} ) ( )
2023-08-26 08:30:22 +00:00
// Handling replies. We'll look up the data of the replied-to event from the Matrix homeserver.
2023-08-27 13:30:07 +00:00
// Note that an <mx-reply> element is not guaranteed because this might be m.new_content.
2023-08-26 08:30:22 +00:00
await ( async ( ) => {
2023-08-27 13:30:07 +00:00
const repliedToEventId = event . content [ "m.relates_to" ] ? . [ "m.in_reply_to" ] ? . event _id
2023-08-26 08:30:22 +00:00
if ( ! repliedToEventId ) return
2023-09-07 12:13:25 +00:00
let repliedToEvent = await di . api . getEvent ( event . room _id , repliedToEventId )
2023-08-26 08:30:22 +00:00
if ( ! repliedToEvent ) return
2023-09-18 10:51:59 +00:00
const row = from ( "event_message" ) . join ( "message_channel" , "message_id" ) . select ( "channel_id" , "message_id" ) . and ( "WHERE event_id = ? ORDER BY part" ) . get ( repliedToEventId )
2023-08-26 08:30:22 +00:00
if ( row ) {
replyLine = ` <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/ ${ guild . id } / ${ row . channel _id } / ${ row . message _id } `
} else {
replyLine = ` <:L1:1144820033948762203><:L2:1144820084079087647> `
}
const sender = repliedToEvent . sender
const senderName = sender . match ( /@([^:]*)/ ) ? . [ 1 ] || sender
2023-09-18 10:51:59 +00:00
const authorID = select ( "sim" , "discord_id" , "WHERE mxid = ?" ) . pluck ( ) . get ( repliedToEvent . sender )
2023-08-26 08:30:22 +00:00
if ( authorID ) {
2023-09-07 11:48:44 +00:00
replyLine += ` <@ ${ authorID } > `
2023-08-26 08:30:22 +00:00
} else {
2023-09-07 11:48:44 +00:00
replyLine += ` Ⓜ️** ${ senderName } ** `
2023-08-26 08:30:22 +00:00
}
2023-09-07 12:13:25 +00:00
// If the event has been edited, the homeserver will include the relation in `unsigned`.
if ( repliedToEvent . unsigned ? . [ "m.relations" ] ? . [ "m.replace" ] ? . content ? . [ "m.new_content" ] ) {
repliedToEvent = repliedToEvent . unsigned [ "m.relations" ] [ "m.replace" ] // Note: this changes which event_id is in repliedToEvent.
repliedToEvent . content = repliedToEvent . content [ "m.new_content" ]
}
2023-09-07 11:48:44 +00:00
let contentPreview
const fileReplyContentAlternative =
( repliedToEvent . content . msgtype === "m.image" ? "🖼️"
: repliedToEvent . content . msgtype === "m.video" ? "🎞️"
: repliedToEvent . content . msgtype === "m.audio" ? "🎶"
: repliedToEvent . content . msgtype === "m.file" ? "📄"
: null )
if ( fileReplyContentAlternative ) {
contentPreview = " " + fileReplyContentAlternative
} else {
2023-09-10 02:59:31 +00:00
const repliedToContent = repliedToEvent . content . formatted _body || repliedToEvent . content . body
2023-09-07 12:13:25 +00:00
const contentPreviewChunks = chunk (
repliedToContent . replace ( /.*<\/mx-reply>/ , "" ) // Remove everything before replies, so just use the actual message body
. replace ( /.*?<\/blockquote>/ , "" ) // If the message starts with a blockquote, don't count it and use the message body afterwards
. replace ( /(?:\n|<br>)+/g , " " ) // Should all be on one line
. replace ( /<span [^>]*data-mx-spoiler\b[^>]*>.*?<\/span>/g , "[spoiler]" ) // Good enough method of removing spoiler content. (I don't want to break out the HTML parser unless I have to.)
. replace ( /<[^>]+>/g , "" ) , 50 ) // Completely strip all other formatting.
2023-09-07 11:48:44 +00:00
contentPreview = ":\n> "
contentPreview += contentPreviewChunks . length > 1 ? contentPreviewChunks [ 0 ] + "..." : contentPreviewChunks [ 0 ]
}
replyLine = ` > ${ replyLine } ${ contentPreview } \n `
2023-08-26 08:30:22 +00:00
} ) ( )
2023-08-28 13:31:52 +00:00
if ( event . content . format === "org.matrix.custom.html" && event . content . formatted _body ) {
let input = event . content . formatted _body
if ( event . content . msgtype === "m.emote" ) {
input = ` * ${ displayName } ${ input } `
2023-08-25 13:43:17 +00:00
}
2023-08-28 13:31:52 +00:00
// Handling mentions of Discord users
input = input . replace ( /("https:\/\/matrix.to\/#\/(@[^"]+)")>/g , ( whole , attributeValue , mxid ) => {
if ( ! utils . eventSenderIsFromDiscord ( mxid ) ) return whole
2023-09-18 10:51:59 +00:00
const userID = select ( "sim" , "discord_id" , "WHERE mxid = ?" ) . pluck ( ) . get ( mxid )
2023-08-28 13:31:52 +00:00
if ( ! userID ) return whole
return ` ${ attributeValue } data-user-id=" ${ userID } "> `
} )
// Handling mentions of Discord rooms
input = input . replace ( /("https:\/\/matrix.to\/#\/(![^"]+)")>/g , ( whole , attributeValue , roomID ) => {
2023-09-18 10:51:59 +00:00
const channelID = select ( "channel_room" , "channel_id" , "WHERE room_id = ?" ) . pluck ( ) . get ( roomID )
2023-08-28 13:31:52 +00:00
if ( ! channelID ) return whole
return ` ${ attributeValue } data-channel-id=" ${ channelID } "> `
} )
// Element adds a bunch of <br> before </blockquote> but doesn't render them. I can't figure out how this even works in the browser, so let's just delete those.
input = input . replace ( /(?:\n|<br ?\/?>\s*)*<\/blockquote>/g , "</blockquote>" )
// The matrix spec hasn't decided whether \n counts as a newline or not, but I'm going to count it, because if it's in the data it's there for a reason.
// But I should not count it if it's between block elements.
input = input . replace ( /(<\/?([^ >]+)[^>]*>)?\n(<\/?([^ >]+)[^>]*>)?/g , ( whole , beforeContext , beforeTag , afterContext , afterTag ) => {
// console.error(beforeContext, beforeTag, afterContext, afterTag)
if ( typeof beforeTag !== "string" && typeof afterTag !== "string" ) {
return "<br>"
}
beforeContext = beforeContext || ""
beforeTag = beforeTag || ""
afterContext = afterContext || ""
afterTag = afterTag || ""
if ( ! BLOCK _ELEMENTS . includes ( beforeTag . toUpperCase ( ) ) && ! BLOCK _ELEMENTS . includes ( afterTag . toUpperCase ( ) ) ) {
return beforeContext + "<br>" + afterContext
} else {
return whole
}
} )
// Note: Element's renderers on Web and Android currently collapse whitespace, like the browser does. Turndown also collapses whitespace which is good for me.
// If later I'm using a client that doesn't collapse whitespace and I want turndown to follow suit, uncomment the following line of code, and it Just Works:
// input = input.replace(/ /g, " ")
// There is also a corresponding test to uncomment, named "event2message: whitespace is retained"
// @ts-ignore bad type from turndown
content = turndownService . turndown ( input )
2023-09-02 11:28:41 +00:00
// It's designed for commonmark, we need to replace the space-space-newline with just newline
2023-08-28 13:31:52 +00:00
content = content . replace ( / \n/g , "\n" )
} else {
// Looks like we're using the plaintext body!
content = event . content . body
if ( event . content . msgtype === "m.emote" ) {
content = ` * ${ displayName } ${ content } `
}
2023-08-25 13:43:17 +00:00
2023-09-03 13:37:33 +00:00
// Markdown needs to be escaped, though take care not to escape the middle of links
// @ts-ignore bad type from turndown
content = turndownService . escape ( content )
2023-08-28 13:31:52 +00:00
}
2023-09-02 11:28:41 +00:00
} else if ( event . type === "m.room.message" && ( event . content . msgtype === "m.file" || event . content . msgtype === "m.video" || event . content . msgtype === "m.audio" || event . content . msgtype === "m.image" ) ) {
content = ""
const filename = event . content . body
2023-09-03 03:40:25 +00:00
if ( "url" in event . content ) {
// Unencrypted
const url = utils . getPublicUrlForMxc ( event . content . url )
assert ( url )
attachments . push ( { id : "0" , filename } )
pendingFiles . push ( { name : filename , url } )
} else {
// Encrypted
const url = utils . getPublicUrlForMxc ( event . content . file . url )
assert ( url )
assert . equal ( event . content . file . key . alg , "A256CTR" )
attachments . push ( { id : "0" , filename } )
pendingFiles . push ( { name : filename , url , key : event . content . file . key . k , iv : event . content . file . iv } )
}
2023-09-02 11:28:41 +00:00
} else if ( event . type === "m.sticker" ) {
content = ""
let filename = event . content . body
if ( event . type === "m.sticker" && event . content . info . mimetype . includes ( "/" ) ) {
filename += "." + event . content . info . mimetype . split ( "/" ) [ 1 ]
}
const url = utils . getPublicUrlForMxc ( event . content . url )
assert ( url )
attachments . push ( { id : "0" , filename } )
pendingFiles . push ( { name : filename , url } )
2023-07-02 13:06:05 +00:00
}
2023-09-10 01:47:24 +00:00
content = displayNameRunoff + replyLine + content
2023-08-26 08:30:22 +00:00
2023-08-25 13:43:17 +00:00
// Split into 2000 character chunks
const chunks = chunk ( content , 2000 )
messages = messages . concat ( chunks . map ( content => ( {
content ,
2023-09-10 01:47:24 +00:00
username : displayNameShortened ,
2023-08-25 13:43:17 +00:00
avatar _url : avatarURL
} ) ) )
2023-09-02 11:28:41 +00:00
if ( attachments . length ) {
// If content is empty (should be the case when uploading a file) then chunk-text will create 0 messages.
// There needs to be a message to add attachments to.
if ( ! messages . length ) messages . push ( {
content ,
2023-09-10 01:47:24 +00:00
username : displayNameShortened ,
2023-09-02 11:28:41 +00:00
avatar _url : avatarURL
} )
messages [ 0 ] . attachments = attachments
// @ts-ignore these will be converted to real files when the message is about to be sent
messages [ 0 ] . pendingFiles = pendingFiles
}
2023-08-27 13:30:07 +00:00
const messagesToEdit = [ ]
const messagesToSend = [ ]
for ( let i = 0 ; i < messages . length ; i ++ ) {
2023-08-28 13:31:52 +00:00
const next = messageIDsToEdit [ 0 ]
if ( next ) {
messagesToEdit . push ( { id : next , message : messages [ i ] } )
messageIDsToEdit . shift ( )
2023-08-27 13:30:07 +00:00
} else {
messagesToSend . push ( messages [ i ] )
}
}
2023-08-30 01:29:16 +00:00
// Ensure there is code coverage for adding, editing, and deleting
if ( messagesToSend . length ) void 0
if ( messagesToEdit . length ) void 0
if ( messageIDsToEdit . length ) void 0
2023-08-27 13:30:07 +00:00
return {
messagesToEdit ,
messagesToSend ,
messagesToDelete : messageIDsToEdit
}
2023-07-02 13:06:05 +00:00
}
module . exports . eventToMessage = eventToMessage