, I respond: Stop drinking paint. (No), I respond: Stop drinking paint. (No), I respond: Stop drinking paint. (No), I respond: Stop drinking paint. (No)In reply to Extremity' + + '
Image
↪️ Brad used /stats — interaction loading...",
- "m.mentions": {},
- msgtype: "m.notice",
- }])
-})
-
test("message2event embeds: nothing but a field", async t => {
const events = await messageToEvent(data.message_with_embeds.nothing_but_a_field, data.guild.general, {})
t.deepEqual(events, [{
+ $type: "m.room.message",
+ body: "> ↪️ @papiophidian: used `/stats`",
+ format: "org.matrix.custom.html",
+ formatted_body: "↪️ @papiophidian used /stats",
+ "m.mentions": {},
+ msgtype: "m.text",
+ }, {
$type: "m.room.message",
"m.mentions": {},
msgtype: "m.notice",
- body: "↪️ PapiOphidian used `/stats`"
- + "\n| ### Amanda 🎵#2192 :online:"
+ body: "| ### Amanda 🎵#2192 :online:"
+ "\n| willow tree, branch 0"
+ "\n| **❯ Uptime:**\n| 3m 55s\n| **❯ Memory:**\n| 64.45MB",
format: "org.matrix.custom.html",
- formatted_body: '↪️ PapiOphidian used /stats'
- + '", "m.mentions": {} }]) - t.equal(called, 1, "should call getJoinedMembers once") + t.equal(called, 2, "should call getStateEvent and getJoinedMembers once each") }) -test("message2event embeds: extreme html is all escaped", async t => { - const events = await messageToEvent(data.message_with_embeds.extreme_html_escaping, data.guild.general) +test("message2event embeds: crazy html is all escaped", async t => { + const events = await messageToEvent(data.message_with_embeds.escaping_crazy_html_tags, data.guild.general) t.deepEqual(events, [{ $type: "m.room.message", msgtype: "m.notice", @@ -152,12 +154,17 @@ test("message2event embeds: title without url", async t => { const events = await messageToEvent(data.message_with_embeds.title_without_url, data.guild.general) t.deepEqual(events, [{ $type: "m.room.message", - msgtype: "m.notice", - body: "↪️ PapiOphidian used `/stats`" - + "\n| ## Hi, I'm Amanda!\n| \n| I condone pirating music!", + body: "> ↪️ @papiophidian: used `/stats`", format: "org.matrix.custom.html", - formatted_body: 'Amanda 🎵#2192
' + formatted_body: '
' @@ -94,7 +86,17 @@ test("message2event embeds: blockquote in embed", async t => { let called = 0 const events = await messageToEvent(data.message_with_embeds.blockquote_in_embed, data.guild.general, {}, { api: { - getEffectivePower: mockGetEffectivePower(), + async getStateEvent(roomID, type, key) { + called++ + t.equal(roomID, "!qzDBLKlildpzrrOnFZ:cadence.moe") + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return { + users: { + "@_ooye_bot:cadence.moe": 100 + } + } + }, async getJoinedMembers(roomID) { called++ t.equal(roomID, "!qzDBLKlildpzrrOnFZ:cadence.moe") @@ -122,11 +124,11 @@ test("message2event embeds: blockquote in embed", async t => { formatted_body: "Amanda 🎵#2192
' + '
willow tree, branch 0' + '
❯ Uptime:
3m 55s' + '
❯ Memory:
64.45MBreply draft
The following is a message composed via consensus of the Stinker Council.
For those who are not currently aware of our existence, we represent the organization known as Wonderland. Our previous mission centered around the assortment and study of puzzling objects, entities and other assorted phenomena. This mission was the focus of our organization for more than 28 years.
Due to circumstances outside of our control, this directive has now changed. Our new mission will be the extermination of the stinker race.
There will be no further communication.
↪️ PapiOphidian used /stats'
- + ``, + formatted_body: "Hi, I'm Amanda!
I condone pirating music!
↪️ @papiophidian used /stats",
+ "m.mentions": {},
+ msgtype: "m.text",
+ }, {
+ $type: "m.room.message",
+ msgtype: "m.notice",
+ body: "| ## Hi, I'm Amanda!\n| \n| I condone pirating music!",
+ format: "org.matrix.custom.html",
+ formatted_body: ``, "m.mentions": {} }]) }) @@ -166,12 +173,17 @@ test("message2event embeds: url without title", async t => { const events = await messageToEvent(data.message_with_embeds.url_without_title, data.guild.general) t.deepEqual(events, [{ $type: "m.room.message", - msgtype: "m.notice", - body: "↪️ PapiOphidian used `/stats`" - + "\n| I condone pirating music!", + body: "> ↪️ @papiophidian: used `/stats`", format: "org.matrix.custom.html", - formatted_body: 'Hi, I'm Amanda!
I condone pirating music!
↪️ PapiOphidian used /stats'
- + ``, + formatted_body: "I condone pirating music!
↪️ @papiophidian used /stats",
+ "m.mentions": {},
+ msgtype: "m.text",
+ }, {
+ $type: "m.room.message",
+ msgtype: "m.notice",
+ body: "| I condone pirating music!",
+ format: "org.matrix.custom.html",
+ formatted_body: ``, "m.mentions": {} }]) }) @@ -180,12 +192,17 @@ test("message2event embeds: author without url", async t => { const events = await messageToEvent(data.message_with_embeds.author_without_url, data.guild.general) t.deepEqual(events, [{ $type: "m.room.message", - msgtype: "m.notice", - body: "↪️ PapiOphidian used `/stats`" - + "\n| ## Amanda\n| \n| I condone pirating music!", + body: "> ↪️ @papiophidian: used `/stats`", format: "org.matrix.custom.html", - formatted_body: 'I condone pirating music!
↪️ PapiOphidian used /stats'
- + ``, + formatted_body: "Amanda
I condone pirating music!
↪️ @papiophidian used /stats",
+ "m.mentions": {},
+ msgtype: "m.text",
+ }, {
+ $type: "m.room.message",
+ msgtype: "m.notice",
+ body: "| ## Amanda\n| \n| I condone pirating music!",
+ format: "org.matrix.custom.html",
+ formatted_body: ``, "m.mentions": {} }]) }) @@ -194,50 +211,17 @@ test("message2event embeds: author url without name", async t => { const events = await messageToEvent(data.message_with_embeds.author_url_without_name, data.guild.general) t.deepEqual(events, [{ $type: "m.room.message", - msgtype: "m.notice", - body: "↪️ PapiOphidian used `/stats`" - + "\n| I condone pirating music!", + body: "> ↪️ @papiophidian: used `/stats`", format: "org.matrix.custom.html", - formatted_body: 'Amanda
I condone pirating music!
↪️ PapiOphidian used /stats'
- + ``, - "m.mentions": {} - }]) -}) - -test("message2event embeds: 4 images", async t => { - const events = await messageToEvent(data.message_with_embeds.four_images, data.guild.general) - t.deepEqual(events, [{ - $type: "m.room.message", + formatted_body: "I condone pirating music!
↪️ @papiophidian used /stats",
+ "m.mentions": {},
msgtype: "m.text",
- body: "[🔀 Forwarded message]\n» https://fixupx.com/i/status/2032003668787020046",
- format: "org.matrix.custom.html",
- formatted_body: "🔀 Forwarded messagehttps://fixupx.com/i/status/2032003668787020046", - "m.mentions": {} }, { $type: "m.room.message", msgtype: "m.notice", - body: "» | ## ⏺️ AUTOMATON WEST (@AUTOMATON_ENG) https://x.com/AUTOMATON_ENG/status/2032003668787020046" - + "\n» | " - + "\n» | 4chan owner Hiroyuki, Evangelion director Hideaki Anno and GACKT to participate in “humanity’s last non\\-AI made social network”" - + "\n» | ︀︀" - + "\n» | ︀︀[automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/](https://automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/)" - + "\n» | " - + "\n» | **[💬](https://x.com/intent/tweet?in_reply_to=2032003668787020046) 36 [🔁](https://x.com/intent/retweet?tweet_id=2032003668787020046) 212 [❤](https://x.com/intent/like?tweet_id=2032003668787020046) 3\\.0K 👁 131\\.7K **" - + "\n» | " - + "\n» | 📸 https://pbs.twimg.com/media/HDMUyf6bQAM3yts.jpg?name=orig" - + "\n» | — FixupX" - + "\n» | 📸 https://pbs.twimg.com/media/HDMUgxybQAE4FtJ.jpg?name=orig" - + "\n» | 📸 https://pbs.twimg.com/media/HDMUrPobgAAeb90.jpg?name=orig" - + "\n» | 📸 https://pbs.twimg.com/media/HDMUuy5bgAAInj5.jpg?name=orig", + body: "| I condone pirating music!", format: "org.matrix.custom.html", - formatted_body: "
", + formatted_body: `" - + "⏺️ AUTOMATON WEST (@AUTOMATON_ENG)
" - + "4chan owner Hiroyuki, Evangelion director Hideaki Anno and GACKT to participate in “humanity’s last non-AI made social network”" - + "
" - + "
︀︀
︀︀automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/" - + "
💬 36 🔁 212 ❤ 3.0K 👁 131.7K📸 https://pbs.twimg.com/media/HDMUyf6bQAM3yts.jpg?name=orig
— FixupX📸 https://pbs.twimg.com/media/HDMUgxybQAE4FtJ.jpg?name=orig
" - + "📸 https://pbs.twimg.com/media/HDMUrPobgAAeb90.jpg?name=orig
" - + "📸 https://pbs.twimg.com/media/HDMUuy5bgAAInj5.jpg?name=orig
`, "m.mentions": {} }]) }) @@ -337,21 +321,6 @@ test("message2event embeds: youtube video", async t => { }]) }) -test("message2event embeds: embed not bridged if its link was spoilered", async t => { - const events = await messageToEvent({ - ...data.message_with_embeds.youtube_video, - content: "||https://youtu.be/kDMHHw8JqLE?si=NaqNjVTtXugHeG_E\n\n\nJutomi I'm gonna make these sounds in your walls tonight||" - }, data.guild.general) - t.deepEqual(events, [{ - $type: "m.room.message", - msgtype: "m.text", - body: "[spoiler]", - format: "org.matrix.custom.html", - formatted_body: `https://youtu.be/kDMHHw8JqLE?si=NaqNjVTtXugHeG_EI condone pirating music!
➿ Cute Corgi Waddle", - "m.mentions": {} - }]) -}) - test("message2event embeds: if discord creates an embed preview for a discord channel link, don't copy that embed", async t => { const events = await messageToEvent(data.message_with_embeds.discord_server_included_punctuation_bad_discord, data.guild.general, {}, { api: { - getEffectivePower: mockGetEffectivePower(), + async getStateEvent(roomID, type, key) { + t.equal(roomID, "!TqlyQmifxGUggEmdBN:cadence.moe") + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return { + users: { + "@_ooye_bot:cadence.moe": 100 + } + } + }, async getJoinedMembers(roomID) { t.equal(roomID, "!TqlyQmifxGUggEmdBN:cadence.moe") return { diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 33d8696..93e120e 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -14,33 +14,30 @@ const file = sync.require("../../matrix/file") const emojiToKey = sync.require("./emoji-to-key") /** @type {import("../actions/lottie")} */ const lottie = sync.require("../actions/lottie") -/** @type {import("../../matrix/utils")} */ -const mxUtils = sync.require("../../matrix/utils") +/** @type {import("../../m2d/converters/utils")} */ +const mxUtils = sync.require("../../m2d/converters/utils") /** @type {import("../../discord/utils")} */ const dUtils = sync.require("../../discord/utils") -/** @type {import("./find-mentions")} */ -const findMentions = sync.require("./find-mentions") -/** @type {import("../../discord/interactions/poll-responses")} */ -const pollResponses = sync.require("../../discord/interactions/poll-responses") const {reg} = require("../../matrix/read-registration") +const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex)) + /** * @param {DiscordTypes.APIMessage} message * @param {DiscordTypes.APIGuild} guild * @param {boolean} useHTML - * @param {string[]} spoilers */ -function getDiscordParseCallbacks(message, guild, useHTML, spoilers = []) { +function getDiscordParseCallbacks(message, guild, useHTML) { return { /** @param {{id: string, type: "discordUser"}} node */ user: node => { const mxid = select("sim", "mxid", {user_id: node.id}).pluck().get() - const interactionMetadata = message.interaction_metadata + const interaction = message.interaction_metadata || message.interaction const username = message.mentions?.find(ment => ment.id === node.id)?.username || message.referenced_message?.mentions?.find(ment => ment.id === node.id)?.username - || (interactionMetadata?.user.id === node.id ? interactionMetadata.user.username : null) - || (message.author?.id === node.id ? message.author.username : null) - || "unknown-user" + || (interaction?.user.id === node.id ? interaction.user.username : null) + || (message.author.id === node.id ? message.author.username : null) + || node.id if (mxid && useHTML) { return `@${username}` } else { @@ -93,10 +90,6 @@ function getDiscordParseCallbacks(message, guild, useHTML, spoilers = []) { here: () => { if (message.mention_everyone) return "@room" return "@here" - }, - spoiler: node => { - spoilers.push(node.raw) - return useHTML } } } @@ -109,10 +102,9 @@ const embedTitleParser = markdown.markdownEngine.parserFor({ /** * @param {{room?: boolean, user_ids?: string[]}} mentions - * @param {Omit
↪️ ${mxid ? tag`${username}` : username} used /${interaction.name}${thinkingText}`
- }
-}
-
-/**
- * @param {any} newEvents merge into events
- * @param {any} events will be modified
- * @param {boolean} forceSameMsgtype whether m.text may only be combined with m.text, etc
- */
-function mergeTextEvents(newEvents, events, forceSameMsgtype) {
- let prev = events.at(-1)
- for (const ne of newEvents) {
- const isAllText = prev?.body && prev?.formatted_body && ["m.text", "m.notice"].includes(ne.msgtype) && ["m.text", "m.notice"].includes(prev?.msgtype)
- const typesPermitted = !forceSameMsgtype || ne?.msgtype === prev?.msgtype
- if (isAllText && typesPermitted) {
- const rep = new mxUtils.MatrixStringBuilder()
- rep.body = prev.body
- rep.formattedBody = prev.formatted_body
- rep.addLine(ne.body, ne.formatted_body)
- prev.body = rep.body
- prev.formatted_body = rep.formattedBody
- } else {
- events.push(ne)
- }
- }
-}
-
/**
* @param {DiscordTypes.APIMessage} message
* @param {DiscordTypes.APIGuild} guild
@@ -296,7 +210,6 @@ function mergeTextEvents(newEvents, events, forceSameMsgtype) {
* @returns {Promise<{$type: string, $sender?: string, [x: string]: any}[]>}
*/
async function messageToEvent(message, guild, options = {}, di) {
- message = structuredClone(message)
const events = []
/* c8 ignore next 7 */
@@ -308,38 +221,6 @@ async function messageToEvent(message, guild, options = {}, di) {
return []
}
- if (message.type === DiscordTypes.MessageType.PollResult) {
- const pollMessageID = message.message_reference?.message_id
- if (!pollMessageID) return []
- const event_id = select("event_message", "event_id", {message_id: pollMessageID}).pluck().get()
- const roomID = select("channel_room", "room_id", {channel_id: message.channel_id}).pluck().get()
- const pollQuestionText = select("poll", "question_text", {message_id: pollMessageID}).pluck().get()
- if (!event_id || !roomID || !pollQuestionText) return [] // drop it if the corresponding poll start was not bridged
-
- const rep = new mxUtils.MatrixStringBuilder()
- rep.addLine(`The poll ${pollQuestionText} has closed.`, tag`The poll ${pollQuestionText} has closed.`)
-
- const {messageString} = pollResponses.getCombinedResults(pollMessageID, true) // poll results have already been double-checked before this point, so these totals will be accurate
- rep.addLine(markdown.toHTML(messageString, {discordOnly: true, escapeHTML: false}), markdown.toHTML(messageString, {}))
-
- const {body, formatted_body} = rep.get()
-
- return [{
- $type: "org.matrix.msc3381.poll.end",
- "m.relates_to": {
- rel_type: "m.reference",
- event_id
- },
- "org.matrix.msc3381.poll.end": {},
- "org.matrix.msc1767.text": body,
- "org.matrix.msc1767.html": formatted_body,
- body: body,
- format: "org.matrix.custom.html",
- formatted_body: formatted_body,
- msgtype: "m.text"
- }]
- }
-
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.
@@ -357,20 +238,16 @@ async function messageToEvent(message, guild, options = {}, di) {
}]
}
- if (message.type === DiscordTypes.MessageType.ChannelFollowAdd) {
- return [{
- $type: "m.room.message",
- msgtype: "m.emote",
- body: `set this room to receive announcements from ${message.content}`,
- format: "org.matrix.custom.html",
- formatted_body: tag`set this room to receive announcements from ${message.content}`,
- "m.mentions": {}
- }]
+ const interaction = message.interaction_metadata || message.interaction
+ if (message.type === DiscordTypes.MessageType.ChatInputCommand && interaction && "name" in interaction) {
+ // Commands are sent by the responding bot. Need to attach the metadata of the person using the command at the top.
+ let content = message.content
+ if (content) content = `\n${content}`
+ else if ((message.flags || 0) & DiscordTypes.MessageFlags.Loading) content = " — interaction loading..."
+ content = `> ↪️ <@${interaction.user.id}> used \`/${interaction.name}\`${content}`
+ message = {...message, content} // editToChanges reuses the object so we can't mutate it. have to clone it
}
- let isInteraction = (message.type === DiscordTypes.MessageType.ChatInputCommand || message.type === DiscordTypes.MessageType.ContextMenuCommand) && message.interaction && "name" in message.interaction
- let isThinkingInteraction = isInteraction && !!((message.flags || 0) & DiscordTypes.MessageFlags.Loading)
-
/**
@type {{room?: boolean, user_ids?: string[]}}
We should consider the following scenarios for mentions:
@@ -388,9 +265,8 @@ async function messageToEvent(message, guild, options = {}, di) {
- So make sure we don't do anything in this case.
*/
const mentions = {}
- /** @type {{event_id: string, room_id: string, source: number, channel_id: string}?} */
+ /** @type {{event_id: string, room_id: string, source: number}?} */
let repliedToEventRow = null
- let repliedToEventInDifferentRoom = false
let repliedToUnknownEvent = false
let repliedToEventSenderMxid = null
@@ -404,22 +280,12 @@ async function messageToEvent(message, guild, options = {}, di) {
// 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 = await getHistoricalEventRow(message.message_reference?.message_id)
- if (row && "event_id" in row) {
- repliedToEventRow = Object.assign(row, {channel_id: row.reference_channel_id})
+ 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)
+ if (row) {
+ repliedToEventRow = row
} else if (message.referenced_message) {
repliedToUnknownEvent = true
}
- } else if (message.type === DiscordTypes.MessageType.ContextMenuCommand && message.interaction && message.message_reference?.message_id) {
- // It could be a /plu/ral emulated reply
- if (message.interaction.name.startsWith("Reply ") && message.content.startsWith("-# [↪](")) {
- const row = await getHistoricalEventRow(message.message_reference?.message_id)
- if (row && "event_id" in row) {
- repliedToEventRow = Object.assign(row, {channel_id: row.reference_channel_id})
- message.content = message.content.replace(/^.*\n/, "")
- isInteraction = false // declutter
- }
- }
} 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:]")
@@ -428,8 +294,8 @@ async function messageToEvent(message, guild, options = {}, di) {
assert(message.embeds[0].description)
const match = message.embeds[0].description.match(/\/channels\/[0-9]*\/[0-9]*\/([0-9]{2,})/)
if (match) {
- const row = await getHistoricalEventRow(match[1])
- if (row && "event_id" in row) {
+ 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:
@@ -447,7 +313,7 @@ async function messageToEvent(message, guild, options = {}, di) {
}
}
message.embeds.shift()
- repliedToEventRow = Object.assign(row, {channel_id: row.reference_channel_id})
+ repliedToEventRow = row
}
}
}
@@ -474,34 +340,6 @@ async function messageToEvent(message, guild, options = {}, di) {
return promise
}
- /**
- * @param {string} messageID
- * @param {string} [timestampChannelID]
- */
- async function getHistoricalEventRow(messageID, timestampChannelID) {
- /** @type {{room_id: string} | {event_id: string, room_id: string, reference_channel_id: string, source: number} | null | undefined} */
- let row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
- .select("event_id", "room_id", "reference_channel_id", "source").where({message_id: messageID}).and("ORDER BY part ASC").get()
- if (!row && timestampChannelID) {
- const ts = dUtils.snowflakeToTimestampExact(messageID)
- const oldestRow = from("historical_channel_room").selectUnsafe("max(upgraded_timestamp)", "room_id")
- .where({reference_channel_id: timestampChannelID}).and("and upgraded_timestamp < ?").get(ts)
- if (oldestRow?.room_id) {
- row = {room_id: oldestRow.room_id}
- try {
- const {event_id} = await di.api.getEventForTimestamp(oldestRow.room_id, ts)
- row = {
- event_id,
- room_id: oldestRow.room_id,
- reference_channel_id: oldestRow.reference_channel_id,
- source: 1
- }
- } catch (e) {}
- }
- }
- return row
- }
-
/**
* Translate Discord message links to Matrix event links.
* If OOYE has handled this message in the past, this is an instant database lookup.
@@ -513,13 +351,27 @@ async function messageToEvent(message, guild, options = {}, di) {
for (const match of [...content.matchAll(/https:\/\/(?:ptb\.|canary\.|www\.)?discord(?:app)?\.com\/channels\/[0-9]+\/([0-9]+)\/([0-9]+)/g)]) {
assert(typeof match.index === "number")
const [_, channelID, messageID] = match
- const result = await (async () => {
- const row = await getHistoricalEventRow(messageID, channelID)
- if (!row) return `${match[0]} [event is from another server]`
- const via = await getViaServersMemo(row.room_id)
- if (!("event_id" in row)) return `[unknown event in https://matrix.to/#/${row.room_id}?${via}]`
- return `https://matrix.to/#/${row.room_id}/${row.event_id}?${via}`
- })()
+ let result
+
+ const roomID = select("channel_room", "room_id", {channel_id: channelID}).pluck().get()
+ if (roomID) {
+ const eventID = select("event_message", "event_id", {message_id: messageID}).pluck().get()
+ const via = await getViaServersMemo(roomID)
+ if (eventID && roomID) {
+ result = `https://matrix.to/#/${roomID}/${eventID}?${via}`
+ } else {
+ const ts = dUtils.snowflakeToTimestampExact(messageID)
+ try {
+ const {event_id} = await di.api.getEventForTimestamp(roomID, ts)
+ result = `https://matrix.to/#/${roomID}/${event_id}?${via}`
+ } catch (e) {
+ // M_NOT_FOUND: Unable to find event from In reply to ${repliedToUserHtml}` - + `` - + html - body = `${repliedToDisplayName}: ${repliedToBody}`.split("\n").map(line => "> " + line).join("\n") // scenario 1 part B for mentions - + "\n\n" + body - } else { // repliedToUnknownEvent - const dateDisplay = dUtils.howOldUnbridgedMessage(referenced.timestamp, message.timestamp) - html = `
${repliedToHtml}
In reply to ${dateDisplay} from ${repliedToDisplayName}:` - + `` - + html - body = `In reply to ${dateDisplay}:\n${repliedToDisplayName}: ${repliedToBody}`.split("\n").map(line => "> " + line).join("\n") - + "\n\n" + body - } + if ((repliedToEventRow || repliedToUnknownEvent) && options.includeReplyFallback !== false) { + let repliedToDisplayName + let repliedToUserHtml + if (repliedToEventRow?.source === 0 && repliedToEventSenderMxid) { + const match = repliedToEventSenderMxid.match(/^@([^:]*)/) + assert(match) + repliedToDisplayName = message.referenced_message?.author.username || match[1] || "a Matrix user" // grab the localpart as the display name, whatever + repliedToUserHtml = `${repliedToDisplayName}` + } else { + repliedToDisplayName = message.referenced_message?.author.global_name || message.referenced_message?.author.username || "a Discord user" + repliedToUserHtml = repliedToDisplayName + } + let repliedToContent = message.referenced_message?.content + if (repliedToContent?.match(/^(-# )?> (-# )?<:L1:/)) { + // 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. + // ┌──────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}/, "") + } + if (repliedToContent == "") repliedToContent = "[Media]" + else if (!repliedToContent) repliedToContent = "[Replied-to message content wasn't provided by Discord]" + const {body: repliedToBody, html: repliedToHtml} = await transformContent(repliedToContent) + if (repliedToEventRow) { + // Generate a reply pointing to the Matrix event we found + html = `
${repliedToHtml}
In reply to ${repliedToUserHtml}` + + `
${repliedToHtml}
In reply to ${dateDisplay} from ${repliedToDisplayName}:` + + `` + + html + body = (`In reply to ${dateDisplay}:\n${repliedToDisplayName}: ` + + repliedToBody).split("\n").map(line => "> " + line).join("\n") + + "\n\n" + body } - } - - if (isInteraction && !isThinkingInteraction && message.interaction && events.length === 0) { - const formattedInteraction = getFormattedInteraction(message.interaction, false) - body = `${formattedInteraction.body}\n${body}` - html = `${formattedInteraction.html}${html}` } const newTextMessageEvent = { $type: "m.room.message", "m.mentions": mentions, msgtype, - body: body, - format: "org.matrix.custom.html", - formatted_body: html + body: body + } + + const isPlaintext = body === html + + if (!isPlaintext || options.alwaysReturnFormattedBody) { + Object.assign(newTextMessageEvent, { + format: "org.matrix.custom.html", + formatted_body: html + }) } events.push(newTextMessageEvent) @@ -731,45 +531,24 @@ async function messageToEvent(message, guild, options = {}, di) { message.content = "changed the channel name to **" + message.content + "**" } - // Handle message type 63, new emoji announcement - // @ts-expect-error - should be changed to a DiscordTypes reference once it has been documented - if (message.type === 63) { - const match = message.content.match(/^<(a?):([^:>]{1,64}):([0-9]+)>$/) - assert(match, `message type 63, which announces a new emoji, did not include an emoji. the actual content was: "${message.content}"`) - const name = match[2] - msgtype = "m.emote" - message.content = `added a new emoji, ${message.content} :${name}:` - } - - // Send Klipy GIFs in customised form - let isKlipyGIF = false - let isOnlyKlipyGIF = false - if (message.embeds?.length === 1 && message.embeds[0].provider?.name === "Klipy" && message.embeds[0].video?.url) { - isKlipyGIF = true - if (message.content.match(/^https?:\/\/klipy\.com[^ \n]+$/)) { - isOnlyKlipyGIF = true - } - } - // Forwarded content appears first - if (message.message_reference?.type === DiscordTypes.MessageReferenceType.Forward && message.message_reference.message_id && message.message_snapshots?.length) { + if (message.message_reference?.type === DiscordTypes.MessageReferenceType.Forward && message.message_snapshots?.length) { // Forwarded notice - const row = await getHistoricalEventRow(message.message_reference.message_id, message.message_reference.channel_id) + const eventID = select("event_message", "event_id", {message_id: message.message_reference.message_id}).pluck().get() const room = select("channel_room", ["room_id", "name", "nick"], {channel_id: message.message_reference.channel_id}).get() const forwardedNotice = new mxUtils.MatrixStringBuilder() if (room) { const roomName = room && (room.nick || room.name) - if (row && "event_id" in row) { - const via = await getViaServersMemo(row.room_id) + const via = await getViaServersMemo(room.room_id) + if (eventID) { forwardedNotice.addLine( `[🔀 Forwarded from #${roomName}]`, - tag`🔀 Forwarded from ${roomName} [jump to event]` + tag`🔀 Forwarded from ${roomName}` ) } else { - const via = await getViaServersMemo(room.room_id) forwardedNotice.addLine( `[🔀 Forwarded from #${roomName}]`, - tag`🔀 Forwarded from ${roomName} [jump to room]` + tag`🔀 Forwarded from ${roomName}` ) } } else { @@ -786,6 +565,7 @@ async function messageToEvent(message, guild, options = {}, di) { // Indent for (const event of forwardedEvents) { if (["m.text", "m.notice"].includes(event.msgtype)) { + event.msgtype = "m.notice" event.body = event.body.split("\n").map(l => "» " + l).join("\n") event.formatted_body = `
${repliedToHtml}
${event.formatted_body}` } @@ -802,37 +582,33 @@ async function messageToEvent(message, guild, options = {}, di) { events.push(...forwardedEvents) } - if (isInteraction && isThinkingInteraction && message.interaction) { - const formattedInteraction = getFormattedInteraction(message.interaction, true) - await addTextEvent(formattedInteraction.body, formattedInteraction.html, "m.notice") - } - // Then text content - if (message.content && !isOnlyKlipyGIF && !isThinkingInteraction) { - // Scan the content for emojihax and replace them with real emojis - let content = message.content.replaceAll(/\[([a-zA-Z0-9_-]{2,32})(?:~[0-9]+)?\]\(https:\/\/cdn\.discordapp\.com\/emojis\/([0-9]+)\.[^ \n)`]+\)/g, (_, name, id) => { - return `<:${name}:${id}>` - }) + 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 (options.scanTextForMentions !== false && 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) + } + } + } - const {body, html} = await transformContent(content, {isTheMessageContent: true}) + const {body, html} = await transformContent(message.content) await addTextEvent(body, html, msgtype) } // Then scheduled events if (message.content && di?.snow) { for (const match of [...message.content.matchAll(/discord\.gg\/([A-Za-z0-9]+)\?event=([0-9]{18,})/g)]) { // snowflake has minimum 18 because the events feature is at least that old - let invite - try { - invite = await di.snow.invite.getInvite(match[1], {guild_scheduled_event_id: match[2]}) - } catch (e) { - // Skip expired/invalid invites and events - if (e.message === `{"message": "Unknown Invite", "code": 10006}`) { - break - } else { - throw e - } - } - + const invite = await di.snow.invite.getInvite(match[1], {guild_scheduled_event_id: match[2]}) const event = invite.guild_scheduled_event if (!event) continue // the event ID provided was not valid @@ -874,114 +650,8 @@ async function messageToEvent(message, guild, options = {}, di) { // Then attachments if (message.attachments) { - const attachmentEvents = await Promise.all(message.attachments.map(attachment => attachmentToEvent(mentions, attachment))) - - // Try to merge attachment events with the previous event - // This means that if the attachments ended up as a text link, and especially if there were many of them, the events will be joined together. - mergeTextEvents(attachmentEvents, events, false) - } - - // Then components - if (message.components?.length) { - const stack = new mxUtils.MatrixStringBuilderStack() - /** @param {DiscordTypes.APIMessageComponent} component */ - async function processComponent(component) { - // Standalone components - if (component.type === DiscordTypes.ComponentType.TextDisplay) { - const {body, html} = await transformContent(component.content) - stack.msb.addParagraph(body, html) - } - else if (component.type === DiscordTypes.ComponentType.Separator) { - stack.msb.addParagraph("----", "
${formatted_body}` - if (stack.msb.body) stack.msb.body += "\n\n" - stack.msb.add(body, formatted_body) - } - else if (component.type === DiscordTypes.ComponentType.Section) { - // May contain text display, possibly more in the future - // Accessory may be button or thumbnail - stack.bump() - for (const innerComponent of component.components) { - await processComponent(innerComponent) - } - if (component.accessory) { - stack.bump() - await processComponent(component.accessory) - const {body, formatted_body} = stack.shift().get() - stack.msb.addLine(body, formatted_body) - } - const {body, formatted_body} = stack.shift().get() - stack.msb.addParagraph(body, formatted_body) - } - else if (component.type === DiscordTypes.ComponentType.ActionRow) { - const linkButtons = component.components.filter(c => c.type === DiscordTypes.ComponentType.Button && c.style === DiscordTypes.ButtonStyle.Link) - if (linkButtons.length) { - stack.msb.addLine("") - for (const linkButton of linkButtons) { - await processComponent(linkButton) - } - } - } - // Components that can only be inside things - else if (component.type === DiscordTypes.ComponentType.Thumbnail) { - // May only be a section accessory - stack.msb.add(`🖼️ ${component.media.url}`, tag`🖼️ ${component.media.url}`) - } - else if (component.type === DiscordTypes.ComponentType.Button) { - // May only be a section accessory or in an action row (up to 5) - if (component.style === DiscordTypes.ButtonStyle.Link) { - assert(component.label) // required for Discord to validate link buttons - stack.msb.add(`[${component.label} ${component.url}] `, tag`${component.label} `) - } - } - - // Not handling file upload or label because they are modal-only components - } - - for (const component of message.components) { - await processComponent(component) - } - - const {body, formatted_body} = stack.msb.get() - if (body.trim().length) { - await addTextEvent(body, formatted_body, "m.text") - } - } - - // Then polls - if (message.poll) { - const pollEvent = await pollToEvent(message.poll) - events.push(pollEvent) + const attachmentEvents = await Promise.all(message.attachments.map(attachmentToEvent.bind(null, mentions))) + events.push(...attachmentEvents) } // Then embeds @@ -995,43 +665,12 @@ async function messageToEvent(message, guild, options = {}, di) { continue // Matrix's own URL previews are fine for images. } - if (embed.type === "video" && embed.video?.url && !embed.title && message.content.includes(embed.video.url)) { - continue // Doesn't add extra information and the direct video URL is already there. - } - - if (embed.type === "poll_result") { - // The code here is only for the message to be bridged to Matrix. Dealing with the Discord-side updates is in d2m/actions/poll-end.js. - } - if (embed.url?.startsWith("https://discord.com/")) { continue // If discord creates an embed preview for a discord channel link, don't copy that embed } - if (embed.url && spoilers.some(sp => sp.match(/\bhttps?:\/\/[a-z]/))) { - // If the original message had spoilered URLs, don't generate any embeds for links. - // This logic is the same as the Discord desktop client. It doesn't match specific embeds to specific spoilered text, it's all or nothing. - // It's not easy to do much better because posting a link like youtu.be generates an embed.url with youtube.com/watch, so you can't match up the text without making at least that a special case. - continue - } - // 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 const rep = new mxUtils.MatrixStringBuilder() - let isAdditionalImage = false - - if (isKlipyGIF) { - assert(embed.video?.url) - rep.add("[GIF] ", "➿ ") - if (embed.title) { - rep.add(`${embed.title} ${embed.video.url}`, tag`${embed.title}`) - } else { - rep.add(embed.video.url) - } - - let {body, formatted_body: html} = rep.get() - html = `
${html}` - await addTextEvent(body, html, "m.text") - continue - } // Provider if (embed.provider?.name && embed.provider.name !== "Tenor") { @@ -1083,11 +722,7 @@ async function messageToEvent(message, guild, options = {}, di) { 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) { - isAdditionalImage = !rep.body && !!events.length - rep.addParagraph(`📸 ${dUtils.getPublicUrlForCdn(chosenImage)}`) - } + if (chosenImage) rep.addParagraph(`📸 ${dUtils.getPublicUrlForCdn(chosenImage)}`) if (embed.video?.url) rep.addParagraph(`🎞️ ${dUtils.getPublicUrlForCdn(embed.video.url)}`) @@ -1096,11 +731,6 @@ async function messageToEvent(message, guild, options = {}, di) { body = body.split("\n").map(l => "| " + l).join("\n") html = `
${html}` - if (isAdditionalImage) { - mergeTextEvents([{...rep.get(), body, html, msgtype: "m.notice"}], events, true) - continue - } - // Send as m.notice to apply the usual automated/subtle appearance, showing this wasn't actually typed by the person await addTextEvent(body, html, "m.notice") } @@ -1138,7 +768,7 @@ async function messageToEvent(message, guild, options = {}, di) { } // Rich replies - if (repliedToEventRow && !repliedToEventInDifferentRoom) { + if (repliedToEventRow) { Object.assign(events[0], { "m.relates_to": { "m.in_reply_to": { @@ -1148,16 +778,6 @@ async function messageToEvent(message, guild, options = {}, di) { }) } - // Strip formatted_body where equivalent to body - if (!options.alwaysReturnFormattedBody) { - for (const event of events) { - if (event.$type === "m.room.message" && "msgtype" in event && ["m.text", "m.notice"].includes(event.msgtype) && event.body === event.formatted_body) { - delete event.format - delete event.formatted_body - } - } - } - return events } diff --git a/src/d2m/converters/message-to-event.test.pk.js b/src/d2m/converters/message-to-event.pk.test.js similarity index 72% rename from src/d2m/converters/message-to-event.test.pk.js rename to src/d2m/converters/message-to-event.pk.test.js index 1323280..ce83d54 100644 --- a/src/d2m/converters/message-to-event.test.pk.js +++ b/src/d2m/converters/message-to-event.pk.test.js @@ -50,7 +50,11 @@ test("message2event: pk reply to matrix is converted to native matrix reply", as ] }, msgtype: "m.text", - body: "this is a reply", + body: "> cadence [they]: now for my next experiment:\n\nthis is a reply", + format: "org.matrix.custom.html", + formatted_body: '
In reply to cadence [they]
' + + "now for my next experiment:
In reply to wing
' + + "some text
In reply to Ampflower 🌺
' + + "[Media]
" - + "" - + "Lillith (INX)
" - + "Display name: Lillith (she/her)" - + "
" - + `🖼️ https://files.inx.moe/p/cdn/lillith.webp` - + "
Pronouns: She/Her" - + "
Message count: 3091
" - + "Proxy tags:" - + "
l;text" - + "l:text" - + "l.text" - + "textl." - + "textl;" - + "textl:
System ID: xffgnx ∙ Member ID: pphhoh
"
- + "Created: 2025-12-31 03:16:45 UTC
` - + "System: INX (
" - + `🖼️ https://files.inx.moe/p/cdn/lillith.webp` - + "xffgnx)" - + "
Member: Lillith (pphhoh)" - + "
Sent by: infinidoge1337 (@unknown-user)" - + "
Account Roles (7)" - + "
§b, !, ‼, Ears Port Ping, Ears Update Ping, Yttr Ping, unsup Ping
" - + "Same hat
" - + `🖼️ Image: image.png
Original Message ID: 1466556003645657118 · <t:1769724599:f>
", - "m.mentions": {}, - msgtype: "m.text", - }]) -}) diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 97fc25d..ee4ec03 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -2,7 +2,6 @@ const {test} = require("supertape") const {messageToEvent} = require("./message-to-event") const {MatrixServerError} = require("../../matrix/mreq") const data = require("../../../test/data") -const {mockGetEffectivePower} = require("../../matrix/utils.test") const Ty = require("../../types") /** @@ -67,7 +66,17 @@ test("message2event: simple room mention", async t => { let called = 0 const events = await messageToEvent(data.message.simple_room_mention, data.guild.general, {}, { api: { - getEffectivePower: mockGetEffectivePower(), + async getStateEvent(roomID, type, key) { + called++ + t.equal(roomID, "!BnKuBPCvyfOkhcUjEu:cadence.moe") + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return { + users: { + "@_ooye_bot:cadence.moe": 100 + } + } + }, async getJoinedMembers(roomID) { called++ t.equal(roomID, "!BnKuBPCvyfOkhcUjEu:cadence.moe") @@ -88,14 +97,24 @@ test("message2event: simple room mention", async t => { format: "org.matrix.custom.html", formatted_body: '#worm-farm' }]) - t.equal(called, 1, "should call getJoinedMembers") + t.equal(called, 2, "should call getStateEvent and getJoinedMembers once each") }) test("message2event: simple room link", async t => { let called = 0 const events = await messageToEvent(data.message.simple_room_link, data.guild.general, {}, { api: { - getEffectivePower: mockGetEffectivePower(), + async getStateEvent(roomID, type, key) { + called++ + t.equal(roomID, "!BnKuBPCvyfOkhcUjEu:cadence.moe") + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return { + users: { + "@_ooye_bot:cadence.moe": 100 + } + } + }, async getJoinedMembers(roomID) { called++ t.equal(roomID, "!BnKuBPCvyfOkhcUjEu:cadence.moe") @@ -116,14 +135,24 @@ test("message2event: simple room link", async t => { format: "org.matrix.custom.html", formatted_body: '#worm-farm' }]) - t.equal(called, 1, "should call getJoinedMembers once") + t.equal(called, 2, "should call getStateEvent and getJoinedMembers once each") }) test("message2event: nicked room mention", async t => { let called = 0 const events = await messageToEvent(data.message.nicked_room_mention, data.guild.general, {}, { api: { - getEffectivePower: mockGetEffectivePower(), + async getStateEvent(roomID, type, key) { + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return { + users: { + "@_ooye_bot:cadence.moe": 100 + } + } + }, async getJoinedMembers(roomID) { called++ t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") @@ -144,7 +173,7 @@ test("message2event: nicked room mention", async t => { format: "org.matrix.custom.html", formatted_body: '#main' }]) - t.equal(called, 1, "should call getJoinedMembers once") + t.equal(called, 2, "should call getStateEvent and getJoinedMembers once each") }) test("message2event: unknown room mention", async t => { @@ -195,7 +224,17 @@ test("message2event: simple message link", async t => { let called = 0 const events = await messageToEvent(data.message.simple_message_link, data.guild.general, {}, { api: { - getEffectivePower: mockGetEffectivePower(), + async getStateEvent(roomID, type, key) { + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return { + users: { + "@_ooye_bot:cadence.moe": 100 + } + } + }, async getJoinedMembers(roomID) { called++ t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") @@ -216,14 +255,13 @@ test("message2event: simple message link", async t => { format: "org.matrix.custom.html", formatted_body: 'https://matrix.to/#/!kLRqKKUQXcibIMtOpl:cadence.moe/$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg?via=cadence.moe&via=super.invalid' }]) - t.equal(called, 1, "should call getJoinedMembers once") + t.equal(called, 2, "should call getStateEvent and getJoinedMembers once each") }) test("message2event: message link that OOYE doesn't know about", async t => { let called = 0 const events = await messageToEvent(data.message.message_link_to_before_ooye, data.guild.general, {}, { api: { - getEffectivePower: mockGetEffectivePower(), async getEventForTimestamp(roomID, ts) { called++ t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") @@ -232,6 +270,17 @@ test("message2event: message link that OOYE doesn't know about", async t => { origin_server_ts: 1613287812754 } }, + async getStateEvent(roomID, type, key) { // for ?via calculation + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return { + users: { + "@_ooye_bot:cadence.moe": 100 + } + } + }, async getJoinedMembers(roomID) { // for ?via calculation called++ t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") @@ -254,7 +303,7 @@ test("message2event: message link that OOYE doesn't know about", async t => { formatted_body: "Me: I'll scroll up to find a certain message I'll sendIn reply to Extremity' + + '
Image
In reply to cadence' + + '
so can you reply to my webhook uwu
In reply to okay 🤍 yay 🤍' + + '
@extremity you owe me $30
In reply to cadence [they]
What about them?
In reply to Ami (she/her)
let me guess they got a lot of bug reports like "empty chest with no loot?"
In reply to Ami (she/her)
let me guess they got a lot of bug reports like "empty chest with no loot?"
public @Nullable EntityType<?>`
- }])
-})
-
-test("message2event: written @mentions do not match in code block", async t => {
- const events = await messageToEvent({
- ...data.message.advanced_written_at_mention_for_matrix,
- content: "```java\npublic @Nullable EntityType>\n```"
- }, data.guild.general, {}, {})
- t.deepEqual(events, [{
- $type: "m.room.message",
- "m.mentions": {},
- msgtype: "m.text",
- body: "```java\npublic @Nullable EntityType>\n```",
- format: "org.matrix.custom.html",
- formatted_body: `public @Nullable EntityType<?>`
- }])
-})
-
-test("message2event: entire message may match elaborate display name", async t => {
- let called = 0
- const events = await messageToEvent({
- ...data.message.advanced_written_at_mention_for_matrix,
- content: "@Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆"
- }, data.guild.general, {}, {
- api: {
- async getJoinedMembers(roomID) {
- called++
- t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
- return new Promise(resolve => {
- setTimeout(() => {
- resolve({
- joined: {
- "@wa:cadence.moe": {
- display_name: "Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆",
- avatar_url: "whatever"
- },
- "@huckleton:cadence.moe": {
- display_name: "huck",
- avatar_url: "whatever"
- },
- "@_ooye_botrac4r:cadence.moe": {
- display_name: "botrac4r",
- avatar_url: "whatever"
- },
- "@_ooye_bot:cadence.moe": {
- display_name: "Out Of Your Element",
- avatar_url: "whatever"
- }
- }
- })
- })
- })
- }
- }
- })
- t.deepEqual(events, [{
- $type: "m.room.message",
- "m.mentions": {
- user_ids: [
- "@wa:cadence.moe",
- ]
- },
- msgtype: "m.text",
- body: "@Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆",
- format: "org.matrix.custom.html",
- formatted_body: `@Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆`
- }])
-})
-
-test("message2event: spoilers are removed from plaintext body", async t => {
- const events = await messageToEvent({
- content: "||**beatrice**||"
- })
- t.deepEqual(events, [{
- $type: "m.room.message",
- "m.mentions": {},
- msgtype: "m.text",
- body: "[spoiler]",
- format: "org.matrix.custom.html",
- formatted_body: `beatrice`
- }])
-})
-
test("message2event: very large attachment is linked instead of being uploaded", async t => {
const events = await messageToEvent({
content: "hey",
@@ -1067,66 +931,18 @@ test("message2event: very large attachment is linked instead of being uploaded",
size: 100e6
}]
})
- t.deepEqual(events, [{
- $type: "m.room.message",
- "m.mentions": {},
- msgtype: "m.text",
- body: "hey\n📄 Uploaded file: https://bridge.example.org/download/discordcdn/123/456/789.mega (100 MB)",
- format: "org.matrix.custom.html",
- formatted_body: 'hey📸 Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/123/456/SPOILER_secret.jpg (38 KB)` - + `📄 Uploaded file: hey.jpg (100 MB)` }, { $type: "m.room.message", "m.mentions": {}, - msgtype: "m.file", - body: "my enemies.txt", - filename: "my enemies.txt", - external_url: "https://bridge.example.org/download/discordcdn/123/456/my_enemies.txt", - url: "mxc://cadence.moe/y89EOTRp2lbeOkgdsEleGOge", - info: { - mimetype: "text/plain", - size: 8911 - } + msgtype: "m.text", + body: "📄 Uploaded file: https://bridge.example.org/download/discordcdn/123/456/789.mega (100 MB)", + format: "org.matrix.custom.html", + formatted_body: '📄 Uploaded file: hey.jpg (100 MB)' }]) }) @@ -1142,19 +958,6 @@ test("message2event: type 4 channel name change", async t => { }]) }) -test("message2event: type 12 channel follow add", async t => { - const events = await messageToEvent(data.special_message.channel_follow_add, data.guild.general) - t.deepEqual(events, [{ - $type: "m.room.message", - "m.mentions": {}, - msgtype: "m.emote", - body: "set this room to receive announcements from PluralKit #downtime", - format: "org.matrix.custom.html", - formatted_body: "set this room to receive announcements from PluralKit #downtime", - "m.mentions": {} - }]) -}) - test("message2event: thread start message reference", async t => { const events = await messageToEvent(data.special_message.thread_start_context, data.guild.general, {}, { api: { @@ -1214,18 +1017,6 @@ test("message2event: emoji that hasn't been registered yet", async t => { }]) }) -test("message2event: emojihax", async t => { - const events = await messageToEvent(data.message.emojihax, data.guild.general, {}) - t.deepEqual(events, [{ - $type: "m.room.message", - "m.mentions": {}, - msgtype: "m.text", - body: "I only violate the don't modify our console part of terms of service :troll:", - format: "org.matrix.custom.html", - formatted_body: `I only violate the don't modify our console part of terms of service
What's cooking, good looking?`, "m.mentions": {}, - msgtype: "m.text", + msgtype: "m.notice", }, { $type: "m.room.message", @@ -1404,7 +1194,6 @@ test("message2event: constructed forwarded message", async t => { test("message2event: constructed forwarded text", async t => { const events = await messageToEvent(data.message.constructed_forwarded_text, {}, {}, { api: { - getEffectivePower: mockGetEffectivePower(), async getJoinedMembers() { return { joined: { @@ -1421,10 +1210,10 @@ test("message2event: constructed forwarded text", async t => { body: "[🔀 Forwarded from #amanda-spam]" + "\n» What's cooking, good looking?", format: "org.matrix.custom.html", - formatted_body: `🔀 Forwarded from amanda-spam [jump to room]` + formatted_body: `🔀 Forwarded from amanda-spam` + `
What's cooking, good looking?`, "m.mentions": {}, - msgtype: "m.text", + msgtype: "m.notice", }, { $type: "m.room.message", @@ -1447,7 +1236,7 @@ test("message2event: don't scan forwarded messages for mentions", async t => { formatted_body: `🔀 Forwarded message` + `
If some folks have spare bandwidth then helping out ArchiveTeam with archiving soon to be deleted research and government data might be worthwhile https://social.luca.run/@luca/113950834185678114`, "m.mentions": {}, - msgtype: "m.text" + msgtype: "m.notice" } ]) }) @@ -1542,7 +1331,6 @@ test("message2event: vc invite event renders embed", async t => { test("message2event: vc invite event renders embed with room link", async t => { const events = await messageToEvent({content: "https://discord.gg/placeholder?event=1381174024801095751"}, {}, {}, { api: { - getEffectivePower: mockGetEffectivePower(), getJoinedMembers: async () => ({ joined: { "@_ooye_bot:cadence.moe": {display_name: null, avatar_url: null}, @@ -1581,28 +1369,6 @@ test("message2event: vc invite event renders embed with room link", async t => { ]) }) -test("message2event: expired/invalid invites are sent as-is", async t => { - const events = await messageToEvent({content: "https://discord.gg/placeholder?event=1381190945646710824"}, {}, {}, { - snow: { - invite: { - async getInvite() { - throw new Error(`{"message": "Unknown Invite", "code": 10006}`) - } - } - } - }) - t.deepEqual(events, [ - { - $type: "m.room.message", - body: "https://discord.gg/placeholder?event=1381190945646710824", - format: "org.matrix.custom.html", - formatted_body: "https://discord.gg/placeholder?event=1381190945646710824", - "m.mentions": {}, - msgtype: "m.text", - } - ]) -}) - test("message2event: channel links are converted even inside lists (parser post-processer descends into list items)", async t => { let called = 0 const events = await messageToEvent({ @@ -1614,7 +1380,6 @@ test("message2event: channel links are converted even inside lists (parser post- + "\nThis list will probably change in the future" }, data.guild.general, {}, { api: { - getEffectivePower: mockGetEffectivePower(), getJoinedMembers(roomID) { called++ t.equal(roomID, "!qzDBLKlildpzrrOnFZ:cadence.moe") @@ -1646,226 +1411,3 @@ test("message2event: channel links are converted even inside lists (parser post- ]) t.equal(called, 1) }) - -test("message2event: emoji added special message", async t => { - const events = await messageToEvent(data.special_message.emoji_added) - t.deepEqual(events, [ - { - $type: "m.room.message", - msgtype: "m.emote", - body: "added a new emoji, :cx_marvelous: :cx_marvelous:", - format: "org.matrix.custom.html", - formatted_body: `added a new emoji,
In reply to Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆cross-room reply`, - "m.mentions": { - user_ids: [ - "@cadence:cadence.moe" - ] - } - } - ]) -}) - -test("message2event: forwarded message with unreferenced mention", async t => { - const events = await messageToEvent({ - type: 0, - content: "", - attachments: [], - embeds: [], - timestamp: "2026-01-20T14:14:21.281Z", - edited_timestamp: null, - flags: 16384, - components: [], - id: "1463174818823405651", - channel_id: "893634327722721290", - author: { - id: "100031256988766208", - username: "leo60228", - discriminator: "0", - avatar: "8a164f29946f23eb4f45cde71a75e5a6", - avatar_decoration_data: null, - public_flags: 768, - global_name: "leo vriska", - primary_guild: null, - collectibles: null, - display_name_styles: null - }, - bot: false, - pinned: false, - mentions: [], - mention_roles: [], - mention_everyone: false, - tts: false, - message_reference: { - type: 1, - channel_id: "937181373943382036", - message_id: "1032034158261846038", - guild_id: "936370934292549712" - }, - message_snapshots: [ - { - message: { - type: 0, - content: "<@77084495118868480>", - attachments: [ - { - id: "1463174815119704114", - filename: "2022-10-18_16-49-46.mp4", - size: 51238885, - url: "https://cdn.discordapp.com/attachments/893634327722721290/1463174815119704114/2022-10-18_16-49-46.mp4?ex=6970df3c&is=696f8dbc&hm=515d3cbcc8464bdada7f4c3d9ccc8174f671cb75391ce21a46a804fcb1e4befe&", - proxy_url: "https://media.discordapp.net/attachments/893634327722721290/1463174815119704114/2022-10-18_16-49-46.mp4?ex=6970df3c&is=696f8dbc&hm=515d3cbcc8464bdada7f4c3d9ccc8174f671cb75391ce21a46a804fcb1e4befe&", - width: 1920, - height: 1080, - content_type: "video/mp4", - content_scan_version: 3, - spoiler: false - } - ], - embeds: [], - timestamp: "2022-10-18T20:55:17.597Z", - edited_timestamp: null, - flags: 0, - components: [] - } - } - ] - }) - t.deepEqual(events, [{ - $type: "m.room.message", - msgtype: "m.text", - body: "[🔀 Forwarded message]\n» @unknown-user:\n» 🎞️ Uploaded file: https://bridge.example.org/download/discordcdn/893634327722721290/1463174815119704114/2022-10-18_16-49-46.mp4 (51 MB)", - format: "org.matrix.custom.html", - formatted_body: "🔀 Forwarded message
[Media]
@unknown-user:", - "m.mentions": {} - }]) -}) - -test("message2event: single-choice poll", async t => { - const events = await messageToEvent(data.message.poll_single_choice, data.guild.general, {}) - t.deepEqual(events, [{ - $type: "org.matrix.msc3381.poll.start", - "org.matrix.msc3381.poll.start": { - question: { - "org.matrix.msc1767.text": "only one answer allowed!", - body: "only one answer allowed!", - msgtype: "m.text" - }, - kind: "org.matrix.msc3381.poll.disclosed", // Discord always lets you see results, so keeping this consistent with that. - max_selections: 1, - answers: [{ - id: "1", - "org.matrix.msc1767.text": "[\ud83d\udc4d] answer one" - }, { - id: "2", - "org.matrix.msc1767.text": "[\ud83d\udc4e] answer two" - }, { - id: "3", - "org.matrix.msc1767.text": "answer three" - }] - }, - "org.matrix.msc1767.text": "only one answer allowed!\n1. [\ud83d\udc4d] answer one\n2. [\ud83d\udc4e] answer two\n3. answer three" - }]) -}) - -test("message2event: multiple-choice poll", async t => { - const events = await messageToEvent(data.message.poll_multiple_choice, data.guild.general, {}) - t.deepEqual(events, [{ - $type: "org.matrix.msc3381.poll.start", - "org.matrix.msc3381.poll.start": { - question: { - "org.matrix.msc1767.text": "more than one answer allowed", - body: "more than one answer allowed", - msgtype: "m.text" - }, - kind: "org.matrix.msc3381.poll.disclosed", // Discord always lets you see results, so keeping this consistent with that. - max_selections: 3, - answers: [{ - id: "1", - "org.matrix.msc1767.text": "[😭] no" - }, { - id: "2", - "org.matrix.msc1767.text": "oh no" - }, { - id: "3", - "org.matrix.msc1767.text": "oh noooooo" - }] - }, - "org.matrix.msc1767.text": "more than one answer allowed\n1. [😭] no\n2. oh no\n3. oh noooooo" - }]) -}) - -test("message2event: smalltext from regular user", async t => { - const events = await messageToEvent({ - content: "-# hmm", - author: { - bot: false - } - }) - t.deepEqual(events, [{ - $type: "m.room.message", - msgtype: "m.text", - "m.mentions": {}, - body: "...hmm" - }]) -}) diff --git a/src/d2m/converters/pins-to-list.js b/src/d2m/converters/pins-to-list.js index 4ad8800..3e890ea 100644 --- a/src/d2m/converters/pins-to-list.js +++ b/src/d2m/converters/pins-to-list.js @@ -3,11 +3,10 @@ const {select} = require("../../passthrough") /** - * @param {import("discord-api-types/v10").RESTGetAPIChannelMessagesPinsResult} pins + * @param {import("discord-api-types/v10").RESTGetAPIChannelPinsResult} pins * @param {{"m.room.pinned_events/"?: {pinned?: string[]}}} kstate */ function pinsToList(pins, kstate) { - /** Most recent last. */ let alreadyPinned = kstate["m.room.pinned_events/"]?.pinned || [] // If any of the already pinned messages are bridged messages then remove them from the already pinned list. @@ -16,13 +15,13 @@ function pinsToList(pins, kstate) { // * Matrix-only unbridged messages that are pinned will remain pinned. alreadyPinned = alreadyPinned.filter(event_id => { const messageID = select("event_message", "message_id", {event_id}).pluck().get() - return !messageID || pins.items.find(m => m.message.id === messageID) // if it is bridged then remove it from the filter + return !messageID || pins.find(m => m.id === messageID) // if it is bridged then remove it from the filter }) /** @type {string[]} */ const result = [] - for (const pin of pins.items) { - const eventID = select("event_message", "event_id", {message_id: pin.message.id}, "ORDER BY part ASC").pluck().get() + for (const message of pins) { + const eventID = select("event_message", "event_id", {message_id: message.id, part: 0}).pluck().get() if (eventID && !alreadyPinned.includes(eventID)) result.push(eventID) } result.reverse() diff --git a/src/d2m/converters/pins-to-list.test.js b/src/d2m/converters/pins-to-list.test.js index 571735e..d0657cb 100644 --- a/src/d2m/converters/pins-to-list.test.js +++ b/src/d2m/converters/pins-to-list.test.js @@ -1,7 +1,6 @@ const {test} = require("supertape") const data = require("../../../test/data") const {pinsToList} = require("./pins-to-list") -const mixin = require("@cloudrac3r/mixin-deep") test("pins2list: converts known IDs, ignores unknown IDs", t => { const result = pinsToList(data.pins.faked, {}) @@ -47,9 +46,7 @@ test("pins2list: already pinned unknown items are not moved", t => { }) test("pins2list: bridged messages can be unpinned", t => { - const shortPins = mixin({}, data.pins.faked) - shortPins.items = shortPins.items.slice(0, -2) - const result = pinsToList(shortPins, { + const result = pinsToList(data.pins.faked.slice(0, -2), { "m.room.pinned_events/": { pinned: [ "$mtR8cJqM4fKno1bVsm8F4wUVqSntt2sq6jav1lyavuA", diff --git a/src/d2m/converters/remove-member-mxids.js b/src/d2m/converters/remove-member-mxids.js deleted file mode 100644 index de26662..0000000 --- a/src/d2m/converters/remove-member-mxids.js +++ /dev/null @@ -1,38 +0,0 @@ -// @ts-check - -const passthrough = require("../../passthrough") -const {db, select, from} = passthrough - -/** - * @param {string} userID discord user ID that left - * @param {string} guildID discord guild ID that they left - */ -function removeMemberMxids(userID, guildID) { - // Get sims for user and remove - let membership = from("sim").join("sim_member", "mxid").join("channel_room", "room_id") - .select("room_id", "mxid").where({user_id: userID, guild_id: guildID}).and("ORDER BY room_id, mxid").all() - membership = membership.concat(from("sim_proxy").join("sim", "user_id").join("sim_member", "mxid").join("channel_room", "room_id") - .select("room_id", "mxid").where({proxy_owner_id: userID, guild_id: guildID}).and("ORDER BY room_id, mxid").all()) - - // Get user installed apps and remove - /** @type {string[]} */ - let userAppDeletions = [] - // 1. Select apps that have 1 user remaining - /** @type {Set
🎞️ Uploaded file: 2022-10-18_16-49-46.mp4 (51 MB)
.*?<\/blockquote>(.....)/s, "$1") // If the message starts with a blockquote, don't count it and use the message body afterwards - repliedToContent = repliedToContent.replace(/(?:\n|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|
)+/g, " ") // Should all be on one line + repliedToContent = repliedToContent.replace(/(?:\n|
)+/g, " ") // Should all be on one line repliedToContent = repliedToContent.replace(/]*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.) repliedToContent = repliedToContent.replace(/]*)>/g, (_, att) => { // Convert Matrix emoji images into Discord emoji markdown const mxcUrlMatch = att.match(/\bsrc="(mxc:\/\/[^"]+)"/) @@ -791,20 +685,20 @@ async function eventToMessage(event, guild, channel, di) { return convertEmoji(mxcUrlMatch?.[1], titleTextMatch?.[1], false, false) }) repliedToContent = repliedToContent.replace(/<[^:>][^>]*>/g, "") // Completely strip all HTML tags and formatting. + repliedToContent = repliedToContent.replace(/\bhttps?:\/\/[^ )]*/g, "<$&>") repliedToContent = entities.decodeHTML5Strict(repliedToContent) // Remove entities like & " const contentPreviewChunks = chunk(repliedToContent, 50) if (contentPreviewChunks.length) { contentPreview = ": " + contentPreviewChunks[0] - contentPreview = contentPreview.replace(/\bhttps?:\/\/[^ )]*/g, "<$&>") if (contentPreviewChunks.length > 1) contentPreview = contentPreview.replace(/[,.']$/, "") + "..." } else { contentPreview = "" } } const sender = repliedToEvent.sender - const authorMention = getUserOrProxyOwnerMention(sender) - if (authorMention) { - replyLine += authorMention + const authorID = getUserOrProxyOwnerID(sender) + if (authorID) { + replyLine += `<@${authorID}>` } else { let senderName = select("member_cache", "displayname", {mxid: sender}).pluck().get() if (!senderName) senderName = sender.match(/@([^:]*)/)?.[1] @@ -813,233 +707,170 @@ async function eventToMessage(event, guild, channel, di) { replyLine = `-# > ${replyLine}${contentPreview}\n` })() - if (shouldProcessTextEvent) { - 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}` - } - - // Handling mentions of Discord users - input = input.replace(/("https:\/\/matrix.to\/#\/((?:@|%40)[^"]+)")>/g, (whole, attributeValue, mxid) => { - mxid = decodeURIComponent(mxid) - if (mxUtils.eventSenderIsFromDiscord(mxid)) { - // Handle mention of an OOYE sim user by their mxid - const id = select("sim", "user_id", {mxid}).pluck().get() - if (!id) return whole - return `${attributeValue} data-user-id="${id}">` - } else { - // Handle mention of a Matrix user by their mxid - // Check if this Matrix user is actually the sim user from another old bridge in the room? - const match = mxid.match(/[^:]*discord[^:]*_([0-9]{6,}):/) // try to match @_discord_123456, @_discordpuppet_123456, etc. - if (match) return `${attributeValue} data-user-id="${match[1]}">` - // Nope, just a real Matrix user. - return whole - } - }) - - // Handling mentions of rooms and room-messages - input = await handleRoomOrMessageLinks(input, di) - - // Stripping colons after mentions - input = input.replace(/( data-user-id.*?<\/a>):?/g, "$1") - input = input.replace(/("https:\/\/matrix.to.*?<\/a>):?/g, "$1") - - // Element adds a bunch of
before
node
- const replacementCode = doc.createElement("code")
- replacementCode.textContent = `[${filename}]`
- // Build its containing node
- const replacement = doc.createElement("span")
- replacement.appendChild(doc.createTextNode(" "))
- replacement.appendChild(replacementCode)
- replacement.appendChild(doc.createTextNode(" "))
- // Replace the code block with the
- preNode.replaceWith(replacement)
- // Upload the code as an attachment
- const content = getCodeContent(preNode.firstChild)
- attachments.push({id: String(attachments.length), filename})
- pendingFiles.push({name: filename, buffer: Buffer.from(content, "utf8")})
- }
- }
- // Suppress link embeds
- if (node.nodeType === 1 && node.tagName === "A") {
- // Suppress if sender tried to add angle brackets
- const inBody = event.content.body.indexOf(node.getAttribute("href"))
- let shouldSuppress = inBody !== -1 && event.content.body[inBody-1] === "<"
- if (!shouldSuppress && guild?.roles) {
- // Suppress if regular users don't have permission
- const permissions = dUtils.getDefaultPermissions(guild, channel?.permission_overwrites)
- const canEmbedLinks = dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.EmbedLinks)
- shouldSuppress = !canEmbedLinks
- }
- if (shouldSuppress) {
- node.setAttribute("data-suppress", "")
- }
- }
- await forEachNode(event, node.firstChild)
- }
- }
- await forEachNode(event, root)
- if (perMessageProfile?.has_fallback) root.querySelectorAll("[data-mx-profile-fallback]").forEach(x => x.remove())
-
- // SPRITE SHEET EMOJIS FEATURE: Emojis at the end of the message that we don't know about will be reuploaded as a sprite sheet.
- // First we need to determine which emojis are at the end.
- endOfMessageEmojis = []
- let match
- let last = input.length
- while ((match = input.slice(0, last).match(/
]*>\s*$/))) {
- if (!match[0].includes("data-mx-emoticon")) break
- const mxcUrl = match[0].match(/\bsrc="(mxc:\/\/[^"]+)"/)
- if (mxcUrl) endOfMessageEmojis.unshift(mxcUrl[1])
- assert(typeof match.index === "number", "Your JavaScript implementation does not comply with TC39: https://tc39.es/ecma262/multipage/text-processing.html#sec-regexpbuiltinexec")
- last = match.index
- }
-
- // @ts-ignore bad type from turndown
- content = turndownService.turndown(root)
-
- // Put < > around any surviving matrix.to links to hide the URL previews
- content = content.replace(/\bhttps?:\/\/matrix\.to\/[^<>\n )]*/g, "<$&>")
-
- // It's designed for commonmark, we need to replace the space-space-newline with just newline
- content = content.replace(/ \n/g, "\n")
-
- // If there's a blockquote at the start of the message body and this message is a reply, they should be visually separated
- if (replyLine && content.startsWith("> ")) content = "\n" + content
-
- // SPRITE SHEET EMOJIS FEATURE:
- content = await linkEndOfMessageSpriteSheet(content)
- } else {
- // Looks like we're using the plaintext body!
- content = event.content.body
- if (perMessageProfile?.has_fallback && perMessageProfile.displayname && content.startsWith(perMessageProfile.displayname + ": ")) {
- // Strip the display name prefix fallback added for clients that don't support per-message profiles
- content = content.slice(perMessageProfile.displayname.length + 2)
- }
-
- if (event.content.msgtype === "m.emote") {
- content = `* ${displayName} ${content}`
- }
-
- content = await handleRoomOrMessageLinks(content, di) // Replace matrix.to links with discord.com equivalents where possible
-
- let offset = 0
- for (const match of [...content.matchAll(/\bhttps?:\/\/[^ )>\n]+/g)]) {
- assert(typeof match.index === "number")
-
- // Respect sender's angle brackets
- const alreadySuppressed = content[match.index-1+offset] === "<" && content[match.index+match.length+offset] === ">"
- if (alreadySuppressed) continue
-
- // Suppress matrix.to links always
- let shouldSuppress = !!match[0].match(/^https?:\/\/matrix\.to\//)
-
- // Suppress if regular users don't have permission
- if (!shouldSuppress && guild?.roles) {
- const permissions = dUtils.getDefaultPermissions(guild, channel.permission_overwrites)
- const canEmbedLinks = dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.EmbedLinks)
- shouldSuppress = !canEmbedLinks
- }
-
- if (shouldSuppress) {
- content = content.slice(0, match.index + offset) + "<" + match[0] + ">" + content.slice(match.index + match[0].length + offset)
- offset += 2
- }
- }
-
- const result = await checkWrittenMentions(content, event.sender, event.room_id, guild, di)
- if (result) {
- content = result.content
- ensureJoined.push(...result.ensureJoined)
- allowedMentionsParse.push(...result.allowedMentionsParse)
- }
-
- // 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)
+ 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}`
}
+
+ // Handling mentions of Discord users
+ input = input.replace(/("https:\/\/matrix.to\/#\/((?:@|%40)[^"]+)")>/g, (whole, attributeValue, mxid) => {
+ mxid = decodeURIComponent(mxid)
+ if (mxUtils.eventSenderIsFromDiscord(mxid)) {
+ // Handle mention of an OOYE sim user by their mxid
+ const id = select("sim", "user_id", {mxid}).pluck().get()
+ if (!id) return whole
+ return `${attributeValue} data-user-id="${id}">`
+ } else {
+ // Handle mention of a Matrix user by their mxid
+ // Check if this Matrix user is actually the sim user from another old bridge in the room?
+ const match = mxid.match(/[^:]*discord[^:]*_([0-9]{6,}):/) // try to match @_discord_123456, @_discordpuppet_123456, etc.
+ if (match) return `${attributeValue} data-user-id="${match[1]}">`
+ // Nope, just a real Matrix user.
+ return whole
+ }
+ })
+
+ // Handling mentions of rooms and room-messages
+ input = await handleRoomOrMessageLinks(input, di)
+
+ // Stripping colons after mentions
+ input = input.replace(/( data-user-id.*?<\/a>):?/g, "$1")
+ input = input.replace(/("https:\/\/matrix.to.*?<\/a>):?/g, "$1")
+
+ // Element adds a bunch of
before 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|
\s*)*<\/blockquote>/g, "")
+
+ // 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 "
"
+ }
+ beforeContext = beforeContext || ""
+ beforeTag = beforeTag || ""
+ afterContext = afterContext || ""
+ afterTag = afterTag || ""
+ if (!mxUtils.BLOCK_ELEMENTS.includes(beforeTag.toUpperCase()) && !mxUtils.BLOCK_ELEMENTS.includes(afterTag.toUpperCase())) {
+ return beforeContext + "
" + 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"
+
+ // Handling written @mentions: we need to look for candidate Discord members to join to the room
+ // This shouldn't apply to code blocks, links, or inside attributes. So editing the HTML tree instead of regular expressions is a sensible choice here.
+ // We're using the domino parser because Turndown uses the same and can reuse this tree.
+ const doc = domino.createDocument(
+ // DOM parsers arrange elements in the and . Wrapping in a custom element ensures elements are reliably arranged in a single element.
+ '' + input + ' '
+ );
+ const root = doc.getElementById("turndown-root");
+ async function forEachNode(node) {
+ for (; node; node = node.nextSibling) {
+ // Check written mentions
+ if (node.nodeType === 3 && node.nodeValue.includes("@") && !nodeIsChildOf(node, ["A", "CODE", "PRE"])) {
+ const result = await checkWrittenMentions(node.nodeValue, event.sender, event.room_id, guild, di)
+ if (result) {
+ node.nodeValue = result.content
+ ensureJoined.push(...result.ensureJoined)
+ allowedMentionsParse.push(...result.allowedMentionsParse)
+ }
+ }
+ // Check for incompatible backticks in code blocks
+ let preNode
+ if (node.nodeType === 3 && node.nodeValue.includes("```") && (preNode = nodeIsChildOf(node, ["PRE"]))) {
+ if (preNode.firstChild?.nodeName === "CODE") {
+ const ext = preNode.firstChild.className.match(/language-(\S+)/)?.[1] || "txt"
+ const filename = `inline_code.${ext}`
+ // Build the replacement node
+ const replacementCode = doc.createElement("code")
+ replacementCode.textContent = `[${filename}]`
+ // Build its containing node
+ const replacement = doc.createElement("span")
+ replacement.appendChild(doc.createTextNode(" "))
+ replacement.appendChild(replacementCode)
+ replacement.appendChild(doc.createTextNode(" "))
+ // Replace the code block with the
+ preNode.replaceWith(replacement)
+ // Upload the code as an attachment
+ const content = getCodeContent(preNode.firstChild)
+ attachments.push({id: String(attachments.length), filename})
+ pendingFiles.push({name: filename, buffer: Buffer.from(content, "utf8")})
+ }
+ }
+ await forEachNode(node.firstChild)
+ }
+ }
+ await forEachNode(root)
+
+ // SPRITE SHEET EMOJIS FEATURE: Emojis at the end of the message that we don't know about will be reuploaded as a sprite sheet.
+ // First we need to determine which emojis are at the end.
+ endOfMessageEmojis = []
+ let match
+ let last = input.length
+ while ((match = input.slice(0, last).match(/
]*>\s*$/))) {
+ if (!match[0].includes("data-mx-emoticon")) break
+ const mxcUrl = match[0].match(/\bsrc="(mxc:\/\/[^"]+)"/)
+ if (mxcUrl) endOfMessageEmojis.unshift(mxcUrl[1])
+ assert(typeof match.index === "number", "Your JavaScript implementation does not comply with TC39: https://tc39.es/ecma262/multipage/text-processing.html#sec-regexpbuiltinexec")
+ last = match.index
+ }
+
+ // @ts-ignore bad type from turndown
+ content = turndownService.turndown(root)
+
+ // Put < > around any surviving matrix.to links to hide the URL previews
+ content = content.replace(/\bhttps?:\/\/matrix\.to\/[^<>\n )]*/g, "<$&>")
+
+ // It's designed for commonmark, we need to replace the space-space-newline with just newline
+ content = content.replace(/ \n/g, "\n")
+
+ // If there's a blockquote at the start of the message body and this message is a reply, they should be visually separated
+ if (replyLine && content.startsWith("> ")) content = "\n" + content
+
+ // SPRITE SHEET EMOJIS FEATURE:
+ content = await uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles, di?.mxcDownloader)
+ } else {
+ // Looks like we're using the plaintext body!
+ content = event.content.body
+
+ if (event.content.msgtype === "m.emote") {
+ content = `* ${displayName} ${content}`
+ }
+
+ content = await handleRoomOrMessageLinks(content, di) // Replace matrix.to links with discord.com equivalents where possible
+ content = content.replace(/\bhttps?:\/\/matrix\.to\/[^<>\n )]*/, "<$&>") // Put < > around any surviving matrix.to links to hide the URL previews
+
+ const result = await checkWrittenMentions(content, event.sender, event.room_id, guild, di)
+ if (result) {
+ content = result.content
+ ensureJoined.push(...result.ensureJoined)
+ allowedMentionsParse.push(...result.allowedMentionsParse)
+ }
+
+ // 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)
}
}
- // Complete content
content = displayNameRunoff + replyLine + content
+
// Split into 2000 character chunks
const chunks = chunk(content, 2000)
-
- // If m.mentions is specified and valid, overwrite allowedMentionsParse with a converted m.mentions
- let allowed_mentions = {parse: allowedMentionsParse}
- if (event.content["m.mentions"]) {
- // Combine requested mentions with detected written mentions to get the full list
- if (Array.isArray(event.content["m.mentions"].user_ids)) {
- for (const mxid of event.content["m.mentions"].user_ids) {
- const user_id = select("sim", "user_id", {mxid}).pluck().get()
- if (!user_id) continue
- allowedMentionsUsers.push(
- select("sim_proxy", "proxy_owner_id", {user_id}).pluck().get() || user_id
- )
- }
- }
- // Specific mentions were requested, so do not parse users
- allowed_mentions.parse = allowed_mentions.parse.filter(x => x !== "users")
- allowed_mentions.users = allowedMentionsUsers
- }
-
- // Assemble chunks into Discord messages content
/** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | stream.Readable}[]})[]} */
const messages = chunks.map(content => ({
content,
- allowed_mentions,
+ allowed_mentions: {
+ parse: allowedMentionsParse
+ },
username: displayNameShortened,
avatar_url: avatarURL
}))
@@ -1057,16 +888,6 @@ async function eventToMessage(event, guild, channel, di) {
messages[0].pendingFiles = pendingFiles
}
- if (pollMessages.length) {
- for (const pollMessage of pollMessages) {
- messages.push({
- username: displayNameShortened,
- avatar_url: avatarURL,
- ...pollMessage,
- })
- }
- }
-
const messagesToEdit = []
const messagesToSend = []
for (let i = 0; i < messages.length; i++) {
diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js
index bc73df7..3d1c918 100644
--- a/src/m2d/converters/event-to-message.test.js
+++ b/src/m2d/converters/event-to-message.test.js
@@ -1,11 +1,21 @@
const assert = require("assert").strict
+const fs = require("fs")
const {test} = require("supertape")
-const DiscordTypes = require("discord-api-types/v10")
const {eventToMessage} = require("./event-to-message")
+const {convertImageStream} = require("./emoji-sheet")
const data = require("../../../test/data")
const {MatrixServerError} = require("../../matrix/mreq")
const {select, discord} = require("../../passthrough")
+/* c8 ignore next 7 */
+function slow() {
+ if (process.argv.includes("--slow")) {
+ return test
+ } else {
+ return test.skip
+ }
+}
+
/**
* @param {string} roomID
* @param {string} eventID
@@ -38,6 +48,25 @@ function sameFirstContentAndWhitespace(t, a, b) {
t.equal(a2, b2)
}
+/**
+ * MOCK: Gets the emoji from the filesystem and converts to uncompressed PNG data.
+ * @param {string} mxc a single mxc:// URL
+ * @returns {Promise} uncompressed PNG data, or undefined if the downloaded emoji is not valid
+*/
+async function mockGetAndConvertEmoji(mxc) {
+ const id = mxc.match(/\/([^./]*)$/)?.[1]
+ let s
+ if (fs.existsSync(`test/res/${id}.png`)) {
+ s = fs.createReadStream(`test/res/${id}.png`)
+ } else {
+ s = fs.createReadStream(`test/res/${id}.gif`)
+ }
+ return convertImageStream(s, () => {
+ s.pause()
+ s.emit("end")
+ })
+}
+
test("event2message: body is used when there is no formatted_body", async t => {
t.deepEqual(
await eventToMessage({
@@ -85,7 +114,7 @@ test("event2message: any markdown in body is escaped, except strikethrough", asy
unsigned: {
age: 405299
}
- }, {}, {}, {
+ }, {}, {
snow: {
guild: {
searchGuildMembers: () => []
@@ -265,35 +294,6 @@ test("event2message: markdown in link text does not attempt to be escaped becaus
username: "cadence [they]",
content: "hey [@mario sports mix [she/her]](), is it possible to listen on a unix socket?",
avatar_url: undefined,
- allowed_mentions: {
- parse: ["roles"],
- users: []
- }
- }]
- }
- )
-})
-
-test("event2message: markdown in link url does not attempt to be escaped (plaintext body, not suppressed)", async t => {
- t.deepEqual(
- await eventToMessage({
- content: {
- msgtype: "m.text",
- body: "the wikimedia commons freaks are gonna love this one https://commons.wikimedia.org/wiki/File:Car_covered_in_traffic_cones.jpg"
- },
- event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
- room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
- sender: "@cadence:cadence.moe",
- type: "m.room.message"
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "cadence [they]",
- content: "the wikimedia commons freaks are gonna love this one https://commons.wikimedia.org/wiki/File:Car_covered_in_traffic_cones.jpg",
- avatar_url: undefined,
allowed_mentions: {
parse: ["users", "roles"]
}
@@ -302,260 +302,6 @@ test("event2message: markdown in link url does not attempt to be escaped (plaint
)
})
-test("event2message: markdown in link url does not attempt to be escaped (plaintext body, link suppressed)", async t => {
- t.deepEqual(
- await eventToMessage({
- content: {
- msgtype: "m.text",
- body: "the wikimedia commons freaks are gonna love this one https://commons.wikimedia.org/wiki/File:Car_covered_in_traffic_cones.jpg"
- },
- event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
- room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
- sender: "@cadence:cadence.moe",
- type: "m.room.message"
- }, {
- id: "123",
- roles: [{
- id: "123",
- name: "@everyone",
- permissions: DiscordTypes.PermissionFlagsBits.SendMessages
- }]
- }, {}),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "cadence [they]",
- content: "the wikimedia commons freaks are gonna love this one ",
- avatar_url: undefined,
- allowed_mentions: {
- parse: ["users", "roles"]
- }
- }]
- }
- )
-})
-
-test("event2message: embeds are suppressed if the guild does not have embed links permission (formatted body)", async t => {
- t.deepEqual(
- await eventToMessage({
- content: {
- body: "posting one of my favourite songs recently (starts at timestamp) https://youtu.be/RhV2X7WQMPA?t=364",
- format: "org.matrix.custom.html",
- formatted_body: `posting one of my favourite songs recently (starts at timestamp) https://youtu.be/RhV2X7WQMPA?t=364`,
- msgtype: "m.text"
- },
- event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
- origin_server_ts: 1688301929913,
- room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
- sender: "@cadence:cadence.moe",
- type: "m.room.message",
- }, {
- id: "123",
- roles: [{
- id: "123",
- name: "@everyone",
- permissions: DiscordTypes.PermissionFlagsBits.SendMessages
- }]
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "cadence [they]",
- content: "posting one of my favourite songs recently (starts at timestamp) ",
- avatar_url: undefined,
- allowed_mentions: {
- parse: ["users", "roles"]
- }
- }]
- }
- )
-})
-
-test("event2message: embeds are suppressed if the guild does not have embed links permission (plaintext body)", async t => {
- t.deepEqual(
- await eventToMessage({
- content: {
- body: "posting one of my favourite songs recently (starts at timestamp) https://youtu.be/RhV2X7WQMPA?t=364",
- msgtype: "m.text"
- },
- event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
- origin_server_ts: 1688301929913,
- room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
- sender: "@cadence:cadence.moe",
- type: "m.room.message",
- }, {
- id: "123",
- roles: [{
- id: "123",
- name: "@everyone",
- permissions: DiscordTypes.PermissionFlagsBits.SendMessages
- }]
- }, {}),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "cadence [they]",
- content: "posting one of my favourite songs recently (starts at timestamp) ",
- avatar_url: undefined,
- allowed_mentions: {
- parse: ["users", "roles"]
- }
- }]
- }
- )
-})
-
-test("event2message: embeds are suppressed if the channel does not have embed links permission (plaintext body)", async t => {
- t.deepEqual(
- await eventToMessage({
- content: {
- body: "posting one of my favourite songs recently (starts at timestamp) https://youtu.be/RhV2X7WQMPA?t=364",
- msgtype: "m.text"
- },
- event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
- origin_server_ts: 1688301929913,
- room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
- sender: "@cadence:cadence.moe",
- type: "m.room.message",
- }, {
- id: "123",
- roles: [{
- id: "123",
- name: "@everyone",
- permissions: DiscordTypes.PermissionFlagsBits.SendMessages | DiscordTypes.PermissionFlagsBits.EmbedLinks
- }]
- }, {
- permission_overwrites: [{
- id: "123",
- type: 0,
- deny: String(DiscordTypes.PermissionFlagsBits.EmbedLinks),
- allow: "0"
- }]
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "cadence [they]",
- content: "posting one of my favourite songs recently (starts at timestamp) ",
- avatar_url: undefined,
- allowed_mentions: {
- parse: ["users", "roles"]
- }
- }]
- }
- )
-})
-
-test("event2message: links retain angle brackets (formatted body)", async t => {
- t.deepEqual(
- await eventToMessage({
- content: {
- body: "posting one of my favourite songs recently (starts at timestamp) ",
- format: "org.matrix.custom.html",
- formatted_body: `posting one of my favourite songs recently (starts at timestamp) https://youtu.be/RhV2X7WQMPA?t=364`,
- msgtype: "m.text"
- },
- event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
- origin_server_ts: 1688301929913,
- room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
- sender: "@cadence:cadence.moe",
- type: "m.room.message",
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "cadence [they]",
- content: "posting one of my favourite songs recently (starts at timestamp) ",
- avatar_url: undefined,
- allowed_mentions: {
- parse: ["users", "roles"]
- }
- }]
- }
- )
-})
-
-test("event2message: links retain angle brackets (plaintext body)", async t => {
- t.deepEqual(
- await eventToMessage({
- content: {
- body: "posting one of my favourite songs recently (starts at timestamp) ",
- msgtype: "m.text"
- },
- event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
- origin_server_ts: 1688301929913,
- room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
- sender: "@cadence:cadence.moe",
- type: "m.room.message",
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "cadence [they]",
- content: "posting one of my favourite songs recently (starts at timestamp) ",
- avatar_url: undefined,
- allowed_mentions: {
- parse: ["users", "roles"]
- }
- }]
- }
- )
-})
-
-test("event2message: links don't have angle brackets added by accident", async t => {
- t.deepEqual(
- await eventToMessage({
- "content": {
- "body": "Wanted to automate WG→AWG config enrichment and ended up basically coding a batch INI processor.\nhttps://github.com/Erquint/wgcbp",
- "m.mentions": {},
- "msgtype": "m.text"
- },
- "origin_server_ts": 1767848218369,
- "sender": "@erquint:agiadn.org",
- "type": "m.room.message",
- "unsigned": {
- "membership": "join"
- },
- "event_id": "$DxPjyI88VYsJGKuGmhFivFeKn-i5MEBEnAhabmsBaXQ",
- "room_id": "!zq94fae5bVKUubZLp7:agiadn.org"
- }, {}, {}, {
- api: {
- async getStateEvent(roomID, type, key) {
- return {
- displayname: "Erquint"
- }
- }
- }
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "Erquint",
- content: "Wanted to automate WG→AWG config enrichment and ended up basically coding a batch INI processor.\nhttps://github.com/Erquint/wgcbp",
- avatar_url: undefined,
- allowed_mentions: {
- parse: ["roles"],
- users: []
- }
- }]
- }
- )
-})
-
test("event2message: basic html is converted to markdown", async t => {
t.deepEqual(
await eventToMessage({
@@ -658,135 +404,6 @@ test("event2message: spoiler reasons work", async t => {
)
})
-test("event2message: media spoilers work", async t => {
- t.deepEqual(
- await eventToMessage({
- content: {
- body: "pitstop.png",
- filename: "pitstop.png",
- info: {
- h: 870,
- mimetype: "image/png",
- size: 729990,
- w: 674,
- "xyz.amorgan.blurhash": "UqOMmRM{_Mx[xZaxR*tQ.8ayxtWBRkRkWUWB"
- },
- msgtype: "m.image",
- "page.codeberg.everypizza.msc4193.spoiler": true,
- url: "mxc://agiadn.org/JY5NvEFojTvYDp5znjGIkkQ7Ez7GwsdT"
- },
- origin_server_ts: 1764885561299,
- room_id: "!zq94fae5bVKUubZLp7:agiadn.org",
- sender: "@underscore_x:agiadn.org",
- type: "m.room.message",
- event_id: "$6P7u-lpu2u73ZrHUru2UG1rPfsh8PfYLPK21o3SNIN4",
- user_id: "@underscore_x:agiadn.org"
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "underscore_x",
- content: "",
- avatar_url: undefined,
- attachments: [{id: "0", filename: "SPOILER_pitstop.png"}],
- pendingFiles: [{
- mxc: "mxc://agiadn.org/JY5NvEFojTvYDp5znjGIkkQ7Ez7GwsdT",
- name: "SPOILER_pitstop.png",
- }]
- }]
- }
- )
-})
-
-test("event2message: media spoilers with reason work", async t => {
- t.deepEqual(
- await eventToMessage({
- content: {
- body: "pitstop.png",
- filename: "pitstop.png",
- info: {
- h: 870,
- mimetype: "image/png",
- size: 729990,
- w: 674,
- "xyz.amorgan.blurhash": "UqOMmRM{_Mx[xZaxR*tQ.8ayxtWBRkRkWUWB"
- },
- msgtype: "m.image",
- "page.codeberg.everypizza.msc4193.spoiler": true,
- "page.codeberg.everypizza.msc4193.spoiler.reason": "golden witch solutions",
- url: "mxc://agiadn.org/JY5NvEFojTvYDp5znjGIkkQ7Ez7GwsdT"
- },
- origin_server_ts: 1764885561299,
- room_id: "!zq94fae5bVKUubZLp7:agiadn.org",
- sender: "@underscore_x:agiadn.org",
- type: "m.room.message",
- event_id: "$6P7u-lpu2u73ZrHUru2UG1rPfsh8PfYLPK21o3SNIN4",
- user_id: "@underscore_x:agiadn.org"
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "underscore_x",
- allowed_mentions: {
- parse: ["users", "roles"]
- },
- content: "(Spoiler: golden witch solutions)",
- avatar_url: undefined,
- attachments: [{id: "0", filename: "SPOILER_pitstop.png"}],
- pendingFiles: [{
- mxc: "mxc://agiadn.org/JY5NvEFojTvYDp5znjGIkkQ7Ez7GwsdT",
- name: "SPOILER_pitstop.png",
- }]
- }]
- }
- )
-})
-
-test("event2message: spoiler files too large for Discord are linked and retain reason", async t => {
- t.deepEqual(
- await eventToMessage({
- content: {
- body: "pitstop.png",
- filename: "pitstop.png",
- info: {
- h: 870,
- mimetype: "image/png",
- size: 40000000,
- w: 674,
- "xyz.amorgan.blurhash": "UqOMmRM{_Mx[xZaxR*tQ.8ayxtWBRkRkWUWB"
- },
- msgtype: "m.image",
- "page.codeberg.everypizza.msc4193.spoiler": true,
- "page.codeberg.everypizza.msc4193.spoiler.reason": "golden witch secrets",
- url: "mxc://agiadn.org/JY5NvEFojTvYDp5znjGIkkQ7Ez7GwsdT"
- },
- origin_server_ts: 1764885561299,
- room_id: "!zq94fae5bVKUubZLp7:agiadn.org",
- sender: "@underscore_x:agiadn.org",
- type: "m.room.message",
- event_id: "$6P7u-lpu2u73ZrHUru2UG1rPfsh8PfYLPK21o3SNIN4",
- user_id: "@underscore_x:agiadn.org"
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "underscore_x",
- allowed_mentions: {
- parse: ["users", "roles"]
- },
- content: "(Spoiler: golden witch secrets)\n🖼️ _Uploaded **SPOILER** file: ||[pitstop.png](https://bridge.example.org/download/matrix/agiadn.org/JY5NvEFojTvYDp5znjGIkkQ7Ez7GwsdT )|| (40 MB)_",
- avatar_url: undefined
- }]
- }
- )
-})
-
test("event2message: markdown syntax is escaped", async t => {
t.deepEqual(
await eventToMessage({
@@ -1298,8 +915,7 @@ test("event2message: lists have appropriate line breaks", async t => {
content: `i am not certain what you mean by "already exists with as discord". my goals are\n\n* bridgeing specific channels with existing matrix rooms\n * optionally maybe entire "servers"\n* offering the bridge as a public service`,
avatar_url: undefined,
allowed_mentions: {
- parse: ["roles"],
- users: []
+ parse: ["users", "roles"]
}
}]
}
@@ -1340,8 +956,7 @@ test("event2message: ordered list start attribute works", async t => {
content: `i am not certain what you mean by "already exists with as discord". my goals are\n\n1. bridgeing specific channels with existing matrix rooms\n 2. optionally maybe entire "servers"\n2. offering the bridge as a public service`,
avatar_url: undefined,
allowed_mentions: {
- parse: ["roles"],
- users: []
+ parse: ["users", "roles"]
}
}]
}
@@ -1437,7 +1052,7 @@ test("event2message: rich reply to a sim user", async t => {
},
"event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8",
"room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", {
type: "m.room.message",
@@ -1467,118 +1082,6 @@ test("event2message: rich reply to a sim user", async t => {
)
})
-test("event2message: rich reply to a sim user, explicitly enabling mentions in client", async t => {
- t.deepEqual(
- await eventToMessage({
- "type": "m.room.message",
- "sender": "@cadence:cadence.moe",
- "content": {
- "msgtype": "m.text",
- "body": "> <@_ooye_kyuugryphon:cadence.moe> Slow news day.\n\nTesting this reply, ignore",
- "format": "org.matrix.custom.html",
- "formatted_body": "In reply to @_ooye_kyuugryphon:cadence.moe
Slow news day.
Testing this reply, ignore",
- "m.relates_to": {
- "m.in_reply_to": {
- "event_id": "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04"
- }
- },
- "m.mentions": {
- user_ids: ["@_ooye_kyuugryphon:cadence.moe"]
- }
- },
- "origin_server_ts": 1693029683016,
- "unsigned": {
- "age": 91,
- "transaction_id": "m1693029682894.510"
- },
- "event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8",
- "room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe"
- }, data.guild.general, data.channel.general, {
- api: {
- getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", {
- type: "m.room.message",
- content: {
- msgtype: "m.text",
- body: "Slow news day."
- },
- sender: "@_ooye_kyuugryphon:cadence.moe"
- })
- }
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "cadence [they]",
- content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:"
- + " Slow news day."
- + "\nTesting this reply, ignore",
- avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
- allowed_mentions: {
- parse: ["roles"],
- users: ["111604486476181504"]
- }
- }]
- }
- )
-})
-
-test("event2message: rich reply to a sim user, explicitly disabling mentions in client", async t => {
- t.deepEqual(
- await eventToMessage({
- "type": "m.room.message",
- "sender": "@cadence:cadence.moe",
- "content": {
- "msgtype": "m.text",
- "body": "> <@_ooye_kyuugryphon:cadence.moe> Slow news day.\n\nTesting this reply, ignore",
- "format": "org.matrix.custom.html",
- "formatted_body": "In reply to @_ooye_kyuugryphon:cadence.moe
Slow news day.
Testing this reply, ignore",
- "m.relates_to": {
- "m.in_reply_to": {
- "event_id": "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04"
- }
- },
- "m.mentions": {}
- },
- "origin_server_ts": 1693029683016,
- "unsigned": {
- "age": 91,
- "transaction_id": "m1693029682894.510"
- },
- "event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8",
- "room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe"
- }, data.guild.general, data.channel.general, {
- api: {
- getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", {
- type: "m.room.message",
- content: {
- msgtype: "m.text",
- body: "Slow news day."
- },
- sender: "@_ooye_kyuugryphon:cadence.moe"
- })
- }
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "cadence [they]",
- content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:"
- + " Slow news day."
- + "\nTesting this reply, ignore",
- avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
- allowed_mentions: {
- parse: ["roles"],
- users: []
- }
- }]
- }
- )
-})
-
test("event2message: rich reply to a rich reply to a multi-line message should correctly strip reply fallback", async t => {
t.deepEqual(
await eventToMessage({
@@ -1599,7 +1102,7 @@ test("event2message: rich reply to a rich reply to a multi-line message should c
unsigned: {},
event_id: "$Q5kNrPxGs31LfWOhUul5I03jNjlxKOwRmWVuivaqCHY",
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!kLRqKKUQXcibIMtOpl:cadence.moe", "$A0Rj559NKOh2VndCZSTJXcvgi42gZWVfVQt73wA2Hn0", {
"type": "m.room.message",
@@ -1674,7 +1177,7 @@ test("event2message: rich reply to an already-edited message will quote the new
},
"event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8",
"room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$DSQvWxOBB2DYaei6b83-fb33dQGYt5LJd_s8Nl2a43Q", {
type: "m.room.message",
@@ -1757,7 +1260,7 @@ test("event2message: rich reply to a missing event will quote from formatted_bod
},
"event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8",
"room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
async getEvent(roomID, eventID) {
called++
@@ -1807,7 +1310,7 @@ test("event2message: rich reply to a missing event without formatted_body will u
},
"event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8",
"room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
async getEvent(roomID, eventID) {
called++
@@ -1858,7 +1361,7 @@ test("event2message: rich reply to a missing event and no reply fallback will no
},
"event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8",
"room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
async getEvent(roomID, eventID) {
called++
@@ -1903,7 +1406,7 @@ test("event2message: should avoid using blockquote contents as reply preview in
},
event_id: "$BpGx8_vqHyN6UQDARPDU51ftrlRBhleutRSgpAJJ--g",
room_id: "!fGgIymcYWOqjbSRUdV:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", {
"type": "m.room.message",
@@ -1943,9 +1446,9 @@ test("event2message: should suppress embeds for links in reply preview", async t
sender: "@rnl:cadence.moe",
content: {
msgtype: "m.text",
- body: `> <@cadence:cadence.moe> https://www.youtube.com/watch?v=uX32idb1jMw\n\nEveryone in the comments is like "you're so ahead of the curve" and "can't believe this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`,
+ body: `> <@cadence:cadence.moe> https://www.youtube.com/watch?v=uX32idb1jMw\n\nEveryone in the comments is like "you're so ahead of the curve" and "crazy that this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`,
format: "org.matrix.custom.html",
- formatted_body: `In reply to @cadence:cadence.moe
https://www.youtube.com/watch?v=uX32idb1jMw
Everyone in the comments is like "you're so ahead of the curve" and "can't believe this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`,
+ formatted_body: `In reply to @cadence:cadence.moe
https://www.youtube.com/watch?v=uX32idb1jMw
Everyone in the comments is like "you're so ahead of the curve" and "crazy that this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`,
"m.relates_to": {
"m.in_reply_to": {
event_id: "$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU"
@@ -1954,7 +1457,7 @@ test("event2message: should suppress embeds for links in reply preview", async t
},
event_id: "$0Bs3rbsXaeZmSztGMx1NIsqvOrkXOpIWebN-dqr09i4",
room_id: "!fGgIymcYWOqjbSRUdV:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU", {
"type": "m.room.message",
@@ -1975,7 +1478,7 @@ test("event2message: should suppress embeds for links in reply preview", async t
username: "RNL",
content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1273204543739396116 **Ⓜcadence [they]**:"
+ " "
- + `\nEveryone in the comments is like "you're so ahead of the curve" and "can't believe this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`,
+ + `\nEveryone in the comments is like "you're so ahead of the curve" and "crazy that this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`,
avatar_url: undefined,
allowed_mentions: {
parse: ["users", "roles"]
@@ -2003,7 +1506,7 @@ test("event2message: should include a reply preview when message ends with a blo
},
event_id: "$n6sg1X9rLeMzCYufJTRvaLzFeLQ-oEXjCWkHtRxcem4",
room_id: "!fGgIymcYWOqjbSRUdV:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$uXM2I6w-XMtim14-OSZ_8Z2uQ6MDAZLT37eYIiEU6KQ", {
type: 'm.room.message',
@@ -2092,7 +1595,7 @@ test("event2message: should include a reply preview when replying to a descripti
},
event_id: "$qCOlszCawu5hlnF2a2PGyXeGGvtoNJdXyRAEaTF0waA",
room_id: "!CzvdIdUQXgUjDVKxeU:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!CzvdIdUQXgUjDVKxeU:cadence.moe", "$zJFjTvNn1w_YqpR4o4ISKUFisNRgZcu1KSMI_LADPVQ", {
type: "m.room.message",
@@ -2177,7 +1680,7 @@ test("event2message: entities are not escaped in main message or reply preview",
},
event_id: "$2I7odT9okTdpwDcqOjkJb_A3utdO4V8Cp3LK6-Rvwcs",
room_id: "!fGgIymcYWOqjbSRUdV:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$yIWjZPi6Xk56fBxJwqV4ANs_hYLjnWI2cNKbZ2zwk60", {
type: "m.room.message",
@@ -2229,7 +1732,7 @@ test("event2message: reply preview converts emoji formatting when replying to a
},
event_id: "$bCMLaLiMfoRajaGTgzaxAci-g8hJfkspVJIKwYktnvc",
room_id: "!TqlyQmifxGUggEmdBN:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!TqlyQmifxGUggEmdBN:cadence.moe", "$zmO-dtPO6FubBkDxJZ5YmutPIsG1RgV5JJku-9LeGWs", {
type: "m.room.message",
@@ -2279,7 +1782,7 @@ test("event2message: reply preview can guess custom emoji based on the name if i
},
event_id: "$bCMLaLiMfoRajaGTgzaxAci-g8hJfkspVJIKwYktnvc",
room_id: "!TqlyQmifxGUggEmdBN:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!TqlyQmifxGUggEmdBN:cadence.moe", "$zmO-dtPO6FubBkDxJZ5YmutPIsG1RgV5JJku-9LeGWs", {
type: "m.room.message",
@@ -2329,7 +1832,7 @@ test("event2message: reply preview uses emoji title text when replying to an unk
},
event_id: "$bCMLaLiMfoRajaGTgzaxAci-g8hJfkspVJIKwYktnvc",
room_id: "!TqlyQmifxGUggEmdBN:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!TqlyQmifxGUggEmdBN:cadence.moe", "$zmO-dtPO6FubBkDxJZ5YmutPIsG1RgV5JJku-9LeGWs", {
type: "m.room.message",
@@ -2379,7 +1882,7 @@ test("event2message: reply preview ignores garbage image", async t => {
},
event_id: "$bCMLaLiMfoRajaGTgzaxAci-g8hJfkspVJIKwYktnvc",
room_id: "!TqlyQmifxGUggEmdBN:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!TqlyQmifxGUggEmdBN:cadence.moe", "$zmO-dtPO6FubBkDxJZ5YmutPIsG1RgV5JJku-9LeGWs", {
type: "m.room.message",
@@ -2429,7 +1932,7 @@ test("event2message: reply to empty message doesn't show an extra line or anythi
},
event_id: "$bCMLaLiMfoRajaGTgzaxAci-g8hJfkspVJIKwYktnvc",
room_id: "!TqlyQmifxGUggEmdBN:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!TqlyQmifxGUggEmdBN:cadence.moe", "$zmO-dtPO6FubBkDxJZ5YmutPIsG1RgV5JJku-9LeGWs", {
type: "m.room.message",
@@ -2489,7 +1992,7 @@ test("event2message: editing a rich reply to a sim user", async t => {
},
"event_id": "$XEgssz13q-a7NLO7UZO2Oepq7tSiDBD7YRfr7Xu_QiA",
"room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: (roomID, eventID) => {
assert.ok(eventID === "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04" || eventID === "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8")
@@ -2570,7 +2073,7 @@ test("event2message: editing a plaintext body message", async t => {
},
"event_id": "$KxGwvVNzNcmlVbiI2m5kX-jMFNi3Jle71-uu1j7P7vM",
"room_id": "!BnKuBPCvyfOkhcUjEu:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!BnKuBPCvyfOkhcUjEu:cadence.moe", "$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSs", {
type: "m.room.message",
@@ -2625,7 +2128,7 @@ test("event2message: editing a plaintext message to be longer", async t => {
},
"event_id": "$KxGwvVNzNcmlVbiI2m5kX-jMFNi3Jle71-uu1j7P7vM",
"room_id": "!BnKuBPCvyfOkhcUjEu:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!BnKuBPCvyfOkhcUjEu:cadence.moe", "$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSs", {
type: "m.room.message",
@@ -2687,7 +2190,7 @@ test("event2message: editing a plaintext message to be shorter", async t => {
},
"event_id": "$KxGwvVNzNcmlVbiI2m5kX-jMFNi3Jle71-uu1j7P7vM",
"room_id": "!BnKuBPCvyfOkhcUjEu:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!BnKuBPCvyfOkhcUjEu:cadence.moe", "$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSt", {
type: "m.room.message",
@@ -2746,7 +2249,7 @@ test("event2message: editing a formatted body message", async t => {
},
"event_id": "$KxGwvVNzNcmlVbiI2m5kX-jMFNi3Jle71-uu1j7P7vM",
"room_id": "!BnKuBPCvyfOkhcUjEu:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!BnKuBPCvyfOkhcUjEu:cadence.moe", "$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSs", {
type: "m.room.message",
@@ -2802,7 +2305,7 @@ test("event2message: rich reply to a matrix user's long message with formatting"
},
"event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8",
"room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", {
"type": "m.room.message",
@@ -2857,7 +2360,7 @@ test("event2message: rich reply to an image", async t => {
},
"event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8",
"room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", {
type: "m.room.message",
@@ -2919,7 +2422,7 @@ test("event2message: rich reply to a spoiler should ensure the spoiler is hidden
},
"event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8",
"room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", {
type: "m.room.message",
@@ -2970,7 +2473,7 @@ test("event2message: with layered rich replies, the preview should only be the r
},
event_id: "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8",
room_id: "!fGgIymcYWOqjbSRUdV:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", {
"type": "m.room.message",
@@ -3031,7 +2534,7 @@ test("event2message: if event is a reply and starts with a quote, they should be
},
room_id: "!TqlyQmifxGUggEmdBN:cadence.moe",
event_id: "$nCvtZeBFedYuEavt4OftloCHc0kaFW2ktHCfIOklhjU",
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!TqlyQmifxGUggEmdBN:cadence.moe", "$tTYQcke93fwocsc1K6itwUq85EG0RZ0ksCuIglKioks", {
sender: "@aflower:syndicated.gay",
@@ -3082,7 +2585,7 @@ test("event2message: rich reply to a deleted event", async t => {
},
event_id: "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8",
room_id: "!TqlyQmifxGUggEmdBN:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!TqlyQmifxGUggEmdBN:cadence.moe", "$f-noT-d-Eo_Xgpc05Ww89ErUXku4NwKWYGHLzWKo1kU", {
type: "m.room.message",
@@ -3140,7 +2643,7 @@ test("event2message: rich reply to a state event with no body", async t => {
},
event_id: "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8",
room_id: "!TqlyQmifxGUggEmdBN:cadence.moe"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
getEvent: mockGetEvent(t, "!TqlyQmifxGUggEmdBN:cadence.moe", "$f-noT-d-Eo_Xgpc05Ww89ErUXku4NwKWYGHLzWKo1kU", {
type: "m.room.topic",
@@ -3168,99 +2671,6 @@ test("event2message: rich reply to a state event with no body", async t => {
)
})
-test("event2message: rich reply with an image", async t => {
- let called = 0
- t.deepEqual(
- await eventToMessage({
- type: "m.room.message",
- sender: "@cadence:cadence.moe",
- content: {
- body: "image.png",
- info: {
- size: 470379,
- mimetype: "image/png",
- thumbnail_info: {
- w: 800,
- h: 450,
- mimetype: "image/png",
- size: 183014
- },
- w: 1920,
- h: 1080,
- "xyz.amorgan.blurhash": "L24_wtVt00xuxvR%NFX74Toz?waL",
- thumbnail_url: "mxc://cadence.moe/lPtnjlleowWCXGOHKVDyoXGn"
- },
- msgtype: "m.image",
- "m.relates_to": {
- "m.in_reply_to": {
- event_id: "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4"
- }
- },
- url: "mxc://cadence.moe/yxMobQMbSqNHpajxgSHtaooG"
- },
- origin_server_ts: 1764127662631,
- unsigned: {
- membership: "join",
- age: 97,
- transaction_id: "m1764127662540.2"
- },
- event_id: "$QOxkw7u8vjTrrdKxEUO13JWSixV7UXAZU1freT1SkHc",
- room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
- }, data.guild.general, data.channel.general, {
- api: {
- getEvent(roomID, eventID) {
- called++
- t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
- t.equal(eventID, "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4")
- return {
- type: "m.room.message",
- sender: "@cadence:cadence.moe",
- content: {
- msgtype: "m.text",
- body: "you have to check every diff above insane on this set https://osu.ppy.sh/beatmapsets/2263303#osu/4826296"
- },
- origin_server_ts: 1763639396419,
- unsigned: {
- membership: "join",
- age: 486586696,
- transaction_id: "m1763639396324.578"
- },
- event_id: "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4",
- room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
- }
- }
- }
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [
- {
- content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/112760669178241024/1128118177155526666 **Ⓜcadence [they]**: you have to check every diff above insane on this...",
- allowed_mentions: {
- parse: ["users", "roles"]
- },
- attachments: [
- {
- filename: "image.png",
- id: "0",
- },
- ],
- avatar_url: undefined,
- pendingFiles: [
- {
- mxc: "mxc://cadence.moe/yxMobQMbSqNHpajxgSHtaooG",
- name: "image.png",
- },
- ],
- username: "cadence [they]",
- },
- ]
- }
- )
-})
-
test("event2message: raw mentioning discord users in plaintext body works", async t => {
t.deepEqual(
await eventToMessage({
@@ -3463,47 +2873,6 @@ test("event2message: mentioning matrix users works", async t => {
)
})
-test("event2message: matrix mentions are not double-escaped when embed links permission is denied", async t => {
- t.deepEqual(
- await eventToMessage({
- content: {
- msgtype: "m.text",
- body: "wrong body",
- format: "org.matrix.custom.html",
- formatted_body: `I'm just ▲ testing mentions`
- },
- event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
- origin_server_ts: 1688301929913,
- room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
- sender: "@cadence:cadence.moe",
- type: "m.room.message",
- unsigned: {
- age: 405299
- }
- }, {
- id: "123",
- roles: [{
- id: "123",
- name: "@everyone",
- permissions: DiscordTypes.PermissionFlagsBits.SendMessages
- }]
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "cadence [they]",
- content: "I'm just [@▲]() testing mentions",
- avatar_url: undefined,
- allowed_mentions: {
- parse: ["users", "roles"]
- }
- }]
- }
- )
-})
-
test("event2message: multiple mentions are both escaped", async t => {
t.deepEqual(
await eventToMessage({
@@ -3652,7 +3021,7 @@ test("event2message: mentioning bridged rooms by alias works", async t => {
unsigned: {
age: 405299
}
- }, {}, {}, {
+ }, {}, {
api: {
async getAlias(alias) {
called++
@@ -3694,7 +3063,7 @@ test("event2message: mentioning bridged rooms by alias works (plaintext body)",
unsigned: {
age: 405299
}
- }, {}, {}, {
+ }, {}, {
api: {
async getAlias(alias) {
called++
@@ -3736,7 +3105,7 @@ test("event2message: mentioning bridged rooms by alias skips the link when alias
unsigned: {
age: 405299
}
- }, {}, {}, {
+ }, {}, {
api: {
async getAlias(alias) {
called++
@@ -3913,7 +3282,7 @@ test("event2message: mentioning unknown bridged events can approximate with time
unsigned: {
age: 405299
}
- }, {}, {}, {
+ }, {}, {
api: {
async getEvent(roomID, eventID) {
called++
@@ -3960,7 +3329,7 @@ test("event2message: mentioning events falls back to original link when server d
unsigned: {
age: 405299
}
- }, {}, {}, {
+ }, {}, {
api: {
async getEvent(roomID, eventID) {
called++
@@ -4006,7 +3375,7 @@ test("event2message: mentioning events falls back to original link when the chan
unsigned: {
age: 405299
}
- }, {}, {}, {
+ }, {}, {
api: {
/* c8 ignore next 3 */
async getEvent() {
@@ -4168,7 +3537,7 @@ test("event2message: caches the member if the member is not known", async t => {
unsigned: {
age: 405299
}
- }, {}, {}, {
+ }, {}, {
api: {
getStateEvent: async (roomID, type, stateKey) => {
called++
@@ -4218,7 +3587,7 @@ test("event2message: does not cache the member if the room is not known", async
unsigned: {
age: 405299
}
- }, {}, {}, {
+ }, {}, {
api: {
getStateEvent: async (roomID, type, stateKey) => {
called++
@@ -4266,7 +3635,7 @@ test("event2message: skips caching the member if the member does not exist, some
unsigned: {
age: 405299
}
- }, {}, {}, {
+ }, {}, {
api: {
getStateEvent: async (roomID, type, stateKey) => {
called++
@@ -4311,7 +3680,7 @@ test("event2message: overly long usernames are shifted into the message content"
unsigned: {
age: 405299
}
- }, {}, {}, {
+ }, {}, {
api: {
getStateEvent: async (roomID, type, stateKey) => {
called++
@@ -4672,166 +4041,6 @@ test("event2message: evil encrypted image attachment works", async t => {
)
})
-test("event2message: large attachments are uploaded if the server boost level is sufficient", async t => {
- t.deepEqual(
- await eventToMessage({
- type: "m.room.message",
- sender: "@cadence:cadence.moe",
- content: {
- body: "cool cat.png",
- filename: "cool cat.png",
- info: {
- size: 90_000_000,
- mimetype: "image/png",
- w: 480,
- h: 480,
- "xyz.amorgan.blurhash": "URTHsVaTpdj2eKZgkkkXp{pHl7feo@lSl9Z$"
- },
- msgtype: "m.image",
- url: "mxc://cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"
- },
- event_id: "$CXQy3Wmg1A-gL_xAesC1HQcQTEXwICLdSwwUx55FBTI",
- room_id: "!BnKuBPCvyfOkhcUjEu:cadence.moe"
- }, {
- features: ["MAX_FILE_SIZE_100_MB"]
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "cadence [they]",
- content: "",
- avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
- attachments: [{id: "0", filename: "cool cat.png"}],
- pendingFiles: [{name: "cool cat.png", mxc: "mxc://cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"}]
- }]
- }
- )
-})
-
-test("event2message: files too large for Discord are linked as as URL", async t => {
- t.deepEqual(
- await eventToMessage({
- type: "m.room.message",
- sender: "@cadence:cadence.moe",
- content: {
- body: "cool cat.png",
- filename: "cool cat.png",
- info: {
- size: 40_000_000,
- mimetype: "image/png",
- w: 480,
- h: 480,
- "xyz.amorgan.blurhash": "URTHsVaTpdj2eKZgkkkXp{pHl7feo@lSl9Z$"
- },
- msgtype: "m.image",
- url: "mxc://cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"
- },
- event_id: "$CXQy3Wmg1A-gL_xAesC1HQcQTEXwICLdSwwUx55FBTI",
- room_id: "!BnKuBPCvyfOkhcUjEu:cadence.moe"
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "cadence [they]",
- content: "🖼️ _Uploaded file: [cool cat.png](https://bridge.example.org/download/matrix/cadence.moe/IvxVJFLEuksCNnbojdSIeEvn) (40 MB)_",
- avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
- allowed_mentions: {
- parse: ["users", "roles"]
- }
- }]
- }
- )
-})
-
-test("event2message: files too large for Discord can have a plaintext caption", async t => {
- t.deepEqual(
- await eventToMessage({
- type: "m.room.message",
- sender: "@cadence:cadence.moe",
- content: {
- body: "Cat emoji surrounded by pink hearts",
- filename: "cool cat.png",
- info: {
- size: 40_000_000,
- mimetype: "image/png",
- w: 480,
- h: 480,
- "xyz.amorgan.blurhash": "URTHsVaTpdj2eKZgkkkXp{pHl7feo@lSl9Z$"
- },
- msgtype: "m.image",
- url: "mxc://cadence.moe/IvxVJFLEuksCNnbojdSIeEvn"
- },
- event_id: "$CXQy3Wmg1A-gL_xAesC1HQcQTEXwICLdSwwUx55FBTI",
- room_id: "!BnKuBPCvyfOkhcUjEu:cadence.moe"
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "cadence [they]",
- content: "Cat emoji surrounded by pink hearts\n🖼️ _Uploaded file: [cool cat.png](https://bridge.example.org/download/matrix/cadence.moe/IvxVJFLEuksCNnbojdSIeEvn) (40 MB)_",
- avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
- allowed_mentions: {
- parse: ["users", "roles"]
- }
- }]
- }
- )
-})
-
-test("event2message: files too large for Discord can have a formatted caption", async t => {
- t.deepEqual(
- await eventToMessage({
- content: {
- body: "this event has `formatting`",
- filename: "5740.jpg",
- format: "org.matrix.custom.html",
- formatted_body: "this event has formatting",
- info: {
- h: 1340,
- mimetype: "image/jpeg",
- size: 40_000_000,
- thumbnail_info: {
- h: 670,
- mimetype: "image/jpeg",
- size: 80157,
- w: 540
- },
- thumbnail_url: "mxc://thomcat.rocks/XhLsOCDBYyearsLQgUUrbAvw",
- w: 1080,
- "xyz.amorgan.blurhash": "KHJQG*55ic-.}?0M58J.9v"
- },
- msgtype: "m.image",
- url: "mxc://thomcat.rocks/RTHsXmcMPXmuHqVNsnbKtRbh"
- },
- origin_server_ts: 1740607766895,
- sender: "@cadence:cadence.moe",
- type: "m.room.message",
- event_id: "$NqNqVgukiQm1nynm9vIr9FIq31hZpQ3udOd7cBIW46U",
- room_id: "!BnKuBPCvyfOkhcUjEu:cadence.moe"
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "cadence [they]",
- content: "this event has `formatting`\n🖼️ _Uploaded file: [5740.jpg](https://bridge.example.org/download/matrix/thomcat.rocks/RTHsXmcMPXmuHqVNsnbKtRbh) (40 MB)_",
- avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
- allowed_mentions: {
- parse: ["users", "roles"]
- }
- }]
- }
- )
-})
-
-
test("event2message: stickers work", async t => {
t.deepEqual(
await eventToMessage({
@@ -4863,17 +4072,17 @@ test("event2message: stickers work", async t => {
messagesToEdit: [],
messagesToSend: [{
username: "cadence [they]",
- content: "[get_real2](https://bridge.example.org/download/sticker/cadence.moe/NyMXQFAAdniImbHzsygScbmN/_.webp)",
+ content: "",
avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
- allowed_mentions: {
- parse: ["users", "roles"]
- }
+ attachments: [{id: "0", filename: "get_real2.gif"}],
+ pendingFiles: [{name: "get_real2.gif", mxc: "mxc://cadence.moe/NyMXQFAAdniImbHzsygScbmN"}]
}]
}
)
})
test("event2message: stickers fetch mimetype from server when mimetype not provided", async t => {
+ let called = 0
t.deepEqual(
await eventToMessage({
type: "m.sticker",
@@ -4884,6 +4093,20 @@ test("event2message: stickers fetch mimetype from server when mimetype not provi
},
event_id: "$mL-eEVWCwOvFtoOiivDP7gepvf-fTYH6_ioK82bWDI0",
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
+ }, {}, {
+ api: {
+ async getMedia(mxc, options) {
+ called++
+ t.equal(mxc, "mxc://cadence.moe/ybOWQCaXysnyUGuUCaQlTGJf")
+ t.equal(options.method, "HEAD")
+ return {
+ status: 200,
+ headers: new Map([
+ ["content-type", "image/gif"]
+ ])
+ }
+ }
+ }
}),
{
ensureJoined: [],
@@ -4891,14 +4114,48 @@ test("event2message: stickers fetch mimetype from server when mimetype not provi
messagesToEdit: [],
messagesToSend: [{
username: "cadence [they]",
- content: "[YESYESYES](https://bridge.example.org/download/sticker/cadence.moe/ybOWQCaXysnyUGuUCaQlTGJf/_.webp)",
+ content: "",
avatar_url: undefined,
- allowed_mentions: {
- parse: ["users", "roles"]
- }
+ attachments: [{id: "0", filename: "YESYESYES.gif"}],
+ pendingFiles: [{name: "YESYESYES.gif", mxc: "mxc://cadence.moe/ybOWQCaXysnyUGuUCaQlTGJf"}]
}]
}
)
+ t.equal(called, 1, "sticker headers should be fetched")
+})
+
+test("event2message: stickers with unknown mimetype are not allowed", async t => {
+ let called = 0
+ try {
+ await eventToMessage({
+ type: "m.sticker",
+ sender: "@cadence:cadence.moe",
+ content: {
+ body: "something",
+ url: "mxc://cadence.moe/ybOWQCaXysnyUGuUCaQlTGJe"
+ },
+ event_id: "$mL-eEVWCwOvFtoOiivDP7gepvf-fTYH6_ioK82bWDI0",
+ room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
+ }, {}, {
+ api: {
+ async getMedia(mxc, options) {
+ called++
+ t.equal(mxc, "mxc://cadence.moe/ybOWQCaXysnyUGuUCaQlTGJe")
+ t.equal(options.method, "HEAD")
+ return {
+ status: 404,
+ headers: new Map([
+ ["content-type", "application/json"]
+ ])
+ }
+ }
+ }
+ })
+ /* c8 ignore next */
+ t.fail("should throw an error")
+ } catch (e) {
+ t.match(e.toString(), "mimetype")
+ }
})
test("event2message: static emojis work", async t => {
@@ -5043,7 +4300,7 @@ test("event2message: guessed @mentions in plaintext may join members to mention"
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
}, {
id: "112760669178241024"
- }, {}, {
+ }, {
snow: {
guild: {
async searchGuildMembers(guildID, options) {
@@ -5096,7 +4353,7 @@ test("event2message: guessed @mentions in formatted body may join members to men
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
}, {
id: "112760669178241024"
- }, {}, {
+ }, {
snow: {
guild: {
async searchGuildMembers(guildID, options) {
@@ -5140,7 +4397,7 @@ test("event2message: guessed @mentions feature will not activate on links or cod
},
event_id: "$u5gSwSzv_ZQS3eM00mnTBCor8nx_A_AwuQz7e59PZk8",
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
- }, {}, {}, {
+ }, {}, {
snow: {
guild: {
/* c8 ignore next 4 */
@@ -5209,9 +4466,9 @@ test("event2message: @room converts to @everyone and is allowed when the room do
},
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
event_id: "$SiXetU9h9Dg-M9Frcw_C6ahnoXZ3QPZe3MVJR5tcB9A"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
- async getStateEvent(roomID, type, key) {
+ getStateEvent(roomID, type, key) {
called++
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
t.equal(type, "m.room.power_levels")
@@ -5222,19 +4479,6 @@ test("event2message: @room converts to @everyone and is allowed when the room do
room: 0
}
}
- },
- async getStateEventOuter(roomID, type, key) {
- t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
- t.equal(type, "m.room.create")
- t.equal(key, "")
- return {
- type: "m.room.create",
- state_key: "",
- sender: "@_ooye_bot:cadence.moe",
- content: {
- room_version: "11"
- }
- }
}
}
}),
@@ -5255,6 +4499,7 @@ test("event2message: @room converts to @everyone and is allowed when the room do
})
test("event2message: @room converts to @everyone but is not allowed when the room restricts who can use it", async t => {
+ let called = 0
t.deepEqual(
await eventToMessage({
type: "m.room.message",
@@ -5267,9 +4512,10 @@ test("event2message: @room converts to @everyone but is not allowed when the roo
},
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
event_id: "$SiXetU9h9Dg-M9Frcw_C6ahnoXZ3QPZe3MVJR5tcB9A"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
- async getStateEvent(roomID, type, key) {
+ getStateEvent(roomID, type, key) {
+ called++
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
t.equal(type, "m.room.power_levels")
t.equal(key, "")
@@ -5279,19 +4525,6 @@ test("event2message: @room converts to @everyone but is not allowed when the roo
room: 20
}
}
- },
- async getStateEventOuter(roomID, type, key) {
- t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
- t.equal(type, "m.room.create")
- t.equal(key, "")
- return {
- type: "m.room.create",
- state_key: "",
- sender: "@_ooye_bot:cadence.moe",
- content: {
- room_version: "11"
- }
- }
}
}
}),
@@ -5312,6 +4545,7 @@ test("event2message: @room converts to @everyone but is not allowed when the roo
})
test("event2message: @room converts to @everyone and is allowed if the user has sufficient power to use it", async t => {
+ let called = 0
t.deepEqual(
await eventToMessage({
type: "m.room.message",
@@ -5324,9 +4558,10 @@ test("event2message: @room converts to @everyone and is allowed if the user has
},
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
event_id: "$SiXetU9h9Dg-M9Frcw_C6ahnoXZ3QPZe3MVJR5tcB9A"
- }, data.guild.general, data.channel.general, {
+ }, data.guild.general, {
api: {
- async getStateEvent(roomID, type, key) {
+ getStateEvent(roomID, type, key) {
+ called++
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
t.equal(type, "m.room.power_levels")
t.equal(key, "")
@@ -5338,19 +4573,6 @@ test("event2message: @room converts to @everyone and is allowed if the user has
room: 20
}
}
- },
- async getStateEventOuter(roomID, type, key) {
- t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
- t.equal(type, "m.room.create")
- t.equal(key, "")
- return {
- type: "m.room.create",
- state_key: "",
- sender: "@_ooye_bot:cadence.moe",
- content: {
- room_version: "11"
- }
- }
}
}
}),
@@ -5436,257 +4658,102 @@ test("event2message: table", async t => {
)
})
-test("event2message: unknown emoji at the end is used for sprite sheet", async t => {
- t.deepEqual(
- await eventToMessage({
- type: "m.room.message",
- sender: "@cadence:cadence.moe",
- content: {
- msgtype: "m.text",
- body: "wrong body",
- format: "org.matrix.custom.html",
- formatted_body: 'a b
'
- },
- event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
- room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
- }),
- {
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "cadence [they]",
- content: "a b [\u2800](https://bridge.example.org/download/sheet?e=cadence.moe%2FRLMgJGfgTPjIQtvvWZsYjhjy)",
- avatar_url: undefined,
- allowed_mentions: {
- parse: ["users", "roles"]
- }
- }],
- ensureJoined: []
- }
- )
+slow()("event2message: unknown emoji at the end is reuploaded as a sprite sheet", async t => {
+ const messages = await eventToMessage({
+ type: "m.room.message",
+ sender: "@cadence:cadence.moe",
+ content: {
+ msgtype: "m.text",
+ body: "wrong body",
+ format: "org.matrix.custom.html",
+ formatted_body: 'a b
'
+ },
+ event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
+ room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
+ }, {}, {mxcDownloader: mockGetAndConvertEmoji})
+ const testResult = {
+ content: messages.messagesToSend[0].content,
+ fileName: messages.messagesToSend[0].pendingFiles[0].name,
+ fileContentStart: messages.messagesToSend[0].pendingFiles[0].buffer.subarray(0, 90).toString("base64")
+ }
+ t.deepEqual(testResult, {
+ content: "a b",
+ fileName: "emojis.png",
+ fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAPoAAAD6AG1e1JrAAALoklEQVR4nM1ZaVBU2RU+LZSIGnAvFUtcRkSk6abpbkDH"
+ })
})
-test("event2message: known emoji from an unreachable server at the end is used for sprite sheet", async t => {
- t.deepEqual(
- await eventToMessage({
- type: "m.room.message",
- sender: "@cadence:cadence.moe",
- content: {
- msgtype: "m.text",
- body: "wrong body",
- format: "org.matrix.custom.html",
- formatted_body: 'a b
'
- },
- event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
- room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
- }),
- {
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "cadence [they]",
- content: "a b [\u2800](https://bridge.example.org/download/sheet?e=cadence.moe%2FbZFuuUSEebJYXUMSxuuSuLTa)",
- avatar_url: undefined,
- allowed_mentions: {
- parse: ["users", "roles"]
- }
- }],
- ensureJoined: []
- }
- )
+slow()("event2message: known emoji from an unreachable server at the end is reuploaded as a sprite sheet", async t => {
+ const messages = await eventToMessage({
+ type: "m.room.message",
+ sender: "@cadence:cadence.moe",
+ content: {
+ msgtype: "m.text",
+ body: "wrong body",
+ format: "org.matrix.custom.html",
+ formatted_body: 'a b
'
+ },
+ event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
+ room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
+ }, {}, {mxcDownloader: mockGetAndConvertEmoji})
+ const testResult = {
+ content: messages.messagesToSend[0].content,
+ fileName: messages.messagesToSend[0].pendingFiles[0].name,
+ fileContentStart: messages.messagesToSend[0].pendingFiles[0].buffer.subarray(0, 90).toString("base64")
+ }
+ t.deepEqual(testResult, {
+ content: "a b",
+ fileName: "emojis.png",
+ fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAOoUlEQVR4nM1aCXBbx3l+Eu8bN0CAuO+TAHGTFAmAJHgT"
+ })
})
-test("event2message: known and unknown emojis in the end are used for sprite sheet", async t => {
- t.deepEqual(
- await eventToMessage({
- type: "m.room.message",
- sender: "@cadence:cadence.moe",
- content: {
- msgtype: "m.text",
- body: "wrong body",
- format: "org.matrix.custom.html",
- formatted_body: 'known unknown:
and known unknown:
'
- },
- event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
- room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
- }),
- {
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "cadence [they]",
- content: "known unknown: <:hippo:230201364309868544> [:ms_robot_dress:](https://bridge.example.org/download/matrix/cadence.moe/wcouHVjbKJJYajkhJLsyeJAA) and known unknown: [\u2800](https://bridge.example.org/download/sheet?e=cadence.moe%2FWbYqNlACRuicynBfdnPYtmvc&e=cadence.moe%2FHYcztccFIPgevDvoaWNsEtGJ)",
- avatar_url: undefined,
- allowed_mentions: {
- parse: ["users", "roles"]
- }
- }],
- ensureJoined: []
- }
- )
+slow()("event2message: known and unknown emojis in the end are reuploaded as a sprite sheet", async t => {
+ const messages = await eventToMessage({
+ type: "m.room.message",
+ sender: "@cadence:cadence.moe",
+ content: {
+ msgtype: "m.text",
+ body: "wrong body",
+ format: "org.matrix.custom.html",
+ formatted_body: 'known unknown:
and known unknown:
'
+ },
+ event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
+ room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
+ }, {}, {mxcDownloader: mockGetAndConvertEmoji})
+ const testResult = {
+ content: messages.messagesToSend[0].content,
+ fileName: messages.messagesToSend[0].pendingFiles[0].name,
+ fileContentStart: messages.messagesToSend[0].pendingFiles[0].buffer.subarray(0, 90).toString("base64")
+ }
+ t.deepEqual(testResult, {
+ content: "known unknown: <:hippo:230201364309868544> [:ms_robot_dress:](https://bridge.example.org/download/matrix/cadence.moe/wcouHVjbKJJYajkhJLsyeJAA) and known unknown:",
+ fileName: "emojis.png",
+ fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAAGAAAAAwCAYAAADuFn/PAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAeXRFWHRSYXcACklQVEMgcHJvZmlsZQogICAgICA0Ngoz"
+ })
})
-test("event2message: com.beeper.per_message_profile overrides displayname and avatar_url", async t => {
- t.deepEqual(
- await eventToMessage({
- type: "m.room.message",
- sender: "@cadence:cadence.moe",
- content: {
- msgtype: "m.text",
- body: "hello from unstable profile",
- "com.beeper.per_message_profile": {
- id: "custom-id",
- displayname: "Unstable Name",
- avatar_url: "mxc://maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo"
- }
- },
- event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
- room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "Unstable Name",
- content: "hello from unstable profile",
- avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo",
- allowed_mentions: {
- parse: ["users", "roles"]
- }
- }]
- }
- )
-})
-
-test("event2message: com.beeper.per_message_profile empty avatar_url clears avatar", async t => {
- t.deepEqual(
- await eventToMessage({
- type: "m.room.message",
- sender: "@cadence:cadence.moe",
- content: {
- msgtype: "m.text",
- body: "hello with cleared avatar",
- "com.beeper.per_message_profile": {
- id: "no-avatar",
- displayname: "No Avatar User",
- avatar_url: ""
- }
- },
- event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
- room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "No Avatar User",
- content: "hello with cleared avatar",
- avatar_url: undefined,
- allowed_mentions: {
- parse: ["users", "roles"]
- }
- }]
- }
- )
-})
-
-test("event2message: data-mx-profile-fallback element is stripped from formatted_body when per-message profile is present", async t => {
- t.deepEqual(
- await eventToMessage({
- type: "m.room.message",
- sender: "@cadence:cadence.moe",
- content: {
- msgtype: "m.text",
- body: "Tidus Herboren: one more test",
- format: "org.matrix.custom.html",
- formatted_body: "Tidus Herboren: one more test",
- "com.beeper.per_message_profile": {
- id: "tidus",
- displayname: "Tidus Herboren",
- avatar_url: "mxc://maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo",
- has_fallback: true
- }
- },
- event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
- room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "Tidus Herboren",
- content: "one more test",
- avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo",
- allowed_mentions: {
- parse: ["users", "roles"]
- }
- }]
- }
- )
-})
-
-test("event2message: displayname prefix is stripped from plain body when per-message profile has_fallback", async t => {
- t.deepEqual(
- await eventToMessage({
- type: "m.room.message",
- sender: "@cadence:cadence.moe",
- content: {
- msgtype: "m.text",
- body: "Tidus Herboren: one more test",
- "com.beeper.per_message_profile": {
- id: "tidus",
- displayname: "Tidus Herboren",
- has_fallback: true
- }
- },
- event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
- room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "Tidus Herboren",
- content: "one more test",
- avatar_url: undefined,
- allowed_mentions: {
- parse: ["users", "roles"]
- }
- }]
- }
- )
-})
-
-test("event2message: all unknown chess emojis are used for sprite sheet", async t => {
- t.deepEqual(
- await eventToMessage({
- type: "m.room.message",
- sender: "@cadence:cadence.moe",
- content: {
- msgtype: "m.text",
- body: "testing :chess_good_move::chess_incorrect::chess_blund::chess_brilliant_move::chess_blundest::chess_draw_black::chess_good_move::chess_incorrect::chess_blund::chess_brilliant_move::chess_blundest::chess_draw_black:",
- format: "org.matrix.custom.html",
- formatted_body: "testing 










"
- },
- event_id: "$Me6iE8C8CZyrDEOYYrXKSYRuuh_25Jj9kZaNrf7LKr4",
- room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
- }),
- {
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "cadence [they]",
- content: "testing [\u2800](https://bridge.example.org/download/sheet?e=cadence.moe%2FlHfmJpzgoNyNtYHdAmBHxXix&e=cadence.moe%2FMtRdXixoKjKKOyHJGWLsWLNU&e=cadence.moe%2FHXfFuougamkURPPMflTJRxGc&e=cadence.moe%2FikYKbkhGhMERAuPPbsnQzZiX&e=cadence.moe%2FAYPpqXzVJvZdzMQJGjioIQBZ&e=cadence.moe%2FUVuzvpVUhqjiueMxYXJiFEAj&e=cadence.moe%2FlHfmJpzgoNyNtYHdAmBHxXix&e=cadence.moe%2FMtRdXixoKjKKOyHJGWLsWLNU&e=cadence.moe%2FHXfFuougamkURPPMflTJRxGc&e=cadence.moe%2FikYKbkhGhMERAuPPbsnQzZiX&e=cadence.moe%2FAYPpqXzVJvZdzMQJGjioIQBZ&e=cadence.moe%2FUVuzvpVUhqjiueMxYXJiFEAj)",
- avatar_url: undefined,
- allowed_mentions: {
- parse: ["users", "roles"]
- }
- }],
- ensureJoined: []
- }
- )
+slow()("event2message: all unknown chess emojis are reuploaded as a sprite sheet", async t => {
+ const messages = await eventToMessage({
+ type: "m.room.message",
+ sender: "@cadence:cadence.moe",
+ content: {
+ msgtype: "m.text",
+ body: "testing :chess_good_move::chess_incorrect::chess_blund::chess_brilliant_move::chess_blundest::chess_draw_black::chess_good_move::chess_incorrect::chess_blund::chess_brilliant_move::chess_blundest::chess_draw_black:",
+ format: "org.matrix.custom.html",
+ formatted_body: "testing 










"
+ },
+ event_id: "$Me6iE8C8CZyrDEOYYrXKSYRuuh_25Jj9kZaNrf7LKr4",
+ room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
+ }, {}, {mxcDownloader: mockGetAndConvertEmoji})
+ const testResult = {
+ content: messages.messagesToSend[0].content,
+ fileName: messages.messagesToSend[0].pendingFiles[0].name,
+ fileContentStart: messages.messagesToSend[0].pendingFiles[0].buffer.subarray(0, 90).toString("base64")
+ }
+ t.deepEqual(testResult, {
+ content: "testing",
+ fileName: "emojis.png",
+ fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAAYAAAABgCAYAAAAU9KWJAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48"
+ })
})
diff --git a/src/m2d/converters/poll-components.js b/src/m2d/converters/poll-components.js
deleted file mode 100644
index a8233e0..0000000
--- a/src/m2d/converters/poll-components.js
+++ /dev/null
@@ -1,227 +0,0 @@
-// @ts-check
-
-const assert = require("assert").strict
-const DiscordTypes = require("discord-api-types/v10")
-const {sync, db, discord, select, from} = require("../../passthrough")
-
-/** @type {import("../actions/setup-emojis")} */
-const setupEmojis = sync.require("../actions/setup-emojis")
-
-/**
- * @param {{count: number}[]} topAnswers
- * @param {number} count
- * @returns {string}
- */
-function getMedal(topAnswers, count) {
- const winningOrTied = count && topAnswers[0].count === count
- const secondOrTied = !winningOrTied && count && topAnswers[1]?.count === count && topAnswers.slice(-1)[0].count !== count
- const thirdOrTied = !winningOrTied && !secondOrTied && count && topAnswers[2]?.count === count && topAnswers.slice(-1)[0].count !== count
- const medal =
- ( winningOrTied ? "🥇"
- : secondOrTied ? "🥈"
- : thirdOrTied ? "🥉"
- : "")
- return medal
-}
-
-/**
- * @param {boolean} isClosed
- * @param {{matrix_option: string, option_text: string, count: number}[]} pollOptions already sorted correctly
- * @returns {DiscordTypes.APIMessageTopLevelComponent[]}
-*/
-function optionsToComponents(isClosed, pollOptions) {
- const topAnswers = pollOptions.toSorted((a, b) => b.count - a.count)
- /** @type {DiscordTypes.APIMessageTopLevelComponent[]} */
- return pollOptions.map(option => {
- const medal = getMedal(topAnswers, option.count)
- return {
- type: DiscordTypes.ComponentType.Container,
- components: [{
- type: DiscordTypes.ComponentType.Section,
- components: [{
- type: DiscordTypes.ComponentType.TextDisplay,
- content: medal && isClosed ? `${medal} ${option.option_text}` : option.option_text
- }],
- accessory: {
- type: DiscordTypes.ComponentType.Button,
- style: medal === "🥇" && isClosed ? DiscordTypes.ButtonStyle.Success : DiscordTypes.ButtonStyle.Secondary,
- label: option.count.toString(),
- custom_id: `POLL_OPTION#${option.matrix_option}`,
- disabled: isClosed
- }
- }]
- }
- })
-}
-
-/**
- * @param {number} maxSelections
- * @param {number} optionCount
- */
-function getMultiSelectString(maxSelections, optionCount) {
- if (maxSelections === 1) {
- return "Select one answer"
- } else if (maxSelections >= optionCount) {
- return "Select one or more answers"
- } else {
- return `Select up to ${maxSelections} answers`
- }
-}
-
-/**
- * @param {number} maxSelections
- * @param {number} optionCount
- */
-function getMultiSelectClosedString(maxSelections, optionCount) {
- if (maxSelections === 1) {
- return "Single choice"
- } else if (maxSelections >= optionCount) {
- return "Multiple choice"
- } else {
- return `Multiple choice (up to ${maxSelections})`
- }
-}
-
-/**
- * @param {boolean} isClosed
- * @param {number} maxSelections
- * @param {string} questionText
- * @param {{matrix_option: string, option_text: string, count: number}[]} pollOptions already sorted correctly
- * @returns {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody}
- */
-function getPollComponents(isClosed, maxSelections, questionText, pollOptions) {
- /** @type {DiscordTypes.APIMessageTopLevelComponent[]} array because it can move around */
- const multiSelectInfoComponent = [{
- type: DiscordTypes.ComponentType.TextDisplay,
- content: isClosed ? `-# ${getMultiSelectClosedString(maxSelections, pollOptions.length)}` : `-# ${getMultiSelectString(maxSelections, pollOptions.length)}`
- }]
- /** @type {DiscordTypes.APIMessageTopLevelComponent} */
- let headingComponent
- if (isClosed) {
- headingComponent = { // This one is for the poll heading.
- type: DiscordTypes.ComponentType.Section,
- components: [
- {
- type: DiscordTypes.ComponentType.TextDisplay,
- content: `## ${questionText}`
- }
- ],
- accessory: {
- type: DiscordTypes.ComponentType.Button,
- style: DiscordTypes.ButtonStyle.Secondary,
- custom_id: "POLL_VOTE",
- label: "Voting closed",
- disabled: true
- }
- }
- } else {
- headingComponent = { // This one is for the poll heading.
- type: DiscordTypes.ComponentType.Section,
- components: [
- {
- type: DiscordTypes.ComponentType.TextDisplay,
- content: `## ${questionText}`
- },
- // @ts-ignore
- multiSelectInfoComponent.pop()
- ],
- accessory: {
- type: DiscordTypes.ComponentType.Button,
- style: DiscordTypes.ButtonStyle.Primary,
- custom_id: "POLL_VOTE",
- label: "Vote!"
- }
- }
- }
- const optionComponents = optionsToComponents(isClosed, pollOptions)
- return {
- flags: DiscordTypes.MessageFlags.IsComponentsV2,
- components: [headingComponent, ...optionComponents, ...multiSelectInfoComponent]
- }
-}
-
-/** @param {string} messageID */
-function getPollComponentsFromDatabase(messageID) {
- const pollRow = select("poll", ["max_selections", "is_closed", "question_text"], {message_id: messageID}).get()
- assert(pollRow)
- /** @type {{matrix_option: string, option_text: string, count: number}[]} */
- const pollResults = db.prepare("SELECT matrix_option, option_text, seq, count(discord_or_matrix_user_id) as count FROM poll_option LEFT JOIN poll_vote USING (message_id, matrix_option) WHERE message_id = ? GROUP BY matrix_option ORDER BY seq").all(messageID)
- return getPollComponents(!!pollRow.is_closed, pollRow.max_selections, pollRow.question_text, pollResults)
-}
-
-/**
- * @param {string} channelID
- * @param {string} messageID
- * @param {string} questionText
- * @param {{matrix_option: string, option_text: string, count: number}[]} pollOptions already sorted correctly
- * @returns {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody}
- */
-function getPollEndMessage(channelID, messageID, questionText, pollOptions) {
- const topAnswers = pollOptions.toSorted((a, b) => b.count - a.count)
- const totalVotes = pollOptions.reduce((a, c) => a + c.count, 0)
- const tied = topAnswers[0].count === topAnswers[1].count
- const titleString = `-# The poll **${questionText}** has closed.`
- let winnerString = ""
- let resultsString = ""
- if (totalVotes == 0) {
- winnerString = "There was no winner"
- } else if (tied) {
- winnerString = "It's a draw!"
- resultsString = `${Math.round((topAnswers[0].count/totalVotes)*100)}%`
- } else {
- const pollWin = select("auto_emoji", ["name", "emoji_id"], {name: "poll_win"}).get()
- winnerString = `${topAnswers[0].option_text} <:${pollWin?.name}:${pollWin?.emoji_id}>`
- resultsString = `Winning answer • ${Math.round((topAnswers[0].count/totalVotes)*100)}%`
- }
- // @ts-ignore
- const guildID = discord.channels.get(channelID).guild_id
- let mainContent = `**${winnerString}**`
- if (resultsString) {
- mainContent += `\n-# ${resultsString}`
- }
- return {
- flags: DiscordTypes.MessageFlags.IsComponentsV2,
- components: [{
- type: DiscordTypes.ComponentType.TextDisplay,
- content: titleString
- }, {
- type: DiscordTypes.ComponentType.Container,
- components: [{
- type: DiscordTypes.ComponentType.Section,
- components: [{
- type: DiscordTypes.ComponentType.TextDisplay,
- content: `**${winnerString}**\n-# ${resultsString}`
- }],
- accessory: {
- type: DiscordTypes.ComponentType.Button,
- style: DiscordTypes.ButtonStyle.Link,
- url: `https://discord.com/channels/${guildID}/${channelID}/${messageID}`,
- label: "View Poll"
- }
- }]
- }]
- }
-}
-
-/**
- * @param {string} channelID
- * @param {string} messageID
- */
-async function getPollEndMessageFromDatabase(channelID, messageID) {
- const pollWin = select("auto_emoji", ["name", "emoji_id"], {name: "poll_win"}).get()
- if (!pollWin) {
- await setupEmojis.setupEmojis()
- }
-
- const pollRow = select("poll", ["max_selections", "question_text"], {message_id: messageID}).get()
- assert(pollRow)
- /** @type {{matrix_option: string, option_text: string, count: number}[]} */
- const pollResults = db.prepare("SELECT matrix_option, option_text, seq, count(discord_or_matrix_user_id) as count FROM poll_option LEFT JOIN poll_vote USING (message_id, matrix_option) WHERE message_id = ? GROUP BY matrix_option ORDER BY seq").all(messageID)
- return getPollEndMessage(channelID, messageID, pollRow.question_text, pollResults)
-}
-
-module.exports.getMultiSelectString = getMultiSelectString
-module.exports.getPollComponents = getPollComponents
-module.exports.getPollComponentsFromDatabase = getPollComponentsFromDatabase
-module.exports.getPollEndMessageFromDatabase = getPollEndMessageFromDatabase
-module.exports.getMedal = getMedal
diff --git a/src/matrix/utils.js b/src/m2d/converters/utils.js
similarity index 50%
rename from src/matrix/utils.js
rename to src/m2d/converters/utils.js
index eee635b..41cb0af 100644
--- a/src/matrix/utils.js
+++ b/src/m2d/converters/utils.js
@@ -1,12 +1,11 @@
// @ts-check
const assert = require("assert").strict
-const Ty = require("../types")
-const {tag} = require("@cloudrac3r/html-template-tag")
-const passthrough = require("../passthrough")
+
+const passthrough = require("../../passthrough")
const {db} = passthrough
-const {reg} = require("./read-registration")
+const {reg} = require("../../matrix/read-registration")
const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex))
/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore
@@ -14,8 +13,6 @@ let hasher = null
// @ts-ignore
require("xxhash-wasm")().then(h => hasher = h)
-const bot = `@${reg.sender_localpart}:${reg.ooye.server_name}`
-
const BLOCK_ELEMENTS = [
"ADDRESS", "ARTICLE", "ASIDE", "AUDIO", "BLOCKQUOTE", "BODY", "CANVAS",
"CENTER", "DD", "DETAILS", "DIR", "DIV", "DL", "DT", "FIELDSET", "FIGCAPTION", "FIGURE",
@@ -60,26 +57,6 @@ function getEventIDHash(eventID) {
return signedHash
}
-class MatrixStringBuilderStack {
- constructor() {
- this.stack = [new MatrixStringBuilder()]
- }
-
- get msb() {
- return this.stack[0]
- }
-
- bump() {
- this.stack.unshift(new MatrixStringBuilder())
- }
-
- shift() {
- const msb = this.stack.shift()
- assert(msb)
- return msb
- }
-}
-
class MatrixStringBuilder {
constructor() {
this.body = ""
@@ -93,7 +70,7 @@ class MatrixStringBuilder {
*/
add(body, formattedBody, condition = true) {
if (condition) {
- if (formattedBody == undefined) formattedBody = tag`${body}`
+ if (formattedBody == undefined) formattedBody = body
this.body += body
this.formattedBody += formattedBody
}
@@ -107,7 +84,7 @@ class MatrixStringBuilder {
*/
addLine(body, formattedBody, condition = true) {
if (condition) {
- if (formattedBody == undefined) formattedBody = tag`${body}`
+ if (formattedBody == undefined) formattedBody = body
if (this.body.length && this.body.slice(-1) !== "\n") this.body += "\n"
this.body += body
const match = this.formattedBody.match(/<\/?([a-zA-Z]+[a-zA-Z0-9]*)[^>]*>\s*$/)
@@ -124,11 +101,10 @@ class MatrixStringBuilder {
*/
addParagraph(body, formattedBody, condition = true) {
if (condition) {
- if (formattedBody == undefined) formattedBody = tag`${body}`
+ if (formattedBody == undefined) formattedBody = body
if (this.body.length && this.body.slice(-1) !== "\n") this.body += "\n\n"
this.body += body
- const match = formattedBody.match(/^<([a-zA-Z]+[a-zA-Z0-9]*)/)
- if (!match || !BLOCK_ELEMENTS.includes(match[1].toUpperCase())) formattedBody = `${formattedBody}
`
+ formattedBody = `${formattedBody}
`
this.formattedBody += formattedBody
}
return this
@@ -151,7 +127,7 @@ class MatrixStringBuilder {
* https://spec.matrix.org/v1.9/appendices/#routing
* https://gitdab.com/cadence/out-of-your-element/issues/11
* @param {string} roomID
- * @param {{[K in "getStateEvent" | "getStateEventOuter" | "getJoinedMembers"]: import("./api")[K]} | {getEffectivePower: (roomID: string, mxids: string[], api: any) => Promise<{powers: Record, allCreators: string[], tombstone: number, roomCreate: Ty.Event.StateOuter, powerLevels: Ty.Event.M_Power_Levels}>, getJoinedMembers: import("./api")["getJoinedMembers"]}} api
+ * @param {{[K in "getStateEvent" | "getJoinedMembers"]: import("../../matrix/api")[K]}} api
*/
async function getViaServers(roomID, api) {
const candidates = []
@@ -160,20 +136,25 @@ async function getViaServers(roomID, api) {
candidates.push(reg.ooye.server_name)
// Candidate 1: Highest joined non-sim non-bot power level user in the room
// https://github.com/matrix-org/matrix-react-sdk/blob/552c65db98b59406fb49562e537a2721c8505517/src/utils/permalinks/Permalinks.ts#L172
- /* c8 ignore next */
- const call = "getEffectivePower" in api ? api.getEffectivePower(roomID, [bot], api) : getEffectivePower(roomID, [bot], api)
- const {allCreators, powerLevels} = await call
- powerLevels.users ??= {}
- const sorted = allCreators.concat(Object.entries(powerLevels.users).sort((a, b) => b[1] - a[1]).map(([mxid]) => mxid)) // Highest...
- for (const mxid of sorted) {
- if (!(mxid in joined)) continue // joined...
- if (userRegex.some(r => mxid.match(r))) continue // non-sim non-bot...
- const match = mxid.match(/:(.*)/)
- assert(match)
- /* c8 ignore next - should be already covered by the userRegex test, but let's be explicit */
- if (candidates.includes(match[1])) continue // from a different server
- candidates.push(match[1])
- break
+ try {
+ /** @type {{users?: {[mxid: string]: number}}} */
+ const powerLevels = await api.getStateEvent(roomID, "m.room.power_levels", "")
+ if (powerLevels.users) {
+ const sorted = Object.entries(powerLevels.users).sort((a, b) => b[1] - a[1]) // Highest...
+ for (const power of sorted) {
+ const mxid = power[0]
+ if (!(mxid in joined)) continue // joined...
+ if (userRegex.some(r => mxid.match(r))) continue // non-sim non-bot...
+ const match = mxid.match(/:(.*)/)
+ assert(match)
+ if (!candidates.includes(match[1])) {
+ candidates.push(match[1])
+ break
+ }
+ }
+ }
+ } catch (e) {
+ // power levels event not found
}
// Candidates 2-3: Most popular servers in the room
/** @type {Map} */
@@ -213,7 +194,7 @@ async function getViaServers(roomID, api) {
* https://spec.matrix.org/v1.9/appendices/#routing
* https://gitdab.com/cadence/out-of-your-element/issues/11
* @param {string} roomID
- * @param {{[K in "getStateEvent" | "getStateEventOuter" | "getJoinedMembers"]: import("./api")[K]}} api
+ * @param {{[K in "getStateEvent" | "getJoinedMembers"]: import("../../matrix/api")[K]}} api
* @returns {Promise}
*/
async function getViaServersQuery(roomID, api) {
@@ -235,20 +216,10 @@ async function getViaServersQuery(roomID, api) {
* @see https://matrix.org/blog/2024/06/26/sunsetting-unauthenticated-media/ background
* @see https://matrix.org/blog/2024/06/20/matrix-v1.11-release/ implementation details
* @see https://www.sqlite.org/fileformat2.html#record_format SQLite integer field size
- * @param {string | null | undefined} mxc
+ * @param {string} mxc
* @returns {string | undefined}
*/
function getPublicUrlForMxc(mxc) {
- const serverAndMediaID = makeMxcPublic(mxc)
- if(!serverAndMediaID) return undefined
- return `${reg.ooye.bridge_origin}/download/matrix/${serverAndMediaID}`
-}
-
-/**
- * @param {string | null | undefined} mxc
- * @returns {string | undefined} mxc URL with protocol stripped, e.g. "cadence.moe/abcdef1234"
- */
-function makeMxcPublic(mxc) {
assert(hasher, "xxhash is not ready yet")
const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
if (!mediaParts) return undefined
@@ -258,145 +229,13 @@ function makeMxcPublic(mxc) {
const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range
db.prepare("INSERT OR IGNORE INTO media_proxy (permitted_hash) VALUES (?)").run(signedHash)
- return serverAndMediaID
+ return `${reg.ooye.bridge_origin}/download/matrix/${serverAndMediaID}`
}
-/**
- * @param {string} roomVersionString
- * @param {number} desiredVersion
- */
-function roomHasAtLeastVersion(roomVersionString, desiredVersion) {
- /*
- I hate this.
- The spec instructs me to compare room versions ordinally, for example, "In room versions 12 and higher..."
- So if the real room version is 13, this should pass the check.
- However, the spec also says "room versions are not intended to be parsed and should be treated as opaque identifiers", "due to versions not being ordered or hierarchical".
- So versions are unordered and opaque and you can't parse them, but you're still expected to parse them to a number and compare them to another number to measure if it's "12 or higher"?
- Theoretically MSC3244 would clean this up, but that isn't happening since Element removed support for MSC3244: https://github.com/element-hq/element-web/commit/644b8415912afb9c5eed54859a444a2ee7224117
- Element replaced it with the following function:
- */
-
- // Assumption: all unstable room versions don't support the feature. Calling code can check for unstable
- // room versions explicitly if it wants to. The spec reserves [0-9] and `.` for its room versions.
- if (!roomVersionString.match(/^[\d.]+$/)) {
- return false;
- }
-
- // Element dev note: While the spec says room versions are not linear, we can make reasonable assumptions
- // until the room versions prove themselves to be non-linear in the spec. We should see this coming
- // from a mile away and can course-correct this function if needed.
- return Number(roomVersionString) >= Number(desiredVersion);
-}
-
-/**
- * Starting in room version 12, creators may not be specified in power levels users.
- * Modifies the input power levels.
- * @param {Ty.Event.StateOuter} roomCreateOuter
- * @param {Ty.Event.M_Power_Levels} powerLevels
- */
-function removeCreatorsFromPowerLevels(roomCreateOuter, powerLevels) {
- assert(roomCreateOuter.sender)
- if (roomHasAtLeastVersion(roomCreateOuter.content.room_version, 12) && powerLevels.users) {
- for (const creator of (roomCreateOuter.content.additional_creators ?? []).concat(roomCreateOuter.sender)) {
- delete powerLevels.users[creator]
- }
- }
- return powerLevels
-}
-
-/**
- * @template {string} T
- * @param {string} roomID
- * @param {T[]} mxids
- * @param {{[K in "getStateEvent" | "getStateEventOuter"]: import("./api")[K]}} api
- * @returns {Promise<{powers: Record, allCreators: string[], tombstone: number, roomCreate: Ty.Event.StateOuter, powerLevels: Ty.Event.M_Power_Levels}>}
- */
-async function getEffectivePower(roomID, mxids, api) {
- /** @type {[Ty.Event.StateOuter, Ty.Event.M_Power_Levels]} */
- const [roomCreate, powerLevels] = await Promise.all([
- api.getStateEventOuter(roomID, "m.room.create", ""),
- api.getStateEvent(roomID, "m.room.power_levels", "")
- ])
- const allCreators =
- ( roomHasAtLeastVersion(roomCreate.content.room_version, 12) ? (roomCreate.content.additional_creators ?? []).concat(roomCreate.sender)
- : [])
- const tombstone =
- ( roomHasAtLeastVersion(roomCreate.content.room_version, 12) ? powerLevels.events?.["m.room.tombstone"] ?? 150
- : powerLevels.events?.["m.room.tombstone"] ?? powerLevels.state_default ?? 50)
- /** @type {Record} */ // @ts-ignore
- const powers = {}
- for (const mxid of mxids) {
- powers[mxid] =
- ( roomHasAtLeastVersion(roomCreate.content.room_version, 12) && allCreators.includes(mxid) ? Infinity
- : powerLevels.users?.[mxid]
- ?? powerLevels.users_default
- ?? 0)
- }
- return {powers, allCreators, tombstone, roomCreate, powerLevels}
-}
-
-/**
- * Set a user's power level within a room.
- * @param {string} roomID
- * @param {string} mxid
- * @param {number} newPower
- * @param {{[K in "getStateEvent" | "getStateEventOuter" | "sendState"]: import("./api")[K]}} api
- */
-async function setUserPower(roomID, mxid, newPower, api) {
- assert(roomID[0] === "!")
- assert(mxid[0] === "@")
- // Yes there's no shortcut https://github.com/matrix-org/matrix-appservice-bridge/blob/2334b0bae28a285a767fe7244dad59f5a5963037/src/components/intent.ts#L352
- const {powerLevels, powers: {[mxid]: oldPowerLevel, [bot]: botPowerLevel}} = await getEffectivePower(roomID, [mxid, bot], api)
-
- // Check if it has really changed to avoid sending a useless state event
- if (oldPowerLevel === newPower) return
-
- // Bridge bot can't demote equal power users, so need to decide which user will send the event
- const eventSender = oldPowerLevel >= botPowerLevel ? mxid : undefined
-
- // Update the event content
- powerLevels.users ??= {}
- if (newPower == null || newPower === (powerLevels.users_default ?? 0)) {
- delete powerLevels.users[mxid]
- } else {
- powerLevels.users[mxid] = newPower
- }
-
- await api.sendState(roomID, "m.room.power_levels", "", powerLevels, eventSender)
-}
-
-/**
- * Set a user's power level for a whole room hierarchy.
- * @param {string} spaceID
- * @param {string} mxid
- * @param {number} power
- * @param {{[K in "getStateEvent" | "getStateEventOuter" | "sendState" | "generateFullHierarchy"]: import("./api")[K]}} api
- */
-async function setUserPowerCascade(spaceID, mxid, power, api) {
- assert(spaceID[0] === "!")
- assert(mxid[0] === "@")
- let seenSpace = false
- for await (const room of api.generateFullHierarchy(spaceID)) {
- if (room.room_id === spaceID) seenSpace = true
- await setUserPower(room.room_id, mxid, power, api)
- }
- if (!seenSpace) {
- await setUserPower(spaceID, mxid, power, api)
- }
-}
-
-module.exports.bot = bot
module.exports.BLOCK_ELEMENTS = BLOCK_ELEMENTS
module.exports.eventSenderIsFromDiscord = eventSenderIsFromDiscord
-module.exports.makeMxcPublic = makeMxcPublic
module.exports.getPublicUrlForMxc = getPublicUrlForMxc
module.exports.getEventIDHash = getEventIDHash
module.exports.MatrixStringBuilder = MatrixStringBuilder
-module.exports.MatrixStringBuilderStack = MatrixStringBuilderStack
module.exports.getViaServers = getViaServers
module.exports.getViaServersQuery = getViaServersQuery
-module.exports.roomHasAtLeastVersion = roomHasAtLeastVersion
-module.exports.removeCreatorsFromPowerLevels = removeCreatorsFromPowerLevels
-module.exports.getEffectivePower = getEffectivePower
-module.exports.setUserPower = setUserPower
-module.exports.setUserPowerCascade = setUserPowerCascade
diff --git a/src/m2d/converters/utils.test.js b/src/m2d/converters/utils.test.js
new file mode 100644
index 0000000..650f420
--- /dev/null
+++ b/src/m2d/converters/utils.test.js
@@ -0,0 +1,178 @@
+// @ts-check
+
+const e = new Error("Custom error")
+
+const {test} = require("supertape")
+const {eventSenderIsFromDiscord, getEventIDHash, MatrixStringBuilder, getViaServers} = require("./utils")
+const util = require("util")
+
+/** @param {string[]} mxids */
+function joinedList(mxids) {
+ /** @type {{[mxid: string]: {display_name: null, avatar_url: null}}} */
+ const joined = {}
+ for (const mxid of mxids) {
+ joined[mxid] = {
+ display_name: null,
+ avatar_url: null
+ }
+ }
+ return {joined}
+}
+
+test("sender type: matrix user", t => {
+ t.notOk(eventSenderIsFromDiscord("@cadence:cadence.moe"))
+})
+
+test("sender type: ooye bot", t => {
+ t.ok(eventSenderIsFromDiscord("@_ooye_bot:cadence.moe"))
+})
+
+test("sender type: ooye puppet", t => {
+ t.ok(eventSenderIsFromDiscord("@_ooye_sheep:cadence.moe"))
+})
+
+test("event hash: hash is the same each time", t => {
+ const eventID = "$example"
+ t.equal(getEventIDHash(eventID), getEventIDHash(eventID))
+})
+
+test("event hash: hash is different for different inputs", t => {
+ t.notEqual(getEventIDHash("$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe1"), getEventIDHash("$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe2"))
+})
+
+test("MatrixStringBuilder: add, addLine, add same text", t => {
+ const gatewayMessage = {t: "MY_MESSAGE", d: {display: "Custom message data"}}
+ let stackLines = e.stack?.split("\n")
+
+ const builder = new MatrixStringBuilder()
+ builder.addLine("\u26a0 Bridged event from Discord not delivered", "\u26a0 Bridged event from Discord not delivered")
+ builder.addLine(`Gateway event: ${gatewayMessage.t}`)
+ builder.addLine(e.toString())
+ if (stackLines) {
+ stackLines = stackLines.slice(0, 2)
+ stackLines[1] = stackLines[1].replace(/\\/g, "/").replace(/(\s*at ).*(\/m2d\/)/, "$1.$2")
+ builder.addLine(`Error trace:`, `Error trace
`)
+ builder.add(`\n${stackLines.join("\n")}`, `${stackLines.join("\n")}`)
+ }
+ builder.addLine("", `Original payload
${util.inspect(gatewayMessage.d, false, 4, false)}`)
+
+ t.deepEqual(builder.get(), {
+ msgtype: "m.text",
+ body: "\u26a0 Bridged event from Discord not delivered"
+ + "\nGateway event: MY_MESSAGE"
+ + "\nError: Custom error"
+ + "\nError trace:"
+ + "\nError: Custom error"
+ + "\n at ./m2d/converters/utils.test.js:3:11)\n",
+ format: "org.matrix.custom.html",
+ formatted_body: "\u26a0 Bridged event from Discord not delivered"
+ + "
Gateway event: MY_MESSAGE"
+ + "
Error: Custom error"
+ + "
Error trace
Error: Custom error\n at ./m2d/converters/utils.test.js:3:11)
"
+ + `Original payload
{ display: 'Custom message data' }`
+ })
+})
+
+test("MatrixStringBuilder: complete code coverage", t => {
+ const builder = new MatrixStringBuilder()
+ builder.add("Line 1")
+ builder.addParagraph("Line 2")
+ builder.add("Line 3")
+ builder.addParagraph("Line 4")
+
+ t.deepEqual(builder.get(), {
+ msgtype: "m.text",
+ body: "Line 1\n\nLine 2Line 3\n\nLine 4",
+ format: "org.matrix.custom.html",
+ formatted_body: "Line 1Line 2
Line 3Line 4
"
+ })
+})
+
+test("getViaServers: returns the server name if the room only has sim users", async t => {
+ const result = await getViaServers("!baby", {
+ getStateEvent: async () => ({}),
+ getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe"])
+ })
+ t.deepEqual(result, ["cadence.moe"])
+})
+
+test("getViaServers: also returns the most popular servers in order", async t => {
+ const result = await getViaServers("!baby", {
+ getStateEvent: async () => ({}),
+ getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@singleuser:selfhosted.invalid", "@hazel:thecollective.invalid", "@june:thecollective.invalid"])
+ })
+ t.deepEqual(result, ["cadence.moe", "thecollective.invalid", "selfhosted.invalid"])
+})
+
+test("getViaServers: does not return IP address servers", async t => {
+ const result = await getViaServers("!baby", {
+ getStateEvent: async () => ({}),
+ getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:45.77.232.172:8443", "@cadence:[::1]:8443", "@cadence:123example.456example.invalid"])
+ })
+ t.deepEqual(result, ["cadence.moe", "123example.456example.invalid"])
+})
+
+test("getViaServers: also returns the highest power level user (100)", async t => {
+ const result = await getViaServers("!baby", {
+ getStateEvent: async () => ({
+ users: {
+ "@moderator:tractor.invalid": 50,
+ "@singleuser:selfhosted.invalid": 100,
+ "@_ooye_bot:cadence.moe": 100
+ }
+ }),
+ getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@singleuser:selfhosted.invalid", "@hazel:thecollective.invalid", "@june:thecollective.invalid", "@moderator:tractor.invalid"])
+ })
+ t.deepEqual(result, ["cadence.moe", "selfhosted.invalid", "thecollective.invalid", "tractor.invalid"])
+})
+
+test("getViaServers: also returns the highest power level user (50)", async t => {
+ const result = await getViaServers("!baby", {
+ getStateEvent: async () => ({
+ users: {
+ "@moderator:tractor.invalid": 50,
+ "@_ooye_bot:cadence.moe": 100
+ }
+ }),
+ getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@moderator:tractor.invalid", "@hazel:thecollective.invalid", "@june:thecollective.invalid", "@singleuser:selfhosted.invalid"])
+ })
+ t.deepEqual(result, ["cadence.moe", "tractor.invalid", "thecollective.invalid", "selfhosted.invalid"])
+})
+
+test("getViaServers: returns at most 4 results", async t => {
+ const result = await getViaServers("!baby", {
+ getStateEvent: async () => ({
+ users: {
+ "@moderator:tractor.invalid": 50,
+ "@singleuser:selfhosted.invalid": 100,
+ "@_ooye_bot:cadence.moe": 100
+ }
+ }),
+ getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@moderator:tractor.invalid", "@singleuser:selfhosted.invalid", "@hazel:thecollective.invalid", "@cadence:123example.456example.invalid"])
+ })
+ t.deepEqual(result.length, 4)
+})
+
+test("getViaServers: returns results even when power levels can't be fetched", async t => {
+ const result = await getViaServers("!baby", {
+ getStateEvent: async () => {
+ throw new Error("event not found or something")
+ },
+ getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@moderator:tractor.invalid", "@singleuser:selfhosted.invalid", "@hazel:thecollective.invalid", "@cadence:123example.456example.invalid"])
+ })
+ t.deepEqual(result.length, 4)
+})
+
+test("getViaServers: only considers power levels of currently joined members", async t => {
+ const result = await getViaServers("!baby", {
+ getStateEvent: async () => ({
+ users: {
+ "@moderator:tractor.invalid": 50,
+ "@former_moderator:missing.invalid": 100,
+ "@_ooye_bot:cadence.moe": 100
+ }
+ }),
+ getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@moderator:tractor.invalid", "@hazel:thecollective.invalid", "@june:thecollective.invalid", "@singleuser:selfhosted.invalid"])
+ })
+ t.deepEqual(result, ["cadence.moe", "tractor.invalid", "thecollective.invalid", "selfhosted.invalid"])
+})
diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js
index c11b696..ce3638c 100644
--- a/src/m2d/event-dispatcher.js
+++ b/src/m2d/event-dispatcher.js
@@ -7,8 +7,6 @@
const util = require("util")
const Ty = require("../types")
const {discord, db, sync, as, select} = require("../passthrough")
-const {tag} = require("@cloudrac3r/html-template-tag")
-const {Semaphore} = require("@chriscdn/promise-semaphore")
/** @type {import("./actions/send-event")} */
const sendEvent = sync.require("./actions/send-event")
@@ -18,20 +16,14 @@ const addReaction = sync.require("./actions/add-reaction")
const redact = sync.require("./actions/redact")
/** @type {import("./actions/update-pins")}) */
const updatePins = sync.require("./actions/update-pins")
-/** @type {import("./actions/vote")}) */
-const vote = sync.require("./actions/vote")
/** @type {import("../matrix/matrix-command-handler")} */
const matrixCommandHandler = sync.require("../matrix/matrix-command-handler")
-/** @type {import("../matrix/utils")} */
-const utils = sync.require("../matrix/utils")
+/** @type {import("./converters/utils")} */
+const utils = sync.require("./converters/utils")
/** @type {import("../matrix/api")}) */
const api = sync.require("../matrix/api")
/** @type {import("../d2m/actions/create-room")} */
const createRoom = sync.require("../d2m/actions/create-room")
-/** @type {import("../matrix/room-upgrade")} */
-const roomUpgrade = require("../matrix/room-upgrade")
-/** @type {import("../d2m/actions/retrigger")} */
-const retrigger = sync.require("../d2m/actions/retrigger")
const {reg} = require("../matrix/read-registration")
let lastReportedEvent = 0
@@ -88,12 +80,6 @@ function stringifyErrorStack(err, depth = 0) {
return collapsed;
}
-function printError(type, source, e, payload) {
- console.error(`Error while processing a ${type} ${source} event:`)
- console.error(e)
- console.dir(payload, {depth: null})
-}
-
/**
* @param {string} roomID
* @param {"Discord" | "Matrix"} source
@@ -102,9 +88,9 @@ function printError(type, source, e, payload) {
* @param {any} payload
*/
async function sendError(roomID, source, type, e, payload) {
- if (source === "Matrix") {
- printError(type, source, e, payload)
- }
+ console.error(`Error while processing a ${type} ${source} event:`)
+ console.error(e)
+ console.dir(payload, {depth: null})
if (Date.now() - lastReportedEvent < 5000) return null
lastReportedEvent = Date.now()
@@ -135,10 +121,10 @@ async function sendError(roomID, source, type, e, payload) {
// Where
const stack = stringifyErrorStack(e)
- builder.addLine(`Error trace:\n${stack}`, tag`Error trace
${stack}`)
+ builder.addLine(`Error trace:\n${stack}`, `Error trace
${stack}`)
// How
- builder.addLine("", tag`Original payload
${util.inspect(payload, false, 4, false)}`)
+ builder.addLine("", `Original payload
${util.inspect(payload, false, 4, false)}`)
}
// Send
@@ -166,37 +152,34 @@ function guard(type, fn) {
}
}
-const errorRetrySema = new Semaphore()
-
/**
* @param {Ty.Event.Outer} reactionEvent
*/
async function onRetryReactionAdd(reactionEvent) {
const roomID = reactionEvent.room_id
- await errorRetrySema.request(async () => {
- const event = await api.getEvent(roomID, reactionEvent.content["m.relates_to"]?.event_id)
+ const event = await api.getEvent(roomID, reactionEvent.content["m.relates_to"]?.event_id)
- // Check that it's a real error from OOYE
- const error = event.content["moe.cadence.ooye.error"]
- if (event.sender !== `@${reg.sender_localpart}:${reg.ooye.server_name}` || !error) return
+ // Check that it's a real error from OOYE
+ const error = event.content["moe.cadence.ooye.error"]
+ if (event.sender !== `@${reg.sender_localpart}:${reg.ooye.server_name}` || !error) return
- // To stop people injecting misleading messages, the reaction needs to come from either the original sender or a room moderator
- if (reactionEvent.sender !== error.payload.sender) {
- // Check if it's a room moderator
- const {powers: {[reactionEvent.sender]: senderPower}, powerLevels} = await utils.getEffectivePower(roomID, [reactionEvent.sender], api)
- if (senderPower < (powerLevels.state_default ?? 50)) return
- }
+ // To stop people injecting misleading messages, the reaction needs to come from either the original sender or a room moderator
+ if (reactionEvent.sender !== event.sender) {
+ // Check if it's a room moderator
+ const powerLevelsStateContent = await api.getStateEvent(roomID, "m.room.power_levels", "")
+ const powerLevel = powerLevelsStateContent.users?.[reactionEvent.sender] || 0
+ if (powerLevel < 50) return
+ }
- // Retry
- if (error.source === "matrix") {
- as.emit(`type:${error.payload.type}`, error.payload)
- } else if (error.source === "discord") {
- discord.cloud.emit("event", error.payload)
- }
+ // Retry
+ if (error.source === "matrix") {
+ as.emit(`type:${error.payload.type}`, error.payload)
+ } else if (error.source === "discord") {
+ discord.cloud.emit("event", error.payload)
+ }
- // Redact the error to stop people from executing multiple retries
- await api.redactEvent(roomID, event.event_id)
- }, roomID)
+ // Redact the error to stop people from executing multiple retries
+ await api.redactEvent(roomID, event.event_id)
}
sync.addTemporaryListener(as, "type:m.room.message", guard("m.room.message",
@@ -211,7 +194,6 @@ async event => {
// @ts-ignore
await matrixCommandHandler.execute(event)
}
- retrigger.messageFinishedBridging(event.event_id)
await api.ackEvent(event)
}))
@@ -221,55 +203,6 @@ sync.addTemporaryListener(as, "type:m.sticker", guard("m.sticker",
*/
async event => {
if (utils.eventSenderIsFromDiscord(event.sender)) return
- const messageResponses = await sendEvent.sendEvent(event)
- retrigger.messageFinishedBridging(event.event_id)
- await api.ackEvent(event)
-}))
-
-sync.addTemporaryListener(as, "type:org.matrix.msc3381.poll.start", guard("org.matrix.msc3381.poll.start",
-/**
- * @param {Ty.Event.Outer_Org_Matrix_Msc3381_Poll_Start} event it is a org.matrix.msc3381.poll.start because that's what this listener is filtering for
- */
-async event => {
- if (utils.eventSenderIsFromDiscord(event.sender)) return
- const messageResponses = await sendEvent.sendEvent(event)
- await api.ackEvent(event)
-}))
-
-sync.addTemporaryListener(as, "type:org.matrix.msc3381.poll.response", guard("org.matrix.msc3381.poll.response",
-/**
- * @param {Ty.Event.Outer_Org_Matrix_Msc3381_Poll_Response} event it is a org.matrix.msc3381.poll.response because that's what this listener is filtering for
- */
-async event => {
- if (utils.eventSenderIsFromDiscord(event.sender)) return
- await vote.updateVote(event) // Matrix votes can't be bridged, so all we do is store it in the database.
- await api.ackEvent(event)
-}))
-
-sync.addTemporaryListener(as, "type:org.matrix.msc3381.poll.end", guard("org.matrix.msc3381.poll.end",
-/**
- * @param {Ty.Event.Outer_Org_Matrix_Msc3381_Poll_End} event it is a org.matrix.msc3381.poll.end because that's what this listener is filtering for
- */
-async event => {
- if (utils.eventSenderIsFromDiscord(event.sender)) return
- const pollEventID = event.content["m.relates_to"]?.event_id
- if (!pollEventID) return // Validity check
- const messageID = select("event_message", "message_id", {event_id: pollEventID, event_type: "org.matrix.msc3381.poll.start", source: 0}).pluck().get()
- if (!messageID) return // Nothing can be done if the parent message was never bridged. Also, Discord-native polls cannot be ended by others, so this only works for polls started on Matrix.
- try {
- var pollEvent = await api.getEvent(event.room_id, pollEventID) // Poll start event must exist for this to be valid
- } catch (e) {
- return
- }
-
- // According to the rules, the poll end is only allowed if it was sent by the poll starter, or by someone with redact powers.
- if (pollEvent.sender !== event.sender) {
- const {powerLevels, powers: {[event.sender]: enderPower}} = await utils.getEffectivePower(event.room_id, [event.sender], api)
- if (enderPower < (powerLevels.redact ?? 50)) {
- return // Not allowed
- }
- }
-
const messageResponses = await sendEvent.sendEvent(event)
await api.ackEvent(event)
}))
@@ -363,7 +296,15 @@ async event => {
await api.ackEvent(event)
}))
-
+function getFromInviteRoomState(inviteRoomState, nskey, key) {
+ if (!Array.isArray(inviteRoomState)) return null
+ for (const event of inviteRoomState) {
+ if (event.type === nskey && event.state_key === "") {
+ return event.content[key]
+ }
+ }
+ return null
+}
sync.addTemporaryListener(as, "type:m.space.child", guard("m.space.child",
/**
@@ -371,18 +312,7 @@ sync.addTemporaryListener(as, "type:m.space.child", guard("m.space.child",
*/
async event => {
if (Array.isArray(event.content.via) && event.content.via.length) { // space child is being added
- try {
- // try to join if able, it's okay if it doesn't want, bot will still respond to invites
- await api.joinRoom(event.state_key)
- // if autojoined a child space, store it in invite (otherwise the child space will be impossible to use with self-service in the future)
- const hierarchy = await api.getHierarchy(event.state_key, {limit: 1})
- const roomProperties = hierarchy.rooms?.[0]
- if (roomProperties?.room_id === event.state_key && roomProperties.room_type === "m.space" && roomProperties.name) {
- db.prepare("INSERT OR IGNORE INTO invite (mxid, room_id, type, name, topic, avatar) VALUES (?, ?, ?, ?, ?, ?)")
- .run(event.sender, event.state_key, roomProperties.room_type, roomProperties.name, roomProperties.topic, roomProperties.avatar_url)
- await updateMemberCachePowerLevels(event.state_key) // store privileged users in member_cache so they are also allowed to perform self-service
- }
- } catch (e) {}
+ await api.joinRoom(event.state_key).catch(() => {}) // try to join if able, it's okay if it doesn't want, bot will still respond to invites
}
}))
@@ -393,62 +323,47 @@ sync.addTemporaryListener(as, "type:m.room.member", guard("m.room.member",
async event => {
if (event.state_key[0] !== "@") return
- if (event.state_key === utils.bot) {
- const upgraded = await roomUpgrade.onBotMembership(event, api, createRoom)
- if (upgraded) return
- }
-
- if (event.content.membership === "invite" && event.state_key === utils.bot) {
- // Supposed to be here already?
- const guildID = select("guild_space", "guild_id", {space_id: event.room_id}).pluck().get()
- if (guildID) {
- await api.joinRoom(event.room_id)
- return
- }
-
+ if (event.content.membership === "invite" && event.state_key === `@${reg.sender_localpart}:${reg.ooye.server_name}`) {
// We were invited to a room. We should join, and register the invite details for future reference in web.
- try {
- var inviteRoomState = await api.getInviteState(event.room_id, event)
- } catch (e) {
- console.error(e)
- return await api.leaveRoomWithReason(event.room_id, `I wasn't able to find out what this room is. Please report this as a bug. Check console for more details. (${e.toString()})`)
- }
- if (inviteRoomState?.encryption) return await api.leaveRoomWithReason(event.room_id, "Encrypted rooms are not supported for bridging. Please use an unencrypted room.")
- if (!inviteRoomState?.name) return await api.leaveRoomWithReason(event.room_id, `Please only invite me to rooms that have a name/avatar set. Update the room details and reinvite.`)
+ const name = getFromInviteRoomState(event.unsigned?.invite_room_state, "m.room.name", "name")
+ const topic = getFromInviteRoomState(event.unsigned?.invite_room_state, "m.room.topic", "topic")
+ const avatar = getFromInviteRoomState(event.unsigned?.invite_room_state, "m.room.avatar", "url")
+ const creationType = getFromInviteRoomState(event.unsigned?.invite_room_state, "m.room.create", "type")
+ if (!name) return await api.leaveRoomWithReason(event.room_id, "Please only invite me to rooms that have a name/avatar set. Update the room details and reinvite!")
await api.joinRoom(event.room_id)
- db.prepare("REPLACE INTO invite (mxid, room_id, type, name, topic, avatar) VALUES (?, ?, ?, ?, ?, ?)").run(event.sender, event.room_id, inviteRoomState.type, inviteRoomState.name, inviteRoomState.topic, inviteRoomState.avatar)
- if (inviteRoomState.avatar) utils.getPublicUrlForMxc(inviteRoomState.avatar) // make sure it's available in the media_proxy allowed URLs
- await updateMemberCachePowerLevels(event.room_id) // store privileged users in member_cache so they are also allowed to perform self-service
- }
-
- if (event.content.membership === "leave" || event.content.membership === "ban") {
- // Member is gone
- // if Matrix member, data was cached in member_cache
- db.prepare("DELETE FROM member_cache WHERE room_id = ? and mxid = ?").run(event.room_id, event.state_key)
- // if Discord member (so kicked/banned by Matrix user), data was cached in sim_member
- db.prepare("DELETE FROM sim_member WHERE room_id = ? and mxid = ?").run(event.room_id, event.state_key)
-
- // Unregister room's use as a direct chat and/or an invite target if the bot itself left
- if (event.state_key === utils.bot) {
- db.prepare("DELETE FROM direct WHERE room_id = ?").run(event.room_id)
- db.prepare("DELETE FROM invite WHERE room_id = ?").run(event.room_id)
- }
+ db.prepare("INSERT OR IGNORE INTO invite (mxid, room_id, type, name, topic, avatar) VALUES (?, ?, ?, ?, ?, ?)").run(event.sender, event.room_id, creationType, name, topic, avatar)
+ if (avatar) utils.getPublicUrlForMxc(avatar) // make sure it's available in the media_proxy allowed URLs
}
if (utils.eventSenderIsFromDiscord(event.state_key)) return
+ if (event.content.membership === "leave" || event.content.membership === "ban") {
+ // Member is gone
+ db.prepare("DELETE FROM member_cache WHERE room_id = ? and mxid = ?").run(event.room_id, event.state_key)
+
+ // Unregister room's use as a direct chat if the bot itself left
+ const bot = `@${reg.sender_localpart}:${reg.ooye.server_name}`
+ if (event.state_key === bot) {
+ db.prepare("DELETE FROM direct WHERE room_id = ?").run(event.room_id)
+ }
+ }
+
const exists = select("channel_room", "room_id", {room_id: event.room_id}) ?? select("guild_space", "space_id", {space_id: event.room_id})
if (!exists) return // don't cache members in unbridged rooms
// Member is here
- let {powers: {[event.state_key]: memberPower}, tombstone} = await utils.getEffectivePower(event.room_id, [event.state_key], api)
- if (memberPower === Infinity) memberPower = tombstone // database storage compatibility
+ let powerLevel = 0
+ try {
+ /** @type {Ty.Event.M_Power_Levels} */
+ const powerLevelsEvent = await api.getStateEvent(event.room_id, "m.room.power_levels", "")
+ powerLevel = powerLevelsEvent.users?.[event.state_key] ?? powerLevelsEvent.users_default ?? 0
+ } catch (e) {}
const displayname = event.content.displayname || null
const avatar_url = event.content.avatar_url
- db.prepare("INSERT INTO member_cache (room_id, mxid, displayname, avatar_url, power_level) VALUES (?, ?, ?, ?, ?) ON CONFLICT DO UPDATE SET displayname = ?, avatar_url = ?, power_level = ?, missing_profile = NULL").run(
+ db.prepare("INSERT INTO member_cache (room_id, mxid, displayname, avatar_url, power_level) VALUES (?, ?, ?, ?, ?) ON CONFLICT DO UPDATE SET displayname = ?, avatar_url = ?, power_level = ?").run(
event.room_id, event.state_key,
- displayname, avatar_url, memberPower,
- displayname, avatar_url, memberPower
+ displayname, avatar_url, powerLevel,
+ displayname, avatar_url, powerLevel
)
}))
@@ -458,49 +373,12 @@ sync.addTemporaryListener(as, "type:m.room.power_levels", guard("m.room.power_le
*/
async event => {
if (event.state_key !== "") return
- await updateMemberCachePowerLevels(event.room_id)
-}))
-
-/**
- * @param {string} roomID
- */
-async function updateMemberCachePowerLevels(roomID) {
- const existingPower = select("member_cache", "mxid", {room_id: roomID}).pluck().all()
- const {powerLevels, allCreators, tombstone} = await utils.getEffectivePower(roomID, [], api)
- const newPower = powerLevels.users || {}
- const newPowerUsers = Object.keys(newPower)
- const relevantUsers = existingPower.concat(newPowerUsers).concat(allCreators)
- for (const mxid of [...new Set(relevantUsers)]) {
- const level = allCreators.includes(mxid) ? tombstone : newPower[mxid] ?? powerLevels.users_default ?? 0
- db.prepare("INSERT INTO member_cache (room_id, mxid, power_level, missing_profile) VALUES (?, ?, ?, 1) ON CONFLICT DO UPDATE SET power_level = ?")
- .run(roomID, mxid, level, level)
+ const existingPower = select("member_cache", "mxid", {room_id: event.room_id}).pluck().all()
+ const newPower = event.content.users || {}
+ for (const mxid of existingPower) {
+ db.prepare("UPDATE member_cache SET power_level = ? WHERE room_id = ? AND mxid = ?").run(newPower[mxid] || 0, event.room_id, mxid)
}
-}
-
-sync.addTemporaryListener(as, "type:m.room.tombstone", guard("m.room.tombstone",
-/**
- * @param {Ty.Event.StateOuter} event
- */
-async event => {
- if (event.state_key !== "") return
- if (!event.content.replacement_room) return
- await roomUpgrade.onTombstone(event, api)
-}))
-
-sync.addTemporaryListener(as, "type:m.room.encryption", guard("m.room.encryption",
-/**
- * @param {Ty.Event.StateOuter} event
- */
-async event => {
- // Dramatically unbridge rooms if they become encrypted
- if (event.state_key !== "") return
- const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
- if (!channelID) return
- const channel = discord.channels.get(channelID)
- if (!channel) return
- await createRoom.unbridgeChannel(channel, channel["guild_id"], "Encrypted rooms are not supported. This room was removed from the bridge.")
}))
module.exports.stringifyErrorStack = stringifyErrorStack
module.exports.sendError = sendError
-module.exports.printError = printError
diff --git a/src/matrix/api.js b/src/matrix/api.js
index f24f4d9..edffc45 100644
--- a/src/matrix/api.js
+++ b/src/matrix/api.js
@@ -5,7 +5,7 @@ const assert = require("assert").strict
const streamWeb = require("stream/web")
const passthrough = require("../passthrough")
-const {sync, db, select} = passthrough
+const {sync} = passthrough
/** @type {import("./mreq")} */
const mreq = sync.require("./mreq")
/** @type {import("./txnid")} */
@@ -44,7 +44,6 @@ async function register(username) {
try {
await mreq.mreq("POST", "/client/v3/register", {
type: "m.login.application_service",
- inhibit_login: true, // https://github.com/element-hq/matrix-bot-sdk/pull/70/changes https://github.com/matrix-org/matrix-spec-proposals/blob/quenting/as-device-management/proposals/4190-as-device-management.md
username
})
} catch (e) {
@@ -79,17 +78,9 @@ async function joinRoom(roomIDOrAlias, mxid, via) {
}
async function inviteToRoom(roomID, mxidToInvite, mxid) {
- try {
- await mreq.mreq("POST", path(`/client/v3/rooms/${roomID}/invite`, mxid), {
- user_id: mxidToInvite
- })
- } catch (e) {
- if (e.message.includes("is already in the room.") || e.message.includes("cannot invite user that is joined")) {
- // Sweet!
- } else {
- throw e
- }
- }
+ await mreq.mreq("POST", path(`/client/v3/rooms/${roomID}/invite`, mxid), {
+ user_id: mxidToInvite
+ })
}
async function leaveRoom(roomID, mxid) {
@@ -130,20 +121,7 @@ async function getEventForTimestamp(roomID, ts) {
/**
* @param {string} roomID
- * @param {"b" | "f"} dir
- * @param {{from?: string, limit?: any}} [pagination]
- * @param {any} [filter]
- */
-async function getEvents(roomID, dir, pagination = {}, filter) {
- filter = filter && JSON.stringify(filter)
- /** @type {Ty.MessagesPagination>} */
- const root = await mreq.mreq("GET", path(`/client/v3/rooms/${roomID}/messages`, null, {...pagination, dir, filter}))
- return root
-}
-
-/**
- * @param {string} roomID
- * @returns {Promise[]>}
+ * @returns {Promise}
*/
function getAllState(roomID) {
return mreq.mreq("GET", `/client/v3/rooms/${roomID}/state`)
@@ -159,100 +137,6 @@ function getStateEvent(roomID, type, key) {
return mreq.mreq("GET", `/client/v3/rooms/${roomID}/state/${type}/${key}`)
}
-/**
- * @param {string} roomID
- * @param {string} type
- * @param {string} key
- * @returns {Promise>} the entire state event
- */
-function getStateEventOuter(roomID, type, key) {
- return mreq.mreq("GET", `/client/v3/rooms/${roomID}/state/${type}/${key}?format=event`)
-}
-
-/**
- * @param {string} roomID
- * @param {{unsigned?: {invite_room_state?: Ty.Event.InviteStrippedState[]}}} [event]
- * @returns {Promise<{name: string?, topic: string?, avatar: string?, type: string?, encryption: string?}>}
- */
-async function getInviteState(roomID, event) {
- function getFromInviteRoomState(strippedState, nskey, key) {
- if (!Array.isArray(strippedState)) return null
- for (const event of strippedState) {
- if (event.type === nskey && event.state_key === "") {
- return event.content[key]
- }
- }
- return null
- }
-
- // Try extracting from event (if passed)
- if (Array.isArray(event?.unsigned?.invite_room_state) && event.unsigned.invite_room_state.length) {
- return {
- name: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.name", "name"),
- topic: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.topic", "topic"),
- avatar: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.avatar", "url"),
- type: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.create", "type"),
- encryption: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.encryption", "algorithm")
- }
- }
-
- // Try calling sliding sync API and extracting from stripped state
- let root
- try {
- /** @type {Ty.R.SSS} */
- root = await mreq.mreq("POST", path("/client/unstable/org.matrix.simplified_msc3575/sync", `@${reg.sender_localpart}:${reg.ooye.server_name}`, {timeout: "0"}), {
- lists: {
- a: {
- ranges: [[0, 999]],
- timeline_limit: 0,
- required_state: [],
- filters: {
- is_invite: true
- }
- }
- }
- })
-
- // Extract from sliding sync response if valid (seems to be okay on Synapse, Tuwunel and Continuwuity at time of writing)
- if ("lists" in root) {
- if (!root.rooms?.[roomID]) {
- const e = new Error("Room data unavailable via SSS")
- e["data_sss"] = root
- throw e
- }
-
- const roomResponse = root.rooms[roomID]
- const strippedState = "stripped_state" in roomResponse ? roomResponse.stripped_state : roomResponse.invite_state
-
- return {
- name: getFromInviteRoomState(strippedState, "m.room.name", "name"),
- topic: getFromInviteRoomState(strippedState, "m.room.topic", "topic"),
- avatar: getFromInviteRoomState(strippedState, "m.room.avatar", "url"),
- type: getFromInviteRoomState(strippedState, "m.room.create", "type"),
- encryption: getFromInviteRoomState(strippedState, "m.room.encryption", "algorithm")
- }
- }
- } catch (e) {}
-
- // Invalid sliding sync response, try alternative (required for Conduit at time of writing)
- const hierarchy = await getHierarchy(roomID, {limit: 1})
- if (hierarchy?.rooms?.[0]?.room_id === roomID) {
- const room = hierarchy?.rooms?.[0]
- return {
- name: room.name ?? null,
- topic: room.topic ?? null,
- avatar: room.avatar_url ?? null,
- type: room.room_type ?? null,
- encryption: (room.encryption || room["im.nheko.summary.encryption"]) ?? null
- }
- }
-
- const e = new Error("Room data unavailable via SSS/hierarchy")
- e["data_sss"] = root
- e["data_hierarchy"] = hierarchy
- throw e
-}
-
/**
* "Any of the AS's users must be in the room. This API is primarily for Application Services and should be faster to respond than /members as it can be implemented more efficiently on the server."
* @param {string} roomID
@@ -415,33 +299,64 @@ async function sendTyping(roomID, isTyping, mxid, duration) {
})
}
-/**
- * @param {string} mxid
- * @param {string} displayname
- * @param {boolean} [inhibitPropagate]
- */
-async function profileSetDisplayname(mxid, displayname, inhibitPropagate) {
- const params = {}
- if (inhibitPropagate) params["org.matrix.msc4069.propagate"] = false
- await mreq.mreq("PUT", path(`/client/v3/profile/${mxid}/displayname`, mxid, params), {
+async function profileSetDisplayname(mxid, displayname) {
+ await mreq.mreq("PUT", path(`/client/v3/profile/${mxid}/displayname`, mxid), {
displayname
})
}
+async function profileSetAvatarUrl(mxid, avatar_url) {
+ await mreq.mreq("PUT", path(`/client/v3/profile/${mxid}/avatar_url`, mxid), {
+ avatar_url
+ })
+}
+
/**
+ * Set a user's power level within a room.
+ * @param {string} roomID
* @param {string} mxid
- * @param {string | null | undefined} avatar_url
- * @param {boolean} [inhibitPropagate]
+ * @param {number} newPower
*/
-async function profileSetAvatarUrl(mxid, avatar_url, inhibitPropagate) {
- const params = {}
- if (inhibitPropagate) params["org.matrix.msc4069.propagate"] = false
- if (avatar_url) {
- await mreq.mreq("PUT", path(`/client/v3/profile/${mxid}/avatar_url`, mxid, params), {
- avatar_url
- })
+async function setUserPower(roomID, mxid, newPower) {
+ assert(roomID[0] === "!")
+ assert(mxid[0] === "@")
+ // Yes there's no shortcut https://github.com/matrix-org/matrix-appservice-bridge/blob/2334b0bae28a285a767fe7244dad59f5a5963037/src/components/intent.ts#L352
+ const power = await getStateEvent(roomID, "m.room.power_levels", "")
+ power.users = power.users || {}
+
+ // Check if it has really changed to avoid sending a useless state event
+ // (Can't diff kstate here because of (a) circular imports (b) kstate has special behaviour diffing power levels)
+ const oldPowerLevel = power.users?.[mxid] ?? power.users_default ?? 0
+ if (oldPowerLevel === newPower) return
+
+ // Bridge bot can't demote equal power users, so need to decide which user will send the event
+ const botPowerLevel = power.users?.[`@${reg.sender_localpart}:${reg.ooye.server_name}`] ?? power.users_default ?? 0
+ const eventSender = oldPowerLevel >= botPowerLevel ? mxid : undefined
+
+ // Update the event content
+ if (newPower == null || newPower === (power.users_default ?? 0)) {
+ delete power.users[mxid]
} else {
- await mreq.mreq("DELETE", path(`/client/v3/profile/${mxid}/avatar_url`, mxid, params))
+ power.users[mxid] = newPower
+ }
+
+ await sendState(roomID, "m.room.power_levels", "", power, eventSender)
+ return power
+}
+
+/**
+ * Set a user's power level for a whole room hierarchy.
+ * @param {string} spaceID
+ * @param {string} mxid
+ * @param {number} power
+ */
+async function setUserPowerCascade(spaceID, mxid, power) {
+ assert(spaceID[0] === "!")
+ assert(mxid[0] === "@")
+ const rooms = await getFullHierarchy(spaceID)
+ await setUserPower(spaceID, mxid, power)
+ for (const room of rooms) {
+ await setUserPower(room.room_id, mxid, power)
}
}
@@ -463,26 +378,19 @@ async function ping() {
}
/**
- * Given an mxc:// URL, and an optional height for thumbnailing, get the file from the content repository. Returns res.
* @param {string} mxc
- * @param {RequestInit & {height?: number | string}} [init]
+ * @param {RequestInit} [init]
* @return {Promise}>}
*/
async function getMedia(mxc, init = {}) {
const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
assert(mediaParts)
- const downloadOrThumbnail = init.height ? "thumbnail" : "download"
- let url = `${mreq.baseUrl}/client/v1/media/${downloadOrThumbnail}/${mediaParts[1]}/${mediaParts[2]}`
- if (init.height) url += "?" + new URLSearchParams({height: String(init.height), width: String(init.height)})
- const res = await fetch(url, {
+ const res = await fetch(`${mreq.baseUrl}/client/v1/media/download/${mediaParts[1]}/${mediaParts[2]}`, {
headers: {
Authorization: `Bearer ${reg.as_token}`
},
...init
})
- if (res.status !== 200) {
- throw await mreq.makeMatrixServerError(res, {...init, url})
- }
if (init.method !== "HEAD") {
assert(res.body)
}
@@ -557,40 +465,6 @@ function getProfile(mxid) {
return mreq.mreq("GET", `/client/v3/profile/${mxid}`)
}
-function versions() {
- return mreq.mreq("GET", "/client/versions")
-}
-
-/**
- * @param {string} mxid
- */
-async function usePrivateChat(mxid) {
- // Check if we have an existing DM
- let roomID = select("direct", "room_id", {mxid}).pluck().get()
- if (roomID) {
- // Check that the person is/still in the room
- try {
- var member = await getStateEvent(roomID, "m.room.member", mxid)
- } catch (e) {}
-
- // Invite them back to the room if needed
- if (!member || member.membership === "leave") {
- await inviteToRoom(roomID, mxid)
- }
- return roomID
- }
-
- // No existing DM, create a new room and invite
- roomID = await createRoom({
- invite: [mxid],
- is_direct: true,
- preset: "trusted_private_chat"
- })
- // Store the newly created room in the database (not using account data due to awkward bugs with misaligned state)
- db.prepare("REPLACE INTO direct (mxid, room_id) VALUES (?, ?)").run(mxid, roomID)
- return roomID
-}
-
module.exports.path = path
module.exports.register = register
module.exports.createRoom = createRoom
@@ -600,11 +474,8 @@ module.exports.leaveRoom = leaveRoom
module.exports.leaveRoomWithReason = leaveRoomWithReason
module.exports.getEvent = getEvent
module.exports.getEventForTimestamp = getEventForTimestamp
-module.exports.getEvents = getEvents
module.exports.getAllState = getAllState
module.exports.getStateEvent = getStateEvent
-module.exports.getStateEventOuter = getStateEventOuter
-module.exports.getInviteState = getInviteState
module.exports.getJoinedMembers = getJoinedMembers
module.exports.getMembers = getMembers
module.exports.getHierarchy = getHierarchy
@@ -618,6 +489,8 @@ module.exports.redactEvent = redactEvent
module.exports.sendTyping = sendTyping
module.exports.profileSetDisplayname = profileSetDisplayname
module.exports.profileSetAvatarUrl = profileSetAvatarUrl
+module.exports.setUserPower = setUserPower
+module.exports.setUserPowerCascade = setUserPowerCascade
module.exports.ping = ping
module.exports.getMedia = getMedia
module.exports.sendReadReceipt = sendReadReceipt
@@ -627,5 +500,3 @@ module.exports.getAccountData = getAccountData
module.exports.setAccountData = setAccountData
module.exports.setPresence = setPresence
module.exports.getProfile = getProfile
-module.exports.versions = versions
-module.exports.usePrivateChat = usePrivateChat
diff --git a/src/matrix/appservice.js b/src/matrix/appservice.js
index 8f85a51..67f16ee 100644
--- a/src/matrix/appservice.js
+++ b/src/matrix/appservice.js
@@ -3,5 +3,6 @@
const {reg} = require("../matrix/read-registration")
const {AppService} = require("@cloudrac3r/in-your-element")
const as = new AppService(reg)
+as.listen()
module.exports.as = as
diff --git a/src/matrix/file.js b/src/matrix/file.js
index c469aea..2070a56 100644
--- a/src/matrix/file.js
+++ b/src/matrix/file.js
@@ -85,7 +85,6 @@ async function _actuallyUploadDiscordFileToMxc(url) {
writeRegistration(reg)
return root
}
- e.uploadURL = url
throw e
}
}
@@ -104,9 +103,9 @@ function memberAvatar(guildID, user, member) {
}
function emoji(emojiID, animated) {
- const base = `/emojis/${emojiID}.webp`
- if (animated) return base + "?animated=true"
- else return base
+ const base = `/emojis/${emojiID}`
+ if (animated) return base + ".gif"
+ else return base + ".png"
}
const stickerFormat = new Map([
diff --git a/src/matrix/kstate.js b/src/matrix/kstate.js
index 3648f2d..03d09e0 100644
--- a/src/matrix/kstate.js
+++ b/src/matrix/kstate.js
@@ -10,8 +10,6 @@ const {sync} = passthrough
const file = sync.require("./file")
/** @type {import("./api")} */
const api = sync.require("./api")
-/** @type {import("./utils")} */
-const utils = sync.require("./utils")
/** Mutates the input. Not recursive - can only include or exclude entire state events. */
function kstateStripConditionals(kstate) {
@@ -47,7 +45,7 @@ async function kstateUploadMxc(obj) {
return obj
}
-/** Automatically strips conditionals and uploads URLs to mxc. m.room.create is removed. */
+/** Automatically strips conditionals and uploads URLs to mxc. */
async function kstateToState(kstate) {
const events = []
kstateStripConditionals(kstate)
@@ -57,30 +55,19 @@ async function kstateToState(kstate) {
assert(slashIndex > 0)
const type = k.slice(0, slashIndex)
const state_key = k.slice(slashIndex + 1)
- if (type === "m.room.create") continue
events.push({type, state_key, content})
}
return events
}
-/** Extracts m.room.create for use in room creation_content. */
-function kstateToCreationContent(kstate) {
- return kstate["m.room.create/"] || {}
-}
-
/**
- * @param {import("../types").Event.StateOuter[]} events
+ * @param {import("../types").Event.BaseStateEvent[]} events
* @returns {any}
*/
function stateToKState(events) {
const kstate = {}
for (const event of events) {
kstate[event.type + "/" + event.state_key] = event.content
-
- // need to remember m.room.create sender for later...
- if (event.type === "m.room.create" && event.state_key === "") {
- kstate["m.room.create/outer"] = event
- }
}
return kstate
}
@@ -94,27 +81,15 @@ function diffKState(actual, target) {
if (key === "m.room.power_levels/") {
// Special handling for power levels, we want to deep merge the actual and target into the final state.
if (!(key in actual)) throw new Error(`want to apply a power levels diff, but original power level data is missing\nstarted with: ${JSON.stringify(actual)}\nwant to apply: ${JSON.stringify(target)}`)
- // if the diff includes users, it needs to be cleaned wrt room version 12
- const cleanedTarget = mixin({}, target[key])
- if (target[key].users && Object.keys(target[key].users).length > 0) {
- assert("m.room.create/" in actual, `want to apply a power levels diff, but original m.room.create/ is missing\nstarted with: ${JSON.stringify(actual)}\nwant to apply: ${JSON.stringify(target)}`)
- assert("m.room.create/outer" in actual, `want to apply a power levels diff, but original m.room.create/outer is missing\nstarted with: ${JSON.stringify(actual)}\nwant to apply: ${JSON.stringify(target)}`)
- utils.removeCreatorsFromPowerLevels(actual["m.room.create/outer"], cleanedTarget)
- }
- const mixedTarget = mixin({}, actual[key], cleanedTarget)
- if (!isDeepStrictEqual(actual[key], mixedTarget)) {
+ const temp = mixin({}, actual[key], target[key])
+ if (!isDeepStrictEqual(actual[key], temp)) {
// they differ. use the newly prepared object as the diff.
- diff[key] = mixedTarget
+ diff[key] = temp
}
- } else if (key === "m.room.create/") {
- // can't be modified - only for kstateToCreationContent
-
- } else if (key === "m.room.topic/") {
- // synapse generates different m.room.topic events on original creation
- // https://github.com/element-hq/synapse/blob/0f2b29511fd88d1dc2278f41fd6e4e2f2989fcb7/synapse/handlers/room.py#L1729
- // diff the `topic` to determine change
- if (!(key in actual) || actual[key].topic !== target[key].topic) {
+ } else if (key === "chat.schildi.hide_ui/read_receipts") {
+ // Special handling: don't add this key if it's new. Do overwrite if already present.
+ if (key in actual) {
diff[key] = target[key]
}
@@ -140,22 +115,10 @@ function diffKState(actual, target) {
/**
* Async because it gets all room state from the homeserver.
* @param {string} roomID
- * @param {[type: string, key: string][]} [limitToEvents]
*/
-async function roomToKState(roomID, limitToEvents) {
- if (!limitToEvents) {
- const root = await api.getAllState(roomID)
- return stateToKState(root)
- } else {
- const root = []
- await Promise.all(limitToEvents.map(async ([type, key]) => {
- try {
- const outer = await api.getStateEventOuter(roomID, type, key)
- root.push(outer)
- } catch (e) {}
- }))
- return stateToKState(root)
- }
+async function roomToKState(roomID) {
+ const root = await api.getAllState(roomID)
+ return stateToKState(root)
}
/**
@@ -172,7 +135,6 @@ async function applyKStateDiffToRoom(roomID, kstate) {
module.exports.kstateStripConditionals = kstateStripConditionals
module.exports.kstateUploadMxc = kstateUploadMxc
module.exports.kstateToState = kstateToState
-module.exports.kstateToCreationContent = kstateToCreationContent
module.exports.stateToKState = stateToKState
module.exports.diffKState = diffKState
module.exports.roomToKState = roomToKState
diff --git a/src/matrix/kstate.test.js b/src/matrix/kstate.test.js
index b67a725..1b67ad5 100644
--- a/src/matrix/kstate.test.js
+++ b/src/matrix/kstate.test.js
@@ -1,5 +1,5 @@
const assert = require("assert")
-const {kstateToState, stateToKState, diffKState, kstateStripConditionals, kstateUploadMxc, kstateToCreationContent} = require("./kstate")
+const {kstateToState, stateToKState, diffKState, kstateStripConditionals, kstateUploadMxc} = require("./kstate")
const {test} = require("supertape")
test("kstate strip: strips false conditions", t => {
@@ -68,8 +68,6 @@ test("kstateUploadMxc and strip: work together", async t => {
test("kstate2state: general", async t => {
t.deepEqual(await kstateToState({
- "m.room.create/": {bogus: true},
- "m.room.create/outer": {bogus: true},
"m.room.name/": {name: "test name"},
"m.room.member/@cadence:cadence.moe": {membership: "join"},
"uk.half-shot.bridge/org.matrix.appservice-irc://irc/epicord.net/#general": {creator: "@cadence:cadence.moe"}
@@ -100,14 +98,6 @@ test("kstate2state: general", async t => {
test("state2kstate: general", t => {
t.deepEqual(stateToKState([
- {
- type: "m.room.create",
- state_key: "",
- sender: "@example:matrix.org",
- content: {
- room_version: "12"
- }
- },
{
type: "m.room.name",
state_key: "",
@@ -132,9 +122,7 @@ test("state2kstate: general", t => {
]), {
"m.room.name/": {name: "test name"},
"m.room.member/@cadence:cadence.moe": {membership: "join"},
- "uk.half-shot.bridge/org.matrix.appservice-irc://irc/epicord.net/#general": {creator: "@cadence:cadence.moe"},
- "m.room.create/": {room_version: "12"},
- "m.room.create/outer": {type: "m.room.create", state_key: "", sender: "@example:matrix.org", content: {room_version: "12"}}
+ "uk.half-shot.bridge/org.matrix.appservice-irc://irc/epicord.net/#general": {creator: "@cadence:cadence.moe"}
})
})
@@ -169,17 +157,6 @@ test("diffKState: detects new properties", t => {
test("diffKState: power levels are mixed together", t => {
const original = {
- "m.room.create/outer": {
- type: "m.room.create",
- state_key: "",
- sender: "@example:matrix.org",
- content: {
- room_version: "11"
- }
- },
- "m.room.create/": {
- room_version: "11"
- },
"m.room.power_levels/": {
"ban": 50,
"events": {
@@ -204,9 +181,6 @@ test("diffKState: power levels are mixed together", t => {
"m.room.power_levels/": {
"events": {
"m.room.avatar": 0
- },
- users: {
- "@example:matrix.org": 100
}
}
})
@@ -227,8 +201,7 @@ test("diffKState: power levels are mixed together", t => {
"redact": 50,
"state_default": 50,
"users": {
- "@example:localhost": 100,
- "@example:matrix.org": 100
+ "@example:localhost": 100
},
"users_default": 0
}
@@ -262,189 +235,30 @@ test("diffKState: kstate keys must contain a slash separator", t => {
t.pass()
})
-test("diffKState: topic does not change if the topic key has not changed", t => {
- t.deepEqual(diffKState({
- "m.room.topic/": {
- topic: "hello",
- "m.topic": {
- "m.text": "hello"
+test("diffKState: don't add hide_ui when not present", t => {
+ test("diffKState: detects new properties", t => {
+ t.deepEqual(
+ diffKState({
+ }, {
+ "chat.schildi.hide_ui/read_receipts/": {}
+ }),
+ {
}
- }
- }, {
- "m.room.topic/": {
- topic: "hello"
- }
- }),
- {})
-})
-
-test("diffKState: topic changes if the topic key has changed", t => {
- t.deepEqual(diffKState({
- "m.room.topic/": {
- topic: "hello",
- "m.topic": {
- "m.text": "hello"
- }
- }
- }, {
- "m.room.topic/": {
- topic: "hello you"
- }
- }),
- {
- "m.room.topic/": {
- topic: "hello you"
- }
+ )
})
})
-test("diffKState: room v12 creators cannot be introduced into power levels", t => {
- const original = {
- "m.room.create/outer": {
- type: "m.room.create",
- state_key: "",
- sender: "@example1:matrix.org",
- content: {
- additional_creators: ["@example2:matrix.org"],
- room_version: "12"
+test("diffKState: overwriten hide_ui when present", t => {
+ test("diffKState: detects new properties", t => {
+ t.deepEqual(
+ diffKState({
+ "chat.schildi.hide_ui/read_receipts/": {hidden: true}
+ }, {
+ "chat.schildi.hide_ui/read_receipts/": {}
+ }),
+ {
+ "chat.schildi.hide_ui/read_receipts/": {}
}
- },
- "m.room.create/": {
- room_version: "12"
- },
- "m.room.power_levels/": {
- "ban": 50,
- "events": {
- "m.room.name": 100,
- "m.room.power_levels": 100
- },
- "events_default": 0,
- "invite": 50,
- "kick": 50,
- "notifications": {
- "room": 20
- },
- "redact": 50,
- "state_default": 50,
- "users": {
- "@example:localhost": 100
- },
- "users_default": 0
- }
- }
- const result = diffKState(original, {
- "m.room.create/": {
- bogus: true
- },
- "m.room.power_levels/": {
- events: {
- "m.room.avatar": 0
- },
- users: {
- "@example1:matrix.org": 100,
- "@example2:matrix.org": 100,
- "@example3:matrix.org": 100
- }
- }
- })
- t.deepEqual(result, {
- "m.room.power_levels/": {
- "ban": 50,
- "events": {
- "m.room.name": 100,
- "m.room.power_levels": 100,
- "m.room.avatar": 0
- },
- "events_default": 0,
- "invite": 50,
- "kick": 50,
- "notifications": {
- "room": 20
- },
- "redact": 50,
- "state_default": 50,
- "users": {
- "@example:localhost": 100,
- "@example3:matrix.org": 100
- },
- "users_default": 0
- }
- })
- t.notDeepEqual(original, result)
-})
-
-test("diffKState: room v12 creators cannot be introduced into power levels - no diff if no changes", t => {
- const original = {
- "m.room.create/outer": {
- type: "m.room.create",
- state_key: "",
- sender: "@example1:matrix.org",
- content: {
- additional_creators: ["@example2:matrix.org"],
- room_version: "12"
- }
- },
- "m.room.create/": {
- additional_creators: ["@example2:matrix.org"],
- room_version: "12"
- },
- "m.room.power_levels/": {
- "ban": 50,
- "events": {
- "m.room.name": 100,
- "m.room.power_levels": 100
- },
- "events_default": 0,
- "invite": 50,
- "kick": 50,
- "notifications": {
- "room": 20
- },
- "redact": 50,
- "state_default": 50,
- "users": {
- "@example:localhost": 100
- },
- "users_default": 0
- }
- }
- const result = diffKState(original, {
- "m.room.power_levels/": {
- users: {
- "@example1:matrix.org": 100,
- "@example2:matrix.org": 100
- }
- }
- })
- t.deepEqual(result, {})
- t.notDeepEqual(original, result)
-})
-
-test("kstateToCreationContent: works", t => {
- const original = {
- "m.room.create/outer": {
- type: "m.room.create",
- state_key: "",
- sender: "@example1:matrix.org",
- content: {
- additional_creators: ["@example2:matrix.org"],
- room_version: "12",
- type: "m.space"
- }
- },
- "m.room.create/": {
- additional_creators: ["@example2:matrix.org"],
- room_version: "12",
- type: "m.space"
- }
- }
- t.deepEqual(kstateToCreationContent(original), {
- additional_creators: ["@example2:matrix.org"],
- room_version: "12",
- type: "m.space"
+ )
})
})
-
-test("kstateToCreationContent: works if empty", t => {
- t.deepEqual(kstateToCreationContent({}), {})
-})
diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js
index b38b4b1..93bc312 100644
--- a/src/matrix/matrix-command-handler.js
+++ b/src/matrix/matrix-command-handler.js
@@ -1,7 +1,6 @@
// @ts-check
const assert = require("assert").strict
-const DiscordTypes = require("discord-api-types/v10")
const Ty = require("../types")
const {pipeline} = require("stream").promises
const sharp = require("sharp")
@@ -9,8 +8,8 @@ const sharp = require("sharp")
const {discord, sync, db, select} = require("../passthrough")
/** @type {import("./api")}) */
const api = sync.require("./api")
-/** @type {import("./utils")} */
-const mxUtils = sync.require("./utils")
+/** @type {import("../m2d/converters/utils")} */
+const mxUtils = sync.require("../m2d/converters/utils")
/** @type {import("../discord/utils")} */
const dUtils = sync.require("../discord/utils")
/** @type {import("./kstate")} */
@@ -59,7 +58,7 @@ async function addButton(roomID, eventID, key, mxid) {
setInterval(() => {
const now = Date.now()
buttons = buttons.filter(b => now - b.created < 2*60*60*1000)
-}, 10*60*1000).unref()
+}, 10*60*1000)
/** @param {Ty.Event.Outer} event */
function onReactionAdd(event) {
@@ -105,8 +104,7 @@ const commands = [{
// Guard
/** @type {string} */ // @ts-ignore
const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
- const channel = discord.channels.get(channelID)
- const guildID = channel?.["guild_id"]
+ const guildID = discord.channels.get(channelID)?.["guild_id"]
let matrixOnlyReason = null
const matrixOnlyConclusion = "So the emoji will be uploaded on Matrix-side only. It will still be usable over the bridge, but may have degraded functionality."
// Check if we can/should upload to Discord, for various causes
@@ -116,7 +114,7 @@ const commands = [{
const guild = discord.guilds.get(guildID)
assert(guild)
const slots = getSlotCount(guild.premium_tier)
- const permissions = dUtils.getDefaultPermissions(guild, channel["permission_overwrites"])
+ const permissions = dUtils.getPermissions([], guild.roles)
if (guild.emojis.length >= slots) {
matrixOnlyReason = "CAPACITY"
} else if (!(permissions & 0x40000000n)) { // MANAGE_GUILD_EXPRESSIONS (apparently CREATE_GUILD_EXPRESSIONS isn't good enough...)
@@ -125,9 +123,12 @@ const commands = [{
}
if (matrixOnlyReason) {
// If uploading to Matrix, check if we have permission
- const {powerLevels, powers: {[mxUtils.bot]: botPower}} = await mxUtils.getEffectivePower(event.room_id, [mxUtils.bot], api)
- const requiredPower = powerLevels.events?.["im.ponies.room_emotes"] ?? powerLevels.state_default ?? 50
- if (botPower < requiredPower) {
+ const state = await api.getAllState(event.room_id)
+ const kstate = ks.stateToKState(state)
+ const powerLevels = kstate["m.room.power_levels/"]
+ const required = powerLevels.events["im.ponies.room_emotes"] ?? powerLevels.state_default ?? 50
+ const have = powerLevels.users[`@${reg.sender_localpart}:${reg.ooye.server_name}`] ?? powerLevels.users_default ?? 0
+ if (have < required) {
return api.sendEvent(event.room_id, "m.room.message", {
...ctx,
msgtype: "m.text",
@@ -176,7 +177,7 @@ const commands = [{
.addLine(`Ⓜ️ *If you were a Discord user, you wouldn't have permission to create emojis. ${matrixOnlyConclusion}`, `Ⓜ️ If you were a Discord user, you wouldn't have permission to create emojis. ${matrixOnlyConclusion}`, matrixOnlyReason === "CAPACITY")
.addLine("[Preview not available in plain text.]", "Preview:")
for (const e of toUpload) {
- b.add("", `:${e.name}:
`)
+ b.add("", `
`)
}
b.addLine("Hit ✅ to add it.")
const sent = await api.sendEvent(event.room_id, "m.room.message", {
@@ -241,8 +242,7 @@ const commands = [{
// Guard
/** @type {string} */ // @ts-ignore
const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
- const channel = discord.channels.get(channelID)
- const guildID = channel?.["guild_id"]
+ const guildID = discord.channels.get(channelID)?.["guild_id"]
if (!guildID) {
return api.sendEvent(event.room_id, "m.room.message", {
...ctx,
@@ -253,7 +253,7 @@ const commands = [{
const guild = discord.guilds.get(guildID)
assert(guild)
- const permissions = dUtils.getDefaultPermissions(guild, channel["permission_overwrites"])
+ const permissions = dUtils.getPermissions([], guild.roles)
if (!(permissions & 0x800000000n)) { // CREATE_PUBLIC_THREADS
return api.sendEvent(event.room_id, "m.room.message", {
...ctx,
@@ -265,59 +265,6 @@ const commands = [{
await discord.snow.channel.createThreadWithoutMessage(channelID, {type: 11, name: words.slice(1).join(" ")})
}
)
-}, {
- aliases: ["invite"],
- execute: replyctx(
- async (event, realBody, words, ctx) => {
- // Guard
- /** @type {string} */ // @ts-ignore
- const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
- const channel = discord.channels.get(channelID)
- const guildID = channel?.["guild_id"]
- if (!guildID) {
- return api.sendEvent(event.room_id, "m.room.message", {
- ...ctx,
- msgtype: "m.text",
- body: "This room isn't bridged to the other side."
- })
- }
-
- const guild = discord.guilds.get(guildID)
- assert(guild)
- const permissions = dUtils.getDefaultPermissions(guild, channel["permission_overwrites"])
- if (!dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.CreateInstantInvite)) {
- return api.sendEvent(event.room_id, "m.room.message", {
- ...ctx,
- msgtype: "m.text",
- body: "This command creates an invite link to the Discord side. But you aren't allowed to do this, because if you were a Discord user, you wouldn't have the Create Invite permission."
- })
- }
-
- try {
- var invite = await discord.snow.channel.createChannelInvite(channelID)
- } catch (e) {
- if (e.message === `{"message": "Missing Permissions", "code": 50013}`) {
- return api.sendEvent(event.room_id, "m.room.message", {
- ...ctx,
- msgtype: "m.text",
- body: "I don't have permission to create invites to the Discord channel/server."
- })
- } else {
- throw e
- }
- }
- const validHours = Math.ceil(invite.max_age / (60 * 60))
- const validUses =
- ( invite.max_uses === 0 ? "unlimited uses"
- : invite.max_uses === 1 ? "single-use"
- : `${invite.max_uses} uses`)
- return api.sendEvent(event.room_id, "m.room.message", {
- ...ctx,
- msgtype: "m.text",
- body: `https://discord.gg/${invite.code}\nValid for next ${validHours} hours, ${validUses}.`
- })
- }
- )
}]
diff --git a/src/matrix/mreq.js b/src/matrix/mreq.js
index bb59506..9179825 100644
--- a/src/matrix/mreq.js
+++ b/src/matrix/mreq.js
@@ -37,21 +37,6 @@ async function _convertBody(body) {
/* c8 ignore start */
-/**
- * @param {Response} res
- * @param {object} opts
- */
-async function makeMatrixServerError(res, opts = {}) {
- delete opts.headers?.["Authorization"]
- if (res.headers.get("content-type") === "application/json") {
- return new MatrixServerError(await res.json(), opts)
- } else if (res.headers.get("content-type")?.startsWith("text/")) {
- return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, message: await res.text()}, opts)
- } else {
- return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, content_type: res.headers.get("content-type")}, opts)
- }
-}
-
/**
* @param {string} method
* @param {string} url
@@ -72,14 +57,7 @@ async function mreq(method, url, bodyIn, extra = {}) {
}, extra)
const res = await fetch(baseUrl + url, opts)
- const text = await res.text()
- try {
- /** @type {any} */
- var root = JSON.parse(text)
- } catch (e) {
- delete opts.headers?.["Authorization"]
- throw new MatrixServerError(text, {baseUrl, url, ...opts})
- }
+ const root = await res.json()
if (!res.ok || root.errcode) {
delete opts.headers?.["Authorization"]
@@ -107,7 +85,6 @@ async function withAccessToken(token, callback) {
}
module.exports.MatrixServerError = MatrixServerError
-module.exports.makeMatrixServerError = makeMatrixServerError
module.exports.baseUrl = baseUrl
module.exports.mreq = mreq
module.exports.withAccessToken = withAccessToken
diff --git a/src/matrix/read-registration.js b/src/matrix/read-registration.js
index 86f99a1..d126851 100644
--- a/src/matrix/read-registration.js
+++ b/src/matrix/read-registration.js
@@ -11,7 +11,7 @@ const registrationFilePath = path.join(process.cwd(), "registration.yaml")
function checkRegistration(reg) {
reg["ooye"].invite = reg.ooye.invite.filter(mxid => mxid.endsWith(`:${reg.ooye.server_name}`)) // one day I will understand why typescript disagrees with dot notation on this line
assert(reg.ooye?.max_file_size)
- assert(reg.ooye?.namespace_prefix != null)
+ assert(reg.ooye?.namespace_prefix)
assert(reg.ooye?.server_name)
assert(reg.sender_localpart?.startsWith(reg.ooye.namespace_prefix), "appservice's localpart must be in the namespace it controls")
assert(reg.ooye?.server_origin.match(/^https?:\/\//), "server origin must start with http or https")
@@ -22,7 +22,7 @@ function checkRegistration(reg) {
/* c8 ignore next 4 */
/** @param {import("../types").AppServiceRegistrationConfig} reg */
function writeRegistration(reg) {
- fs.writeFileSync(registrationFilePath, JSON.stringify(reg, null, 2) + "\n")
+ fs.writeFileSync(registrationFilePath, JSON.stringify(reg, null, 2))
}
/**
@@ -57,8 +57,7 @@ function getTemplateRegistration(serverName) {
max_file_size: 5000000,
content_length_workaround: false,
include_user_id_in_mxid: false,
- invite: [],
- receive_presences: true
+ invite: []
}
}
}
@@ -78,11 +77,6 @@ function readRegistration() {
/** @type {import("../types").AppServiceRegistrationConfig} */ // @ts-ignore
let reg = readRegistration()
-fs.watch(registrationFilePath, {persistent: false}, () => {
- let newReg = readRegistration()
- Object.assign(reg, newReg)
-})
-
module.exports.registrationFilePath = registrationFilePath
module.exports.readRegistration = readRegistration
module.exports.getTemplateRegistration = getTemplateRegistration
diff --git a/src/matrix/read-registration.test.js b/src/matrix/read-registration.test.js
index a8dcc25..5fb3b55 100644
--- a/src/matrix/read-registration.test.js
+++ b/src/matrix/read-registration.test.js
@@ -1,6 +1,6 @@
// @ts-check
-const {tryToCatch} = require("try-to-catch")
+const tryToCatch = require("try-to-catch")
const {test} = require("supertape")
const {reg, checkRegistration, getTemplateRegistration} = require("./read-registration")
diff --git a/src/matrix/room-upgrade.js b/src/matrix/room-upgrade.js
deleted file mode 100644
index e7de906..0000000
--- a/src/matrix/room-upgrade.js
+++ /dev/null
@@ -1,96 +0,0 @@
-// @ts-check
-
-const assert = require("assert/strict")
-const Ty = require("../types")
-const {Semaphore} = require("@chriscdn/promise-semaphore")
-const {tag} = require("@cloudrac3r/html-template-tag")
-const {db, sync, select, from} = require("../passthrough")
-
-/** @type {import("./utils")}) */
-const utils = sync.require("./utils")
-
-const roomUpgradeSema = new Semaphore()
-
-/**
- * @param {Ty.Event.StateOuter} event
- * @param {import("./api")} api
- */
-async function onTombstone(event, api) {
- // Preconditions (checked by event-dispatcher, enforced here)
- assert.equal(event.state_key, "")
- assert.ok(event.content.replacement_room)
-
- // Set up
- const oldRoomID = event.room_id
- const newRoomID = event.content.replacement_room
- const channel = select("channel_room", ["name", "channel_id"], {room_id: oldRoomID}).get()
- if (!channel) return
- db.prepare("REPLACE INTO room_upgrade_pending (new_room_id, old_room_id) VALUES (?, ?)").run(newRoomID, oldRoomID)
-
- // Try joining
- try {
- await api.joinRoom(newRoomID)
- } catch (e) {
- const message = new utils.MatrixStringBuilder()
- message.add(
- `You upgraded the bridged room ${channel.name}. To keep bridging, I need you to invite me to the new room: https://matrix.to/#/${newRoomID}`,
- tag`You upgraded the bridged room ${channel.name}. To keep bridging, I need you to invite me to the new room: https://matrix.to/#/${newRoomID}`
- )
- const privateRoomID = await api.usePrivateChat(event.sender)
- await api.sendEvent(privateRoomID, "m.room.message", message.get())
- }
-
- // Now wait to be invited to/join the room that has the upgrade pending...
-}
-
-/**
- * @param {Ty.Event.StateOuter} event
- * @param {import("./api")} api
- * @param {import("../d2m/actions/create-room")} createRoom
- * @returns {Promise} whether to cancel other membership actions
- */
-async function onBotMembership(event, api, createRoom) {
- // Preconditions (checked by event-dispatcher, enforced here)
- assert.equal(event.type, "m.room.member")
- assert.equal(event.state_key, utils.bot)
-
- return await roomUpgradeSema.request(async () => {
- // Check if an upgrade is pending for this room
- const newRoomID = event.room_id
- const oldRoomID = select("room_upgrade_pending", "old_room_id", {new_room_id: newRoomID}).pluck().get()
- if (!oldRoomID) return false
- const channelRow = from("channel_room").join("guild_space", "guild_id").where({room_id: oldRoomID}).select("space_id", "guild_id", "channel_id").get()
- assert(channelRow) // this could only fail if the channel was unbridged or something between upgrade and joining
-
- // Check if is join/invite
- if (event.content.membership !== "invite" && event.content.membership !== "join") return false
-
- // If invited, join
- if (event.content.membership === "invite") {
- await api.joinRoom(newRoomID)
- }
-
- // Remove old room from space
- await api.sendState(channelRow.space_id, "m.space.child", oldRoomID, {})
- // await api.sendState(oldRoomID, "m.space.parent", spaceID, {}) // keep this - the room isn't advertised but should still be grouped if opened
-
- // Remove declaration that old room is bridged (if able)
- try {
- await api.sendState(oldRoomID, "uk.half-shot.bridge", `moe.cadence.ooye://discord/${channelRow.guild_id}/${channelRow.channel_id}`, {})
- } catch (e) { /* c8 ignore next */ }
-
- // Update database
- db.transaction(() => {
- db.prepare("DELETE FROM room_upgrade_pending WHERE new_room_id = ?").run(newRoomID)
- db.prepare("UPDATE channel_room SET room_id = ? WHERE channel_id = ?").run(newRoomID, channelRow.channel_id)
- db.prepare("INSERT INTO historical_channel_room (room_id, reference_channel_id, upgraded_timestamp) VALUES (?, ?, ?)").run(newRoomID, channelRow.channel_id, Date.now())
- })()
-
- // Sync
- await createRoom.syncRoom(channelRow.channel_id)
- return true
- }, event.room_id)
-}
-
-module.exports.onTombstone = onTombstone
-module.exports.onBotMembership = onBotMembership
diff --git a/src/matrix/room-upgrade.test.js b/src/matrix/room-upgrade.test.js
deleted file mode 100644
index 3de1a8f..0000000
--- a/src/matrix/room-upgrade.test.js
+++ /dev/null
@@ -1,169 +0,0 @@
-const {test} = require("supertape")
-const {select} = require("../passthrough")
-const {onTombstone, onBotMembership} = require("./room-upgrade")
-
-test("join upgraded room: only cares about upgrades in progress", async t => {
- let called = 0
- await onBotMembership({
- type: "m.room.member",
- state_key: "@_ooye_bot:cadence.moe",
- room_id: "!JBxeGYnzQwLnaooOLD:cadence.moe",
- content: {
- membership: "invite"
- }
- }, {
- /* c8 ignore next 4 */
- async joinRoom(roomID) {
- called++
- throw new Error("should not join this room")
- }
- })
- t.equal(called, 0)
-})
-
-test("tombstone: only cares about bridged rooms", async t => {
- let called = 0
- await onTombstone({
- event_id: "$tombstone",
- type: "m.room.tombstone",
- state_key: "",
- sender: "@cadence:cadence.moe",
- origin_server_ts: 0,
- room_id: "!imaginary:cadence.moe",
- content: {
- body: "This room has been replaced",
- replacement_room: "!JBxeGYnzQwLnaooNEW:cadence.moe"
- }
- }, {
- /* c8 ignore next 4 */
- async joinRoom(roomID) {
- called++
- throw new Error("should not join this room")
- }
- })
- t.equal(called, 0)
-})
-
-test("tombstone: joins new room and stores upgrade in database", async t => {
- let called = 0
- await onTombstone({
- event_id: "$tombstone",
- type: "m.room.tombstone",
- state_key: "",
- sender: "@cadence:cadence.moe",
- origin_server_ts: 0,
- room_id: "!JBxeGYnzQwLnaooOLD:cadence.moe",
- content: {
- body: "This room has been replaced",
- replacement_room: "!JBxeGYnzQwLnaooNEW:cadence.moe"
- }
- }, {
- async joinRoom(roomID) {
- called++
- t.equal(roomID, "!JBxeGYnzQwLnaooNEW:cadence.moe")
- return roomID
- }
- })
- t.equal(called, 1)
- t.ok(select("room_upgrade_pending", ["old_room_id", "new_room_id"], {new_room_id: "!JBxeGYnzQwLnaooNEW:cadence.moe", old_room_id: "!JBxeGYnzQwLnaooOLD:cadence.moe"}).get())
-})
-
-test("tombstone: requests invite from upgrader if can't join room", async t => {
- let called = 0
- await onTombstone({
- event_id: "$tombstone",
- type: "m.room.tombstone",
- state_key: "",
- sender: "@cadence:cadence.moe",
- origin_server_ts: 0,
- room_id: "!JBxeGYnzQwLnaooOLD:cadence.moe",
- content: {
- body: "This room has been replaced",
- replacement_room: "!JBxeGYnzQwLnaooNEW:cadence.moe"
- }
- }, {
- async joinRoom(roomID) {
- called++
- t.equal(roomID, "!JBxeGYnzQwLnaooNEW:cadence.moe")
- throw new Error("access denied or something")
- },
- async usePrivateChat(sender) {
- called++
- t.equal(sender, "@cadence:cadence.moe")
- return "!private"
- },
- async sendEvent(roomID, type, content) {
- called++
- t.equal(roomID, "!private")
- t.equal(type, "m.room.message")
- t.deepEqual(content, {
- msgtype: "m.text",
- body: "You upgraded the bridged room winners. To keep bridging, I need you to invite me to the new room: https://matrix.to/#/!JBxeGYnzQwLnaooNEW:cadence.moe",
- format: "org.matrix.custom.html",
- formatted_body: `You upgraded the bridged room winners. To keep bridging, I need you to invite me to the new room: https://matrix.to/#/!JBxeGYnzQwLnaooNEW:cadence.moe`
- })
- }
- })
- t.equal(called, 3)
-})
-
-test("join upgraded room: only cares about invites/joins", async t => {
- let called = 0
- await onBotMembership({
- type: "m.room.member",
- state_key: "@_ooye_bot:cadence.moe",
- room_id: "!JBxeGYnzQwLnaooNEW:cadence.moe",
- content: {
- membership: "leave"
- }
- }, {
- /* c8 ignore next 4 */
- async joinRoom(roomID) {
- called++
- throw new Error("should not join this room")
- }
- })
- t.equal(called, 0)
-})
-
-test("join upgraded room: joins invited room, updates database", async t => {
- let called = 0
- await onBotMembership({
- type: "m.room.member",
- state_key: "@_ooye_bot:cadence.moe",
- room_id: "!JBxeGYnzQwLnaooNEW:cadence.moe",
- content: {
- membership: "invite"
- }
- }, {
- async joinRoom(roomID) {
- called++
- t.equal(roomID, "!JBxeGYnzQwLnaooNEW:cadence.moe")
- return roomID
- },
- async sendState(roomID, type, key, content) {
- called++
- if (type === "m.space.child") {
- t.equal(roomID, "!CvQMeeqXIkgedUpkzv:cadence.moe") // space
- t.equal(key, "!JBxeGYnzQwLnaooOLD:cadence.moe")
- t.deepEqual(content, {})
- return "$child"
- } else if (type === "uk.half-shot.bridge") {
- t.equal(roomID, "!JBxeGYnzQwLnaooOLD:cadence.moe")
- t.equal(key, "moe.cadence.ooye://discord/1345641201902288987/598707048112193536")
- t.deepEqual(content, {})
- return "$bridge"
- }
- /* c8 ignore next */
- throw new Error(`unexpected sendState: ${roomID} - ${type}/${key}`)
- }
- }, {
- async syncRoom(channelID) {
- called++
- t.equal(channelID, "598707048112193536")
- }
- })
- t.equal(called, 4)
- t.equal(select("channel_room", "room_id", {channel_id: "598707048112193536"}).pluck().get(), "!JBxeGYnzQwLnaooNEW:cadence.moe")
- t.equal(select("historical_channel_room", "historical_room_index", {reference_channel_id: "598707048112193536"}).pluck().all().length, 2)
-})
diff --git a/src/matrix/utils.test.js b/src/matrix/utils.test.js
deleted file mode 100644
index 842c513..0000000
--- a/src/matrix/utils.test.js
+++ /dev/null
@@ -1,420 +0,0 @@
-// @ts-check
-
-const {select} = require("../passthrough")
-const {test} = require("supertape")
-const {eventSenderIsFromDiscord, getEventIDHash, MatrixStringBuilder, getViaServers, roomHasAtLeastVersion, removeCreatorsFromPowerLevels, setUserPower} = require("./utils")
-const util = require("util")
-
-/** @param {string[]} mxids */
-function joinedList(mxids) {
- /** @type {{[mxid: string]: {display_name: null, avatar_url: null}}} */
- const joined = {}
- for (const mxid of mxids) {
- joined[mxid] = {
- display_name: null,
- avatar_url: null
- }
- }
- return {joined}
-}
-
-test("sender type: matrix user", t => {
- t.notOk(eventSenderIsFromDiscord("@cadence:cadence.moe"))
-})
-
-test("sender type: ooye bot", t => {
- t.ok(eventSenderIsFromDiscord("@_ooye_bot:cadence.moe"))
-})
-
-test("sender type: ooye puppet", t => {
- t.ok(eventSenderIsFromDiscord("@_ooye_sheep:cadence.moe"))
-})
-
-test("event hash: hash is the same each time", t => {
- const eventID = "$example"
- t.equal(getEventIDHash(eventID), getEventIDHash(eventID))
-})
-
-test("event hash: hash is different for different inputs", t => {
- t.notEqual(getEventIDHash("$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe1"), getEventIDHash("$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe2"))
-})
-
-test("MatrixStringBuilder: add, addLine, add same text", t => {
- const e = {
- stack: "Error: Custom error\n at ./example.test.js:3:11)",
- toString() {
- return "Error: Custom error"
- }
- }
- const gatewayMessage = {t: "MY_MESSAGE", d: {display: "Custom message data"}}
- let stackLines = e.stack.split("\n")
-
- const builder = new MatrixStringBuilder()
- builder.addLine("\u26a0 Bridged event from Discord not delivered", "\u26a0 Bridged event from Discord not delivered")
- builder.addLine(`Gateway event: ${gatewayMessage.t}`)
- builder.addLine(e.toString())
- if (stackLines) {
- stackLines = stackLines.slice(0, 2)
- stackLines[1] = stackLines[1].replace(/\\/g, "/").replace(/(\s*at ).*(\/m2d\/)/, "$1.$2")
- builder.addLine(`Error trace:`, `Error trace
`)
- builder.add(`\n${stackLines.join("\n")}`, `${stackLines.join("\n")}`)
- }
- builder.addLine("", `Original payload
${util.inspect(gatewayMessage.d, false, 4, false)}`)
-
- t.deepEqual(builder.get(), {
- msgtype: "m.text",
- body: "\u26a0 Bridged event from Discord not delivered"
- + "\nGateway event: MY_MESSAGE"
- + "\nError: Custom error"
- + "\nError trace:"
- + "\nError: Custom error"
- + "\n at ./example.test.js:3:11)\n",
- format: "org.matrix.custom.html",
- formatted_body: "\u26a0 Bridged event from Discord not delivered"
- + "
Gateway event: MY_MESSAGE"
- + "
Error: Custom error"
- + "
Error trace
Error: Custom error\n at ./example.test.js:3:11)
"
- + `Original payload
{ display: 'Custom message data' }`
- })
-})
-
-test("MatrixStringBuilder: complete code coverage", t => {
- const builder = new MatrixStringBuilder()
- builder.add("Line 1")
- builder.addParagraph("Line 2")
- builder.add("Line 3")
- builder.addParagraph("Line 4")
-
- t.deepEqual(builder.get(), {
- msgtype: "m.text",
- body: "Line 1\n\nLine 2Line 3\n\nLine 4",
- format: "org.matrix.custom.html",
- formatted_body: "Line 1Line 2
Line 3Line 4
"
- })
-})
-
-/**
- * @param {string[]} [creators]
- * @param {{[x: string]: number}} [users]
- * @param {string} [roomVersion]
- */
-function mockGetEffectivePower(creators = ["@_ooye_bot:cadence.moe"], users = {}, roomVersion = "12") {
- return async function getEffectivePower(roomID, mxids) {
- return {
- allCreators: creators,
- powerLevels: {users},
- powers: mxids.reduce((a, mxid) => {
- if (creators.includes(mxid) && roomHasAtLeastVersion(roomVersion, 12)) a[mxid] = Infinity
- else if (mxid in users) a[mxid] = users[mxid]
- else a[mxid] = 0
- return a
- }, {}),
- roomCreate: {
- type: "m.room.create",
- state_key: "",
- sender: creators[0],
- content: {
- additional_creators: creators.slice(1),
- room_version: roomVersion
- },
- room_id: roomID,
- origin_server_ts: 0,
- event_id: "$create"
- },
- tombstone: roomVersion === "12" ? 150 : 100,
- }
- }
-}
-
-test("getViaServers: returns the server name if the room only has sim users", async t => {
- const result = await getViaServers("!baby", {
- getEffectivePower: mockGetEffectivePower(),
- getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe"])
- })
- t.deepEqual(result, ["cadence.moe"])
-})
-
-test("getViaServers: also returns the most popular servers in order", async t => {
- const result = await getViaServers("!baby", {
- getEffectivePower: mockGetEffectivePower(),
- getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@singleuser:selfhosted.invalid", "@hazel:thecollective.invalid", "@june:thecollective.invalid"])
- })
- t.deepEqual(result, ["cadence.moe", "thecollective.invalid", "selfhosted.invalid"])
-})
-
-test("getViaServers: does not return IP address servers", async t => {
- const result = await getViaServers("!baby", {
- getEffectivePower: mockGetEffectivePower(),
- getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:45.77.232.172:8443", "@cadence:[::1]:8443", "@cadence:123example.456example.invalid"])
- })
- t.deepEqual(result, ["cadence.moe", "123example.456example.invalid"])
-})
-
-test("getViaServers: also returns the highest power level user (v12 creator)", async t => {
- const result = await getViaServers("!baby", {
- getEffectivePower: mockGetEffectivePower(["@_ooye_bot:cadence.moe", "@singleuser:selfhosted.invalid"], {
- "@moderator:tractor.invalid": 50
- }),
- getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@singleuser:selfhosted.invalid", "@hazel:thecollective.invalid", "@june:thecollective.invalid", "@moderator:tractor.invalid"])
- })
- t.deepEqual(result, ["cadence.moe", "selfhosted.invalid", "thecollective.invalid", "tractor.invalid"])
-})
-
-test("getViaServers: also returns the highest power level user (100)", async t => {
- const result = await getViaServers("!baby", {
- getEffectivePower: mockGetEffectivePower(["@_ooye_bot:cadence.moe"], {
- "@moderator:tractor.invalid": 50,
- "@singleuser:selfhosted.invalid": 100
- }),
- getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@singleuser:selfhosted.invalid", "@hazel:thecollective.invalid", "@june:thecollective.invalid", "@moderator:tractor.invalid"])
- })
- t.deepEqual(result, ["cadence.moe", "selfhosted.invalid", "thecollective.invalid", "tractor.invalid"])
-})
-
-test("getViaServers: also returns the highest power level user (50)", async t => {
- const result = await getViaServers("!baby", {
- getEffectivePower: mockGetEffectivePower(["@_ooye_bot:cadence.moe"], {
- "@moderator:tractor.invalid": 50
- }),
- getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@moderator:tractor.invalid", "@hazel:thecollective.invalid", "@june:thecollective.invalid", "@singleuser:selfhosted.invalid"])
- })
- t.deepEqual(result, ["cadence.moe", "tractor.invalid", "thecollective.invalid", "selfhosted.invalid"])
-})
-
-test("getViaServers: returns at most 4 results", async t => {
- const result = await getViaServers("!baby", {
- getEffectivePower: mockGetEffectivePower(["@_ooye_bot:cadence.moe"], {
- "@moderator:tractor.invalid": 50,
- "@singleuser:selfhosted.invalid": 100
- }),
- getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@moderator:tractor.invalid", "@singleuser:selfhosted.invalid", "@hazel:thecollective.invalid", "@cadence:123example.456example.invalid"])
- })
- t.deepEqual(result.length, 4)
-})
-
-test("getViaServers: only considers power levels of currently joined members", async t => {
- const result = await getViaServers("!baby", {
- getEffectivePower: mockGetEffectivePower(["@_ooye_bot:cadence.moe", "@former_moderator:missing.invalid"], {
- "@moderator:tractor.invalid": 50
- }),
- getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@moderator:tractor.invalid", "@hazel:thecollective.invalid", "@june:thecollective.invalid", "@singleuser:selfhosted.invalid"])
- })
- t.deepEqual(result, ["cadence.moe", "tractor.invalid", "thecollective.invalid", "selfhosted.invalid"])
-})
-
-test("roomHasAtLeastVersion: v9 < v11", t => {
- t.equal(roomHasAtLeastVersion("9", 11), false)
-})
-
-test("roomHasAtLeastVersion: v12 >= v11", t => {
- t.equal(roomHasAtLeastVersion("12", 11), true)
-})
-
-test("roomHasAtLeastVersion: v12 >= v12", t => {
- t.equal(roomHasAtLeastVersion("12", 12), true)
-})
-
-test("roomHasAtLeastVersion: custom versions never match", t => {
- t.equal(roomHasAtLeastVersion("moe.cadence.silly", 11), false)
-})
-
-test("removeCreatorsFromPowerLevels: removes the creator from a v12 room", t => {
- t.deepEqual(removeCreatorsFromPowerLevels({
- type: "m.room.create",
- state_key: "",
- sender: "@_ooye_bot:cadence.moe",
- room_id: "!example",
- event_id: "$create",
- origin_server_ts: 0,
- content: {
- room_version: "12"
- }
- }, {
- users: {
- "@_ooye_bot:cadence.moe": 100
- }
- }), {
- users: {
- }
- })
-})
-
-test("removeCreatorsFromPowerLevels: removes all creators from a v12 room", t => {
- t.deepEqual(removeCreatorsFromPowerLevels({
- type: "m.room.create",
- state_key: "",
- sender: "@_ooye_bot:cadence.moe",
- room_id: "!example",
- event_id: "$create",
- origin_server_ts: 0,
- content: {
- additional_creators: ["@cadence:cadence.moe"],
- room_version: "12"
- }
- }, {
- users: {
- "@_ooye_bot:cadence.moe": 100,
- "@cadence:cadence.moe": 100
- }
- }), {
- users: {
- }
- })
-})
-
-test("removeCreatorsFromPowerLevels: doesn't touch a v11 room", t => {
- t.deepEqual(removeCreatorsFromPowerLevels({
- type: "m.room.create",
- state_key: "",
- sender: "@_ooye_bot:cadence.moe",
- room_id: "!example",
- event_id: "$create",
- origin_server_ts: 0,
- content: {
- additional_creators: ["@cadence:cadence.moe"],
- room_version: "11"
- }
- }, {
- users: {
- "@_ooye_bot:cadence.moe": 100,
- "@cadence:cadence.moe": 100
- }
- }), {
- users: {
- "@_ooye_bot:cadence.moe": 100,
- "@cadence:cadence.moe": 100
- }
- })
-})
-
-test("set user power: no-op", async t => {
- let called = 0
- await setUserPower("!room", "@cadence:cadence.moe", 0, {
- async getStateEvent(roomID, type, key) {
- called++
- t.equal(roomID, "!room")
- t.equal(type, "m.room.power_levels")
- t.equal(key, "")
- return {}
- },
- async getStateEventOuter(roomID, type, key) {
- called++
- t.equal(roomID, "!room")
- t.equal(type, "m.room.create")
- t.equal(key, "")
- return {
- type: "m.room.create",
- state_key: "",
- sender: "@_ooye_bot:cadence.moe",
- room_id: "!room",
- origin_server_ts: 0,
- event_id: "$create",
- content: {
- room_version: "11"
- }
- }
- },
- /* c8 ignore next 4 */
- async sendState() {
- called++
- throw new Error("should not try to send state")
- }
- })
- t.equal(called, 2)
-})
-
-test("set user power: bridge bot must promote unprivileged users", async t => {
- let called = 0
- await setUserPower("!room", "@cadence:cadence.moe", 100, {
- async getStateEvent(roomID, type, key) {
- called++
- t.equal(roomID, "!room")
- t.equal(type, "m.room.power_levels")
- t.equal(key, "")
- return {
- users: {"@_ooye_bot:cadence.moe": 100}
- }
- },
- async getStateEventOuter(roomID, type, key) {
- called++
- t.equal(roomID, "!room")
- t.equal(type, "m.room.create")
- t.equal(key, "")
- return {
- type: "m.room.create",
- state_key: "",
- sender: "@_ooye_bot:cadence.moe",
- room_id: "!room",
- origin_server_ts: 0,
- event_id: "$create",
- content: {
- room_version: "11"
- }
- }
- },
- async sendState(roomID, type, key, content, mxid) {
- called++
- t.equal(roomID, "!room")
- t.equal(type, "m.room.power_levels")
- t.equal(key, "")
- t.deepEqual(content, {
- users: {
- "@_ooye_bot:cadence.moe": 100,
- "@cadence:cadence.moe": 100
- }
- })
- t.equal(mxid, undefined)
- return "$sent"
- }
- })
- t.equal(called, 3)
-})
-
-test("set user power: privileged users must demote themselves", async t => {
- let called = 0
- await setUserPower("!room", "@cadence:cadence.moe", 0, {
- async getStateEvent(roomID, type, key) {
- called++
- t.equal(roomID, "!room")
- t.equal(type, "m.room.power_levels")
- t.equal(key, "")
- return {
- users: {
- "@cadence:cadence.moe": 100,
- "@_ooye_bot:cadence.moe": 100
- }
- }
- },
- async getStateEventOuter(roomID, type, key) {
- called++
- t.equal(roomID, "!room")
- t.equal(type, "m.room.create")
- t.equal(key, "")
- return {
- type: "m.room.create",
- state_key: "",
- sender: "@_ooye_bot:cadence.moe",
- room_id: "!room",
- origin_server_ts: 0,
- event_id: "$create",
- content: {
- room_version: "11"
- }
- }
- },
- async sendState(roomID, type, key, content, mxid) {
- called++
- t.equal(roomID, "!room")
- t.equal(type, "m.room.power_levels")
- t.equal(key, "")
- t.deepEqual(content, {
- users: {"@_ooye_bot:cadence.moe": 100}
- })
- t.equal(mxid, "@cadence:cadence.moe")
- return "$sent"
- }
- })
- t.equal(called, 3)
-})
-
-module.exports.mockGetEffectivePower = mockGetEffectivePower
diff --git a/src/stdin.js b/src/stdin.js
index 2548d42..fea5fad 100644
--- a/src/stdin.js
+++ b/src/stdin.js
@@ -23,26 +23,10 @@ const setPresence = sync.require("./d2m/actions/set-presence")
const channelWebhook = sync.require("./m2d/actions/channel-webhook")
const guildID = "112760669178241024"
-async function ping() {
- const result = await api.ping().catch(e => ({ok: false, status: "net", root: e.message}))
- if (result.ok) {
- return "Ping OK. The homeserver and OOYE are talking to each other fine."
- } else {
- if (typeof result.root === "string") {
- var msg = `Cannot reach homeserver: ${result.root}`
- } else if (result.root.error) {
- var msg = `Homeserver said: [${result.status}] ${result.root.error}`
- } else {
- var msg = `Homeserver said: [${result.status}] ${JSON.stringify(result.root)}`
- }
- return msg + "\nMatrix->Discord won't work until you fix this.\nIf your installation has recently changed, consider `npm run setup` again."
- }
-}
-
if (process.stdin.isTTY) {
setImmediate(() => {
if (!passthrough.repl) {
- const cli = repl.start({prompt: "", eval: customEval, writer: s => s})
+ const cli = repl.start({ prompt: "", eval: customEval, writer: s => s })
Object.assign(cli.context, passthrough)
passthrough.repl = cli
}
diff --git a/src/types.d.ts b/src/types.d.ts
index be037ca..c7cb006 100644
--- a/src/types.d.ts
+++ b/src/types.d.ts
@@ -1,5 +1,3 @@
-import * as DiscordTypes from "discord-api-types/v10"
-
export type AppServiceRegistrationConfig = {
id: string
as_token: string
@@ -34,7 +32,6 @@ export type AppServiceRegistrationConfig = {
discord_cdn_origin?: string,
web_password: string
time_zone?: string
- receive_presences: boolean
}
old_bridge?: {
as_token: string
@@ -67,7 +64,6 @@ export type InitialAppServiceRegistrationConfig = {
content_length_workaround: boolean
invite: string[]
include_user_id_in_mxid: boolean
- receive_presences: boolean
}
}
@@ -76,13 +72,6 @@ export type WebhookCreds = {
token: string
}
-/** Discord API message->author. A webhook as an author. */
-export type WebhookAuthor = {
- username: string
- avatar: string | null
- id: string
-}
-
export type PkSystem = {
id: string
uuid: string
@@ -145,6 +134,21 @@ export namespace Event {
}
}
+ export type BaseStateEvent = {
+ type: string
+ room_id: string
+ sender: string
+ content: any
+ state_key: string
+ origin_server_ts: number
+ unsigned?: any
+ event_id: string
+ user_id: string
+ age: number
+ replaces_state: string
+ prev_content?: any
+ }
+
export type StrippedChildStateEvent = {
type: string
state_key: string
@@ -153,37 +157,6 @@ export namespace Event {
content: any
}
- export type InviteStrippedState = {
- type: string
- state_key: string
- sender: string
- content: Event.M_Room_Create | Event.M_Room_Name | Event.M_Room_Avatar | Event.M_Room_Topic | Event.M_Room_JoinRules | Event.M_Room_CanonicalAlias | Event.M_Room_Encryption
- }
-
- export type M_Room_Create = {
- additional_creators?: string[]
- "m.federate"?: boolean
- room_version: string
- type?: string
- predecessor?: {
- room_id: string
- event_id?: string
- }
- }
-
- export type M_Room_JoinRules = {
- join_rule: "public" | "knock" | "invite" | "private" | "restricted" | "knock_restricted"
- allow?: {
- type: string
- room_id: string
- }[]
- }
-
- export type M_Room_CanonicalAlias = {
- alias?: string
- alt_aliases?: string[]
- }
-
export type M_Room_Message = {
msgtype: "m.text" | "m.emote"
body: string
@@ -208,7 +181,6 @@ export namespace Event {
filename?: string
url: string
info?: any
- "page.codeberg.everypizza.msc4193.spoiler"?: boolean
"m.relates_to"?: {
"m.in_reply_to": {
event_id: string
@@ -226,7 +198,6 @@ export namespace Event {
format?: "org.matrix.custom.html"
formatted_body?: string
filename?: string
- "page.codeberg.everypizza.msc4193.spoiler"?: boolean
file: {
url: string
iv: string
@@ -271,49 +242,6 @@ export namespace Event {
export type Outer_M_Sticker = Outer & {type: "m.sticker"}
- export type Org_Matrix_Msc3381_Poll_Start = {
- "org.matrix.msc3381.poll.start": {
- question: {
- "org.matrix.msc1767.text": string
- body: string
- msgtype: string
- },
- kind: string
- max_selections: number
- answers: {
- id: string
- "org.matrix.msc1767.text": string
- }[]
- "org.matrix.msc1767.text": string
- }
- }
-
- export type Outer_Org_Matrix_Msc3381_Poll_Start = Outer & {type: "org.matrix.msc3381.poll.start"}
-
- export type Org_Matrix_Msc3381_Poll_Response = {
- "org.matrix.msc3381.poll.response": {
- answers: string[]
- }
- "m.relates_to": {
- rel_type: string
- event_id: string
- }
- }
-
- export type Outer_Org_Matrix_Msc3381_Poll_Response = Outer & {type: "org.matrix.msc3381.poll.response"}
-
- export type Org_Matrix_Msc3381_Poll_End = {
- "org.matrix.msc3381.poll.end": {},
- "org.matrix.msc1767.text": string,
- body: string,
- "m.relates_to": {
- rel_type: string
- event_id: string
- }
- }
-
- export type Outer_Org_Matrix_Msc3381_Poll_End = Outer & {type: "org.matrix.msc3381.poll.end"}
-
export type M_Room_Member = {
membership: string
displayname?: string
@@ -321,6 +249,7 @@ export namespace Event {
}
export type M_Room_Avatar = {
+ discord_path?: string
url?: string
}
@@ -385,17 +314,6 @@ export namespace Event {
}> & {
redacts: string
}
-
- export type M_Room_Tombstone = {
- body: string
- replacement_room: string
- }
-
- export type M_Room_Encryption = {
- algorithm: string
- rotation_period_ms?: number
- rotation_period_msgs?: number
- }
}
export namespace R {
@@ -439,79 +357,21 @@ export namespace R {
guest_can_join: boolean
join_rule?: string
name?: string
- topic?: string
num_joined_members: number
room_id: string
room_type?: string
- encryption?: string
}
export type ResolvedRoom = {
room_id: string
servers: string[]
}
-
- export type SSS = {
- pos: string
- lists: {
- [list_key: string]: {
- count: number
- }
- }
- rooms: {
- [room_id: string]: {
- bump_stamp: number
- /** Omitted if user not in room (peeking) */
- membership?: Membership
- /** Names of lists that match this room */
- lists: string[]
- }
- // If user has been in the room - at least, that's what the spec says. Synapse returns some of these, such as `name` and `avatar`, for invites as well. Go nuts.
- & {
- name?: string
- avatar?: string
- heroes?: any[]
- /** According to account data */
- is_dm?: boolean
- /** If false, omitted fields are unchanged from their previous value. If true, omitted fields means the fields are not set. */
- initial?: boolean
- expanded_timeline?: boolean
- required_state?: Event.StateOuter[]
- timeline_events?: Event.Outer[]
- prev_batch?: string
- limited?: boolean
- num_live?: number
- joined_count?: number
- invited_count?: number
- notification_count?: number
- highlight_count?: number
- }
- // If user is invited or knocked
- & ({
- /** @deprecated */
- invite_state: Event.InviteStrippedState[]
- } | {
- stripped_state: Event.InviteStrippedState[]
- })
- }
- extensions: {
- [extension_key: string]: any
- }
- }
}
-export type Membership = "invite" | "knock" | "join" | "leave" | "ban"
-
export type Pagination = {
chunk: T[]
next_batch?: string
- prev_batch?: string
-}
-
-export type MessagesPagination = {
- chunk: T[]
- start: string
- end?: string
+ prev_match?: string
}
export type HierarchyPagination = {
diff --git a/src/web/pug-sync.js b/src/web/pug-sync.js
index e61c53b..f49f5a2 100644
--- a/src/web/pug-sync.js
+++ b/src/web/pug-sync.js
@@ -31,15 +31,7 @@ function addGlobals(obj) {
*/
function render(event, filename, locals) {
const path = join(__dirname, "pug", filename)
- return renderPath(event, path, locals)
-}
-/**
- * @param {import("h3").H3Event} event
- * @param {string} path
- * @param {Record} locals
- */
-function renderPath(event, path, locals) {
function compile() {
try {
const template = compileFile(path, {pretty})
@@ -77,7 +69,6 @@ function renderPath(event, path, locals) {
compile()
fs.watch(path, {persistent: false}, compile)
fs.watch(join(__dirname, "pug", "includes"), {persistent: false}, compile)
- fs.watch(join(__dirname, "pug", "fragments"), {persistent: false}, compile)
}
const cb = pugCache.get(path)
@@ -98,5 +89,4 @@ function createRoute(router, url, filename) {
module.exports.addGlobals = addGlobals
module.exports.render = render
-module.exports.renderPath = renderPath
module.exports.createRoute = createRoute
diff --git a/src/web/pug/agi-optout.pug b/src/web/pug/agi-optout.pug
deleted file mode 100644
index 795e675..0000000
--- a/src/web/pug/agi-optout.pug
+++ /dev/null
@@ -1,24 +0,0 @@
-extends includes/template.pug
-
-block body
- h1.ta-center.fs-display2.fc-green-400 April Fools!
- .ws7.m-auto
- .s-prose.fs-body2
- p Sheesh, wouldn't that be horrible?
- if guild_id
- p Fake AI messages have now been #[strong.fc-green-600 deactivated for everyone in your server.]
- p Hope the prank entertained you. #[a(href="https://cadence.moe/contact") Send love or hate mail here.]
-
- h2 What actually happened?
- ul
- li A secret event was added for the duration of 1st April 2026 (UTC).
- li If a message matches a preset pattern, a preset response is posted to chat by an AI-ified profile of the previous author.
- li It only happens at most once per hour in each server.
- li I tried to design it to not interrupt any serious/sensitive topics. I am deeply sorry if that didn't work out.
- li No AI generated materials have ever been used in Out Of Your Element: no code, no prose, no images, no jokes.
- li It'll always deactivate itself on 2nd April, no matter what, and I'll remove the relevant code shortly after.
- if guild_id
- .s-prose.fl-grow1.mt16
- p If you thought it was funny, feel free to opt back in. This affects the entire server, so please be courteous.
- form(method="post" action=rel(`/agi/optin?guild_id=${guild_id}`))
- button(type="submit").s-btn.s-btn__muted Opt back in
diff --git a/src/web/pug/agi.pug b/src/web/pug/agi.pug
deleted file mode 100644
index 029c02a..0000000
--- a/src/web/pug/agi.pug
+++ /dev/null
@@ -1,41 +0,0 @@
-extends includes/template.pug
-
-block title
- title AGI in Discord
-
-block body
- style.
- .ai-gradient {
- background: linear-gradient(100deg, #fb72f2, #072ea4);
- color: transparent;
- background-clip: text;
- }
-
- h1.ta-center.fs-display2.ai-gradient AGI in Discord:#[br]Revolutionizing the Future of Communications
- .ws7.m-auto
- .s-prose.fs-body2
- p In the ever-changing and turbulent world of AI, it's crucial to always be one step ahead.
- p That's why Out Of Your Element has partnered with #[strong Grimace AI] to provide you tomorrow's technology, today.
- ul
- li #[strong Always online:] Miss your friends when they log off? Now you can talk to facsimiles of them etched into an unfeeling machine, always and forever!
- li #[strong Smarter than ever:] Pissed off when somebody says something #[em wrong] on the internet? Frustrated with having to stay up all night correcting them? With Grimace Truth (available in Pro+ Ultra plan), all information is certified true to reality, so you'll never have to worry about those frantic Google searches at 3 AM.
- li #[strong Knows you better than yourself:] We aren't just training on your data; we're copying minds into our personality matrix — including yours. Do you find yourself enjoying the sound of your own voice more than anything else? Our unique simulation of You is here to help.
-
- h1.mt64.mb32 Frequently Asked Questions
- .s-link-preview
- .s-link-preview--header.fd-column
- .s-link-preview--title.fs-title.pl4 How to opt out?
- .s-link-preview--details.fc-red-500
- != icons.Icons.IconFire
- = ` 20,000% higher search volume for this question in the last hour`
- .s-link-preview--body
- .s-prose
- h2.fs-body3 Is this really goodbye? 😢😢😢😢😢
- p I can't convince you to stay?
- p As not just a customer service representative but someone with a shared vision, I simply want you to know that everyone at Grimace AI will miss all the time that we've shared together with you.
- form(method="post" action=(guild_id ? rel(`/agi/optout?guild_id=${guild_id}`) : rel("/agi/optout"))).d-flex.g4.mt16
- button(type="button").s-btn.s-btn__filled Nevermind, I'll stay :)
- button(type="submit").s-btn.s-btn__danger.s-btn__outlined Opt out for 3 days
-
-
- div(style="height: 200px")
diff --git a/src/web/pug/fragments/default-roles-list.pug b/src/web/pug/fragments/default-roles-list.pug
deleted file mode 100644
index 3b36549..0000000
--- a/src/web/pug/fragments/default-roles-list.pug
+++ /dev/null
@@ -1,5 +0,0 @@
-//- locals: guild, guild_id
-
-include ../includes/default-roles-list.pug
-+default-roles-list(guild, guild_id)
-+add-roles-menu(guild, guild_id)
diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug
index 9791ae3..92ffa1b 100644
--- a/src/web/pug/guild.pug
+++ b/src/web/pug/guild.pug
@@ -1,5 +1,4 @@
extends includes/template.pug
-include includes/default-roles-list.pug
mixin badge-readonly
.s-badge.s-badge__xs.s-badge__icon.s-badge__muted
@@ -14,7 +13,7 @@ mixin badge-private
mixin discord(channel, radio=false)
//- Previously, we passed guild.roles as the second parameter, but this doesn't quite match Discord's behaviour. See issue #42 for why this was changed.
//- Basically we just want to assign badges based on the channel overwrites, without considering the guild's base permissions. /shrug
- - let permissions = dUtils.getPermissions(guild_id, [], [{id: guild_id, name: "@everyone", permissions: 1<<10 | 1<<11}], null, channel.permission_overwrites)
+ - let permissions = dUtils.getPermissions([], [{id: guild_id, name: "@everyone", permissions: 1<<10 | 1<<11}], null, channel.permission_overwrites)
.s-user-card.s-user-card__small
if !dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.ViewChannel)
!= icons.Icons.IconLock
@@ -76,8 +75,7 @@ block body
button.s-btn(class=space_id ? "s-btn__muted" : "s-btn__filled" hx-get=rel(`/qr?guild_id=${guild_id}`) hx-indicator="closest button" hx-swap="outerHTML" hx-disabled-elt="this") Show QR
if space_id
- h2.mt48.fs-headline1 Server settings
- h3.mt32.fs-category How Matrix users join
+ h3.mt32.fs-category Privacy level
span#privacy-level-loading
.s-card
form(hx-post=rel("/api/privacy-level") hx-trigger="change" hx-indicator="#privacy-level-loading" hx-disabled-elt="input")
@@ -106,25 +104,7 @@ block body
p.s-description.m0 Shareable invite links, like Discord
p.s-description.m0 Publicly listed in directory, like Discord server discovery
- h3.mt32.fs-category Default roles
- .s-card
- form(method="post" action=rel("/api/default-roles") hx-post=rel("/api/default-roles") hx-sync="this:drop" hx-indicator="#add-role-loading" hx-target="#default-roles-list" hx-select="#default-roles-list" hx-swap="outerHTML")#default-roles
- input(type="hidden" name="guild_id" value=guild_id)
- .d-flex.fw-wrap.g4
- .s-tag.s-tag__md.fs-body1.s-tag__required @everyone
-
- +default-roles-list(guild, guild_id)
-
- button(type="button" popovertarget="role-add").s-btn__dropdown.s-tag.s-tag__md.fs-body1.p0
- .s-tag--dismiss.m1
- != icons.Icons.IconPlusSm
-
- #role-add.s-popover(popover style="display: revert").ws2.px0.py4.bs-lg.overflow-visible
- .s-popover--arrow.s-popover--arrow__tc
- +add-roles-menu(guild, guild_id)
- p.fc-medium.mb0.mt8 Matrix users will start with these roles. If your main channels are gated by a role, use this to let Matrix users skip the gate.
-
- h3.mt32.fs-category Features
+ h2.mt48.fs-headline1 Features
.s-card.d-grid.px0.g16
form.d-flex.ai-center.g16
#url-preview-loading.p8
@@ -144,27 +124,18 @@ block body
| Show online statuses on Matrix
p.s-description This might cause lag on really big Discord servers.
- form.d-flex.ai-center.g16
- #webhook-profile-loading.p8
- - value = !!select("guild_space", "webhook_profile", {guild_id}).pluck().get()
- input(type="hidden" name="guild_id" value=guild_id)
- input.s-toggle-switch#webhook-profile(name="webhook_profile" type="checkbox" hx-post=rel("/api/webhook-profile") hx-indicator="#webhook-profile-loading" hx-disabled-elt="this" checked=value autocomplete="off")
- label.s-label(for="webhook-profile")
- | Create persistent Matrix sims for webhooks
- p.s-description Useful when using other Discord bridges. Otherwise, not ideal, as sims will clutter the Matrix user list and will never be cleaned up.
-
if space_id
h2.mt48.fs-headline1 Channel setup
h3.mt32.fs-category Linked channels
.s-card.bs-sm.p0
- form.s-table-container(method="post" action=rel("/api/unlink"))
+ form.s-table-container(method="post" action=rel("/api/unlink") hx-confirm="Do you want to unlink these channels?\nIt may take a moment to clean up Matrix resources.")
input(type="hidden" name="guild_id" value=guild_id)
table.s-table.s-table__bx-simple
each row in linkedChannelsWithDetails
tr
td.w40: +discord(row.channel)
- td.p2: button.s-btn.s-btn__muted.s-btn__xs(name="channel_id" cx-prevent-default hx-post=rel("/api/unlink") hx-confirm="Do you want to unlink these channels?\nIt may take a moment to clean up Matrix resources." value=row.channel.id hx-indicator="this" hx-disabled-elt="this")!= icons.Icons.IconLinkSm
+ td.p2: button.s-btn.s-btn__muted.s-btn__xs(name="channel_id" value=row.channel.id hx-post=rel("/api/unlink") hx-trigger="click" hx-disabled-elt="this")!= icons.Icons.IconLinkSm
td: +matrix(row)
else
tr
@@ -205,19 +176,6 @@ block body
!= icons.Icons.IconMerge
= ` Link`
- h3.mt32.fs-category Unlink server
- form.s-card.d-flex.gx16.ai-center(method="post" action=rel("/api/unlink-space"))
- input(type="hidden" name="guild_id" value=guild.id)
- .fl-grow1.s-prose.s-prose__sm.lh-lg
- p.fc-medium.
- Not using this bridge, or just made a mistake? You can unlink the whole server and all its channels.#[br]
- This may take a minute to process. Please be patient and wait until the page refreshes.
- div
- button.s-btn.s-btn__icon.s-btn__danger.s-btn__outlined(cx-prevent-default hx-post=rel("/api/unlink-space") hx-confirm="Do you want to unlink this server and all its channels?\nIt may take a minute to clean up Matrix resources." hx-indicator="this" hx-disabled-elt="this")
- != icons.Icons.IconUnsync
- span.ml4= ` Unlink`
-
- if space_id
details.mt48
summary Debug room list
.d-grid.grid__2.gx24
@@ -238,7 +196,7 @@ block body
ul.my8.ml24
each row in removedWrongTypeChannels
li: a(href=`https://discord.com/channels/${guild_id}/${row.id}`) (#{row.type}) #{row.name}
- h3.mt24 Unavailable channels: Discord bot can't access
+ h3.mt24 Unavailable channels: Bridge can't access
.s-card.p0
ul.my8.ml24
each row in removedPrivateChannels
@@ -249,11 +207,6 @@ block body
ul.my8.ml24
each row in removedLinkedRooms
li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name
- h3.mt24 Unavailable rooms: Encryption not supported
- .s-card.p0
- ul.my8.ml24
- each row in removedEncryptedRooms
- li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name
h3.mt24 Unavailable rooms: Wrong type
.s-card.p0
ul.my8.ml24
diff --git a/src/web/pug/guild_not_linked.pug b/src/web/pug/guild_not_linked.pug
index 04d2dae..59de2fb 100644
--- a/src/web/pug/guild_not_linked.pug
+++ b/src/web/pug/guild_not_linked.pug
@@ -4,7 +4,7 @@ mixin space(space)
.s-user-card.flex__1
span.s-avatar.s-avatar__32.s-user-card--avatar
if space.avatar
- img.s-avatar--image(src=mUtils.getPublicUrlForMxc(space.avatar) alt="")
+ img.s-avatar--image(src=mUtils.getPublicUrlForMxc(space.avatar))
else
.s-avatar--letter.bg-silver-400.bar-md(aria-hidden="true")= space.name[0]
.s-user-card--info.ai-start
@@ -42,23 +42,12 @@ block body
| You need to log in with Matrix first.
a.s-btn.s-btn__matrix.s-btn__outlined(href=rel(`/log-in-with-matrix`, {next: `./guild?guild_id=${guild_id}`})) Log in with Matrix
- h3.mt48.fs-category Other choices
- .s-card.d-grid.g16
+ h3.mt48.fs-category Auto-create
+ .s-card
form.d-flex.ai-center.g8(method="post" action=rel("/api/autocreate") hx-post=rel("/api/autocreate") hx-indicator="#easy-mode-button")
input(type="hidden" name="guild_id" value=guild_id)
input(type="hidden" name="autocreate" value="true")
label.s-label.fl-grow1
- | Do it automatically
+ | Changed your mind?
p.s-description If you want, OOYE can create and manage the Matrix space so you don't have to.
- button.s-btn.s-btn__icon.s-btn__outlined#easy-mode-button
- != icons.Icons.IconWand
- span.ml4= ` Use easy mode`
-
- form.d-flex.gx16.ai-center(method="post" action=rel("/api/unlink-space"))
- input(type="hidden" name="guild_id" value=guild.id)
- label.s-label.fl-grow1
- | Cancel
- p.s-description Don't want to link this server after all? Here's the button for you.
- button.s-btn.s-btn__icon.s-btn__muted.s-btn__outlined(cx-prevent-default hx-post=rel("/api/unlink-space") hx-indicator="this" hx-disabled-elt="this")
- != icons.Icons.IconUnsync
- span.ml4= ` Unlink`
+ button.s-btn.s-btn__outlined#easy-mode-button Use easy mode
diff --git a/src/web/pug/home.pug b/src/web/pug/home.pug
index 8b86533..d562250 100644
--- a/src/web/pug/home.pug
+++ b/src/web/pug/home.pug
@@ -41,18 +41,16 @@ block body
= ` Set up self-service`
.s-prose
- block bridge-info
- h2 What is this?
- p #[a(href="https://gitdab.com/cadence/out-of-your-element") Out Of Your Element] is a bridge between the Discord and Matrix chat apps. It lets people on both platforms chat with each other without needing to get everyone on the same app.
- p Just chat like usual, and the bridge will forward messages back and forth between the two platforms, so everyone sees the whole conversation.
- p All kinds of content are supported, including pictures, threads, emojis, and @mentions.
- p It's really easy to set up, even if you only have Discord. Just add the bot to your server, and it'll make everything available on Matrix automatically.
+ h2 What is this?
+ p #[a(href="https://gitdab.com/cadence/out-of-your-element") Out Of Your Element] is a bridge between the Discord and Matrix chat apps. It lets people on both platforms chat with each other without needing to get everyone on the same app.
+ p Just chat like usual, and the bridge will forward messages back and forth between the two platforms, so everyone sees the whole conversation.
+ p All kinds of content are supported, including pictures, threads, emojis, and @mentions.
+ p It's really easy to set up, even if you only have Discord. Just add the bot to your server, and it'll make everything available on Matrix automatically.
if locked
- block locked-info
- h2 This is a private instance
- p Anybody can run their own instance of the Out Of Your Element software. The person running this instance has made it private, so you can't add it to your server just yet. If you know who's in charge of #{reg.ooye.server_name}, ask them for the password.
+ h2 This is a private instance
+ p Anybody can run their own instance of the Out Of Your Element software. The person running this instance has made it private, so you can't add it to your server just yet. If you know who's in charge of #{reg.ooye.server_name}, ask them for the password.
- h2 Run your own instance
- p You can still use Out Of Your Element by running your own copy of the software, but this requires some technical skill.
- p To get started, #[a(href="https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/get-started.md") check the installation instructions.]
+ h2 Run your own instance
+ p You can still use Out Of Your Element by running your own copy of the software, but this requires some technical skill.
+ p To get started, #[a(href="https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/get-started.md") check the installation instructions.]
diff --git a/src/web/pug/includes/default-roles-list.pug b/src/web/pug/includes/default-roles-list.pug
deleted file mode 100644
index 8c0a4e0..0000000
--- a/src/web/pug/includes/default-roles-list.pug
+++ /dev/null
@@ -1,19 +0,0 @@
-mixin default-roles-list(guild, guild_id)
- #default-roles-list(style="display: contents")
- each roleID in select("role_default", "role_id", {guild_id}).pluck().all()
- - let r = guild.roles.find(r => r.id === roleID)
- if r
- .s-tag.s-tag__md.fs-body1= r.name
- span(id=`role-loading-${roleID}`)
- button(name="remove_role" value=roleID hx-post="api/default-roles" hx-trigger="click consume" hx-indicator=`#role-loading-${roleID}`).s-tag--dismiss
- != icons.Icons.IconClearSm
-
-mixin add-roles-menu(guild, guild_id)
- ul.s-menu(role="menu" hx-swap-oob="true").overflow-y-auto.overflow-x-hidden#add-roles-menu
- li.s-menu--title.d-flex(role="separator") Select role
- span#add-role-loading
- each r in guild.roles.sort((a, b) => b.position - a.position)
- if r.id !== guild_id && !r.managed
- - let selected = !!select("role_default", "role_id", {guild_id, role_id: r.id}).get()
- li(role="menuitem")
- button(name="toggle_role" value=r.id class={"is-selected": selected}).s-block-link.s-block-link__left= r.name
diff --git a/src/web/pug/includes/template.pug b/src/web/pug/includes/template.pug
index be1d005..d9f1c30 100644
--- a/src/web/pug/includes/template.pug
+++ b/src/web/pug/includes/template.pug
@@ -1,23 +1,13 @@
-mixin guild-menuitem(guild)
- - let bridgedRoomCount = from("channel_room").selectUnsafe("count(*) as count").where({guild_id: guild.id}).and("AND thread_parent IS NULL").get().count
- li(role="menuitem")
- a.s-topbar--item.s-user-card.d-flex.p4(href=rel(`/guild?guild_id=${guild.id}`) class={"bg-purple-200": bridgedRoomCount === 0, "h:bg-purple-300": bridgedRoomCount === 0})
- +guild(guild, bridgedRoomCount)
-
-mixin guild(guild, bridgedRoomCount)
+mixin guild(guild)
span.s-avatar.s-avatar__32.s-user-card--avatar
if guild.icon
- img.s-avatar--image(src=`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png?size=32` alt="")
+ img.s-avatar--image(src=`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png?size=32`)
else
.s-avatar--letter.bg-silver-400.bar-md(aria-hidden="true")= guild.name[0]
.s-user-card--info.ai-start
strong= guild.name
- if bridgedRoomCount != null
- ul.s-user-card--awards
- if bridgedRoomCount
- li #{bridgedRoomCount} bridged rooms
- else
- li.fc-purple Not yet linked
+ ul.s-user-card--awards
+ li #{discord.guildChannelMap.get(guild.id).filter(c => [0, 5, 15, 16].includes(discord.channels.get(c).type)).length} channels
mixin define-theme(name, h, s, l)
style.
@@ -65,12 +55,9 @@ mixin define-themed-button(name, theme)
doctype html
html(lang="en")
head
- block title
- title Out Of Your Element
+ title Out Of Your Element
link(rel="stylesheet" type="text/css" href=rel("/static/stacks.min.css"))
- //- Please use responsibly!!!!!
- link(rel="stylesheet" type="text/css" href=rel("/custom.css"))
meta(name="htmx-config" content='{"requestClass":"is-loading"}')
style.
@@ -89,43 +76,16 @@ html(lang="en")
--_ts-multiple-bg: var(--green-400);
--_ts-multiple-fc: var(--white);
}
- .s-avatar {
- --_av-bg: var(--white);
- }
- .s-avatar .s-avatar--letter {
- color: var(--white);
- }
.s-btn__dropdown:has(+ :popover-open) {
background-color: var(--theme-topbar-item-background-hover, var(--black-200)) !important;
}
- .s-btn__dropdown.s-tag:has(+ :popover-open) .s-tag--dismiss {
- background-color: var(--black-500) !important;
- color: var(--black-150) !important;
- }
- .s-tag .is-loading {
- margin-right: -4px;
- }
- .s-tag .is-loading + .s-tag--dismiss {
- display: none !important;
- }
- a.s-block-link, .s-block-link {
- --_bl-bs-color: var(--green-400);
- }
- @media (prefers-color-scheme: dark) {
- body.theme-system .s-popover {
- --_po-bg: var(--black-100);
- --_po-bc: var(--bc-light);
- --_po-bs: var(--bs-lg);
- --_po-arrow-fc: var(--black-100);
- }
- }
+define-themed-button("matrix", "black")
body.themed.theme-system
header.s-topbar
- a.s-topbar--skip-link(href="#content") Skip to main content
+ .s-topbar--skip-link(href="#content") Skip to main content
.s-topbar--container.wmx9
a.s-topbar--logo(href=rel("/"))
- img.s-avatar.s-avatar__32(src=rel("/icon.png") alt="")
+ img.s-avatar.s-avatar__32(src=rel("/icon.png"))
nav.s-topbar--navigation
ul.s-topbar--content
li.ps-relative.g8
@@ -154,28 +114,19 @@ html(lang="en")
.s-popover--content.overflow-y-auto.overflow-x-hidden
ul.s-menu(role="menu")
each guild in [...managed].map(id => discord.guilds.get(id)).filter(g => g).sort((a, b) => a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1)
- +guild-menuitem(guild)
+ li(role="menuitem")
+ a.s-topbar--item.s-user-card.d-flex.p4(href=rel(`/guild?guild_id=${guild.id}`))
+ +guild(guild)
//- Body
.mx-auto.w100.wmx9.py24.px8.fs-body1#content
block body
//- Guild list popover
script.
document.querySelectorAll("[popovertarget]").forEach(e => {
- const target = document.getElementById(e.getAttribute("popovertarget"))
- e.addEventListener("click", calculate)
- target.addEventListener("toggle", calculate)
- function calculate() {
- const buttonRect = e.getBoundingClientRect()
- const targetRect = target.getBoundingClientRect()
- const t = `:popover-open { position: absolute; top: ${Math.floor(buttonRect.bottom + window.scrollY)}px; left: ${Math.floor(Math.max(targetRect.width / 2, buttonRect.left + buttonRect.width / 2))}px; width: ${Math.floor(buttonRect.width)}px; transform: translateX(-50%); box-sizing: content-box; margin: 0 }`
+ e.addEventListener("click", () => {
+ const rect = e.getBoundingClientRect()
+ const t = `:popover-open { position: absolute; top: ${Math.floor(rect.bottom)}px; left: ${Math.floor(rect.left + rect.width / 2)}px; width: ${Math.floor(rect.width)}px; transform: translateX(-50%); box-sizing: content-box; margin: 0 }`
document.styleSheets[0].insertRule(t, document.styleSheets[0].cssRules.length)
- }
- })
- //- Prevent default
- script.
- document.querySelectorAll("[cx-prevent-default]").forEach(e => {
- e.addEventListener("click", event => {
- event.preventDefault()
})
})
script(src=rel("/static/htmx.js"))
diff --git a/src/web/routes/agi.js b/src/web/routes/agi.js
deleted file mode 100644
index f899455..0000000
--- a/src/web/routes/agi.js
+++ /dev/null
@@ -1,36 +0,0 @@
-// @ts-check
-
-const {z} = require("zod")
-const {defineEventHandler, getValidatedQuery, sendRedirect} = require("h3")
-const {as, from, sync, db} = require("../../passthrough")
-
-/** @type {import("../pug-sync")} */
-const pugSync = sync.require("../pug-sync")
-
-const schema = {
- opt: z.object({
- guild_id: z.string().regex(/^[0-9]+$/)
- })
-}
-
-as.router.get("/agi", defineEventHandler(async event => {
- return pugSync.render(event, "agi.pug", {})
-}))
-
-as.router.get("/agi/optout", defineEventHandler(async event => {
- return pugSync.render(event, "agi-optout.pug", {})
-}))
-
-as.router.post("/agi/optout", defineEventHandler(async event => {
- const parseResult = await getValidatedQuery(event, schema.opt.safeParse)
- if (parseResult.success) {
- db.prepare("INSERT OR IGNORE INTO agi_optout (guild_id) VALUES (?)").run(parseResult.data.guild_id)
- }
- return sendRedirect(event, "", 302)
-}))
-
-as.router.post("/agi/optin", defineEventHandler(async event => {
- const {guild_id} = await getValidatedQuery(event, schema.opt.parse)
- db.prepare("DELETE FROM agi_optout WHERE guild_id = ?").run(guild_id)
- return sendRedirect(event, `../agi?guild_id=${guild_id}`, 302)
-}))
diff --git a/src/web/routes/download-discord.js b/src/web/routes/download-discord.js
index 769fc9c..bbf33b0 100644
--- a/src/web/routes/download-discord.js
+++ b/src/web/routes/download-discord.js
@@ -38,6 +38,7 @@ function timeUntilExpiry(url) {
assert(ex) // refreshed urls from the discord api always include this parameter
const time = parseInt(ex, 16)*1000 - Date.now()
if (time > 0) return time
+ return false
}
function defineMediaProxyHandler(domain) {
@@ -70,7 +71,6 @@ function defineMediaProxyHandler(domain) {
refreshed = await promise
const time = timeUntilExpiry(refreshed)
assert(time) // the just-refreshed URL will always be in the future
- /* c8 ignore next 3 */
setTimeout(() => {
cache.delete(url)
}, time).unref()
@@ -83,5 +83,3 @@ function defineMediaProxyHandler(domain) {
as.router.get(`/download/discordcdn/:channel_id/:attachment_id/:file_name`, defineMediaProxyHandler("cdn.discordapp.com"))
as.router.get(`/download/discordmedia/:channel_id/:attachment_id/:file_name`, defineMediaProxyHandler("media.discordapp.net"))
-
-module.exports._cache = cache
diff --git a/src/web/routes/download-discord.test.js b/src/web/routes/download-discord.test.js
index 0d4b884..b0b0077 100644
--- a/src/web/routes/download-discord.test.js
+++ b/src/web/routes/download-discord.test.js
@@ -1,10 +1,23 @@
// @ts-check
-const assert = require("assert").strict
-const {tryToCatch} = require("try-to-catch")
+const tryToCatch = require("try-to-catch")
const {test} = require("supertape")
const {router} = require("../../../test/web")
-const {_cache} = require("./download-discord")
+const {MatrixServerError} = require("../../matrix/mreq")
+
+const snow = {
+ channel: {
+ async refreshAttachmentURLs(attachments) {
+ if (typeof attachments === "string") attachments = [attachments]
+ return {
+ refreshed_urls: attachments.map(a => ({
+ original: a,
+ refreshed: a + `?ex=${Math.floor(Date.now() / 1000 + 3600).toString(16)}`
+ }))
+ }
+ }
+ }
+}
test("web download discord: access denied if not a known attachment", async t => {
const [error] = await tryToCatch(() =>
@@ -13,7 +26,8 @@ test("web download discord: access denied if not a known attachment", async t =>
channel_id: "1",
attachment_id: "2",
file_name: "image.png"
- }
+ },
+ snow
})
)
t.ok(error)
@@ -28,70 +42,8 @@ test("web download discord: works if a known attachment", async t => {
file_name: "image.png"
},
event,
- snow: {
- channel: {
- async refreshAttachmentURLs(attachments) {
- assert(Array.isArray(attachments))
- return {
- refreshed_urls: attachments.map(a => ({
- original: a,
- refreshed: a + `?ex=${Math.floor(Date.now() / 1000 + 3600).toString(16)}`
- }))
- }
- }
- }
- }
+ snow
})
t.equal(event.node.res.statusCode, 302)
t.match(event.node.res.getHeader("location"), /https:\/\/cdn.discordapp.com\/attachments\/655216173696286746\/1314358913482621010\/image\.png\?ex=/)
})
-
-test("web download discord: uses cache", async t => {
- let notCalled = true
- const event = {}
- await router.test("get", "/download/discordcdn/:channel_id/:attachment_id/:file_name", {
- params: {
- channel_id: "655216173696286746",
- attachment_id: "1314358913482621010",
- file_name: "image.png"
- },
- event,
- snow: {
- channel: {
- /* c8 ignore next 4 */
- async refreshAttachmentURLs(attachments) {
- notCalled = false
- throw new Error("tried to refresh when it should be in cache")
- }
- }
- }
- })
- t.ok(notCalled)
-})
-
-test("web download discord: refreshes when cache has expired", async t => {
- _cache.set(`https://cdn.discordapp.com/attachments/655216173696286746/1314358913482621010/image.png`, Promise.resolve(`https://cdn.discordapp.com/blah?ex=${Math.floor(new Date("2026-01-01").getTime() / 1000 + 3600).toString(16)}`))
- let called = 0
- await router.test("get", "/download/discordcdn/:channel_id/:attachment_id/:file_name", {
- params: {
- channel_id: "655216173696286746",
- attachment_id: "1314358913482621010",
- file_name: "image.png"
- },
- snow: {
- channel: {
- async refreshAttachmentURLs(attachments) {
- called++
- assert(Array.isArray(attachments))
- return {
- refreshed_urls: attachments.map(a => ({
- original: a,
- refreshed: a + `?ex=${Math.floor(Date.now() / 1000 + 3600).toString(16)}`
- }))
- }
- }
- }
- }
- })
- t.equal(called, 1)
-})
diff --git a/src/web/routes/download-matrix.js b/src/web/routes/download-matrix.js
index 82e2f7e..8f790c5 100644
--- a/src/web/routes/download-matrix.js
+++ b/src/web/routes/download-matrix.js
@@ -1,7 +1,7 @@
// @ts-check
const assert = require("assert/strict")
-const {defineEventHandler, getValidatedRouterParams, setResponseStatus, setResponseHeader, createError, H3Event, getValidatedQuery} = require("h3")
+const {defineEventHandler, getValidatedRouterParams, setResponseStatus, setResponseHeader, sendStream, createError, H3Event} = require("h3")
const {z} = require("zod")
/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore
@@ -11,25 +11,10 @@ require("xxhash-wasm")().then(h => hasher = h)
const {sync, as, select} = require("../../passthrough")
-/** @type {import("../../m2d/actions/emoji-sheet")} */
-const emojiSheet = sync.require("../../m2d/actions/emoji-sheet")
-/** @type {import("../../m2d/converters/emoji-sheet")} */
-const emojiSheetConverter = sync.require("../../m2d/converters/emoji-sheet")
-
-/** @type {import("../../m2d/actions/sticker")} */
-const sticker = sync.require("../../m2d/actions/sticker")
-
const schema = {
params: z.object({
server_name: z.string(),
media_id: z.string()
- }),
- sheet: z.object({
- e: z.array(z.string()).or(z.string())
- }),
- sticker: z.object({
- server_name: z.string().regex(/^[^/]+$/),
- media_id: z.string().regex(/^[A-Za-z0-9_-]+$/)
})
}
@@ -42,16 +27,10 @@ function getAPI(event) {
return event.context.api || sync.require("../../matrix/api")
}
-/**
- * @param {H3Event} event
- * @returns {typeof emojiSheet["getAndConvertEmoji"]}
- */
-function getMxcDownloader(event) {
- /* c8 ignore next */
- return event.context.mxcDownloader || emojiSheet.getAndConvertEmoji
-}
+as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(async event => {
+ const params = await getValidatedRouterParams(event, schema.params.parse)
-function verifyMediaHash(serverAndMediaID) {
+ const serverAndMediaID = `${params.server_name}/${params.media_id}`
const unsignedHash = hasher.h64(serverAndMediaID)
const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range
@@ -62,12 +41,7 @@ function verifyMediaHash(serverAndMediaID) {
data: `The file you requested isn't permitted by this media proxy.`
})
}
-}
-as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(async event => {
- const params = await getValidatedRouterParams(event, schema.params.parse)
-
- verifyMediaHash(`${params.server_name}/${params.media_id}`)
const api = getAPI(event)
const res = await api.getMedia(`mxc://${params.server_name}/${params.media_id}`)
@@ -79,32 +53,3 @@ as.router.get(`/download/matrix/:server_name/:media_id`, defineEventHandler(asyn
setResponseHeader(event, "Transfer-Encoding", "chunked")
return res.body
}))
-
-as.router.get(`/download/sheet`, defineEventHandler(async event => {
- const query = await getValidatedQuery(event, schema.sheet.parse)
-
- /** remember that these have no mxc:// protocol in the string for space reasons */
- let mxcs = query.e
- if (!Array.isArray(mxcs)) {
- mxcs = [mxcs]
- }
-
- for (const serverAndMediaID of mxcs) {
- verifyMediaHash(serverAndMediaID)
- }
-
- const buffer = await emojiSheetConverter.compositeMatrixEmojis(mxcs.map(s => `mxc://${s}`), getMxcDownloader(event))
- setResponseHeader(event, "Content-Type", "image/png")
- return buffer
-}))
-
-as.router.get(`/download/sticker/:server_name/:media_id/_.webp`, defineEventHandler(async event => {
- const {server_name, media_id} = await getValidatedRouterParams(event, schema.sticker.parse)
- /** remember that this has no mxc:// protocol in the string */
- const mxc = server_name + "/" + media_id
- verifyMediaHash(mxc)
-
- const stream = await sticker.getAndResizeSticker(`mxc://${mxc}`)
- setResponseHeader(event, "Content-Type", "image/webp")
- return stream
-}))
diff --git a/src/web/routes/download-matrix.test.js b/src/web/routes/download-matrix.test.js
index 610a62d..421d2da 100644
--- a/src/web/routes/download-matrix.test.js
+++ b/src/web/routes/download-matrix.test.js
@@ -1,11 +1,8 @@
// @ts-check
-const fs = require("fs")
-const {convertImageStream} = require("../../m2d/converters/emoji-sheet")
-const {tryToCatch} = require("try-to-catch")
+const tryToCatch = require("try-to-catch")
const {test} = require("supertape")
const {router} = require("../../../test/web")
-const streamWeb = require("stream/web")
test("web download matrix: access denied if not a known attachment", async t => {
const [error] = await tryToCatch(() =>
@@ -28,7 +25,6 @@ test("web download matrix: works if a known attachment", async t => {
},
event,
api: {
- // @ts-ignore
async getMedia(mxc, init) {
return new Response("", {status: 200, headers: {"content-type": "image/png"}})
}
@@ -37,52 +33,3 @@ test("web download matrix: works if a known attachment", async t => {
t.equal(event.node.res.statusCode, 200)
t.equal(event.node.res.getHeader("content-type"), "image/png")
})
-
-/**
- * MOCK: Gets the emoji from the filesystem and converts to uncompressed PNG data.
- * @param {string} mxc a single mxc:// URL
- * @returns {Promise} uncompressed PNG data, or undefined if the downloaded emoji is not valid
-*/
-async function mockGetAndConvertEmoji(mxc) {
- const id = mxc.match(/\/([^./]*)$/)?.[1]
- let s
- if (fs.existsSync(`test/res/${id}.png`)) {
- s = fs.createReadStream(`test/res/${id}.png`)
- } else {
- s = fs.createReadStream(`test/res/${id}.gif`)
- }
- return convertImageStream(s, () => {
- s.pause()
- s.emit("end")
- })
-}
-
-test("web sheet: single emoji", async t => {
- const event = {}
- const sheet = await router.test("get", "/download/sheet?e=cadence.moe%2FRLMgJGfgTPjIQtvvWZsYjhjy", {
- event,
- mxcDownloader: mockGetAndConvertEmoji
- })
- t.equal(event.node.res.statusCode, 200)
- t.equal(sheet.subarray(0, 90).toString("base64"), "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAPoAAAD6AG1e1JrAAALoklEQVR4nM1ZaVBU2RU+LZSIGnAvFUtcRkSk6abpbkDH")
-})
-
-test("web sheet: multiple sources", async t => {
- const event = {}
- const sheet = await router.test("get", "/download/sheet?e=cadence.moe%2FWbYqNlACRuicynBfdnPYtmvc&e=cadence.moe%2FHYcztccFIPgevDvoaWNsEtGJ", {
- event,
- mxcDownloader: mockGetAndConvertEmoji
- })
- t.equal(event.node.res.statusCode, 200)
- t.equal(sheet.subarray(0, 90).toString("base64"), "iVBORw0KGgoAAAANSUhEUgAAAGAAAAAwCAYAAADuFn/PAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAT/klEQVR4nOVcC3CVRZbuS2KAIMpDQt5PQkIScm/uvYRX")
-})
-
-test("web sheet: big sheet", async t => {
- const event = {}
- const sheet = await router.test("get", "/download/sheet?e=cadence.moe%2FlHfmJpzgoNyNtYHdAmBHxXix&e=cadence.moe%2FMtRdXixoKjKKOyHJGWLsWLNU&e=cadence.moe%2FHXfFuougamkURPPMflTJRxGc&e=cadence.moe%2FikYKbkhGhMERAuPPbsnQzZiX&e=cadence.moe%2FAYPpqXzVJvZdzMQJGjioIQBZ&e=cadence.moe%2FUVuzvpVUhqjiueMxYXJiFEAj&e=cadence.moe%2FlHfmJpzgoNyNtYHdAmBHxXix&e=cadence.moe%2FMtRdXixoKjKKOyHJGWLsWLNU&e=cadence.moe%2FHXfFuougamkURPPMflTJRxGc&e=cadence.moe%2FikYKbkhGhMERAuPPbsnQzZiX&e=cadence.moe%2FAYPpqXzVJvZdzMQJGjioIQBZ&e=cadence.moe%2FUVuzvpVUhqjiueMxYXJiFEAj", {
- event,
- mxcDownloader: mockGetAndConvertEmoji
- })
- t.equal(event.node.res.statusCode, 200)
- t.equal(sheet.subarray(0, 90).toString("base64"), "iVBORw0KGgoAAAANSUhEUgAAAYAAAABgCAYAAAAU9KWJAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAgAElEQVR4nOx9B3hUVdr/KIpKL2nT0pPpLRNQkdXddV1c")
-})
diff --git a/src/web/routes/guild-settings.js b/src/web/routes/guild-settings.js
index 8119f93..b640d36 100644
--- a/src/web/routes/guild-settings.js
+++ b/src/web/routes/guild-settings.js
@@ -4,12 +4,10 @@ const assert = require("assert/strict")
const {z} = require("zod")
const {defineEventHandler, createError, readValidatedBody, getRequestHeader, setResponseHeader, sendRedirect, H3Event} = require("h3")
-const {as, db, sync, select, discord} = require("../../passthrough")
+const {as, db, sync, select} = require("../../passthrough")
/** @type {import("../auth")} */
const auth = sync.require("../auth")
-/** @type {import("../pug-sync")} */
-const pugSync = sync.require("../pug-sync")
/** @type {import("../../d2m/actions/set-presence")} */
const setPresence = sync.require("../../d2m/actions/set-presence")
@@ -22,14 +20,6 @@ function getCreateSpace(event) {
return event.context.createSpace || sync.require("../../d2m/actions/create-space")
}
-const schema = {
- defaultRoles: z.object({
- guild_id: z.string(),
- toggle_role: z.string().optional(),
- remove_role: z.string().optional()
- })
-}
-
/**
* @typedef Options
* @prop {(value: string?) => number} transform
@@ -84,8 +74,6 @@ as.router.post("/api/autocreate", defineToggle("autocreate", {
as.router.post("/api/url-preview", defineToggle("url_preview"))
-as.router.post("/api/webhook-profile", defineToggle("webhook_profile"))
-
as.router.post("/api/presence", defineToggle("presence", {
after() {
setPresence.guildPresenceSetting.update()
@@ -104,39 +92,3 @@ as.router.post("/api/privacy-level", defineToggle("privacy_level", {
await createSpace.syncSpaceFully(guildID) // this is inefficient but OK to call infrequently on user request
}
}))
-
-as.router.post("/api/default-roles", defineEventHandler(async event => {
- const parsedBody = await readValidatedBody(event, schema.defaultRoles.parse)
-
- const managed = await auth.getManagedGuilds(event)
- const guildID = parsedBody.guild_id
- if (!managed.has(guildID)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"})
-
- const roleID = parsedBody.toggle_role || parsedBody.remove_role
- assert(roleID)
- assert.notEqual(guildID, roleID) // the @everyone role is always default
-
- const guild = discord.guilds.get(guildID)
- assert(guild)
-
- let shouldRemove = !!parsedBody.remove_role
- if (!shouldRemove) {
- shouldRemove = !!select("role_default", "role_id", {guild_id: guildID, role_id: roleID}).get()
- }
-
- if (shouldRemove) {
- db.prepare("DELETE FROM role_default WHERE guild_id = ? AND role_id = ?").run(guildID, roleID)
- } else {
- assert(guild.roles.find(r => r.id === roleID))
- db.prepare("INSERT OR IGNORE INTO role_default (guild_id, role_id) VALUES (?, ?)").run(guildID, roleID)
- }
-
- const createSpace = getCreateSpace(event)
- await createSpace.syncSpaceFully(guildID) // this is inefficient but OK to call infrequently on user request
-
- if (getRequestHeader(event, "HX-Request")) {
- return pugSync.render(event, "fragments/default-roles-list.pug", {guild, guild_id: guildID})
- } else {
- return sendRedirect(event, `/guild?guild_id=${guildID}`, 302)
- }
-}))
diff --git a/src/web/routes/guild-settings.test.js b/src/web/routes/guild-settings.test.js
index 00acb89..fccc266 100644
--- a/src/web/routes/guild-settings.test.js
+++ b/src/web/routes/guild-settings.test.js
@@ -1,6 +1,6 @@
// @ts-check
-const {tryToCatch} = require("try-to-catch")
+const tryToCatch = require("try-to-catch")
const {router, test} = require("../../../test/web")
const {select} = require("../../passthrough")
const {MatrixServerError} = require("../../matrix/mreq")
diff --git a/src/web/routes/guild.js b/src/web/routes/guild.js
index 70092d5..8c2d99d 100644
--- a/src/web/routes/guild.js
+++ b/src/web/routes/guild.js
@@ -18,9 +18,7 @@ const createSpace = sync.require("../../d2m/actions/create-space")
/** @type {import("../auth")} */
const auth = require("../auth")
/** @type {import("../../discord/utils")} */
-const dUtils = sync.require("../../discord/utils")
-/** @type {import("../../matrix/utils")} */
-const mxUtils = sync.require("../../matrix/utils")
+const utils = sync.require("../../discord/utils")
const {reg} = require("../../matrix/read-registration")
const schema = {
@@ -54,40 +52,23 @@ function getAPI(event) {
const validNonce = new LRUCache({max: 200})
/**
- * @param {{type: number, parent_id?: string | null, position?: number}} channel
- * @param {Map} channels
+ * Modifies the input, removing items that don't pass the filter. Returns the items that didn't pass.
+ * @param {T[]} xs
+ * @param {(x: T, i?: number) => any} fn
+ * @template T
+ * @returns T[]
*/
-function getPosition(channel, channels) {
- let position = 0
-
- // Categories always appear below un-categorised channels. Their contents can be ordered.
- // So categories, and things in them, will have their position multiplied by a big number. The category's big number. The regular position small number sorts within the category.
- // Categories are size 2000.
- let foundCategory = channel
- while (foundCategory.parent_id) {
- const f = channels.get(foundCategory.parent_id)
- assert(f)
- foundCategory = f
+function filterTo(xs, fn) {
+ /** @type {T[]} */
+ const filtered = []
+ for (let i = xs.length-1; i >= 0; i--) {
+ const x = xs[i]
+ if (!fn(x, i)) {
+ filtered.unshift(x)
+ xs.splice(i, 1)
+ }
}
- if (foundCategory.type === DiscordTypes.ChannelType.GuildCategory) position = ((foundCategory.position || 0) + 1) * 2000
-
- // Categories always appear above what they contain.
- if (channel.type === DiscordTypes.ChannelType.GuildCategory) position -= 0.5
-
- // Within a category, voice channels are always sorted to the bottom. The text/voice split is size 1000 each.
- if ([DiscordTypes.ChannelType.GuildVoice, DiscordTypes.ChannelType.GuildStageVoice].includes(channel.type)) position += 1000
-
- // Channels are manually ordered within the text/voice split.
- if (typeof channel.position === "number") position += channel.position
-
- // Threads appear below their channel.
- if ([DiscordTypes.ChannelType.PublicThread, DiscordTypes.ChannelType.PrivateThread, DiscordTypes.ChannelType.AnnouncementThread].includes(channel.type)) {
- position += 0.5
- let parent = channels.get(channel.parent_id || "")
- if (parent && parent["position"]) position += parent["position"]
- }
-
- return position
+ return filtered
}
/**
@@ -96,58 +77,50 @@ function getPosition(channel, channels) {
* @param {string[]} roles
*/
function getChannelRoomsLinks(guild, rooms, roles) {
+ function getPosition(channel) {
+ let position = 0
+ let looking = channel
+ while (looking.parent_id) {
+ looking = discord.channels.get(looking.parent_id)
+ position = looking.position * 1000
+ }
+ if (channel.position) position += channel.position
+ return position
+ }
+
let channelIDs = discord.guildChannelMap.get(guild.id)
assert(channelIDs)
let linkedChannels = select("channel_room", ["channel_id", "room_id", "name", "nick"], {channel_id: channelIDs}).all()
- let linkedChannelsWithDetails = linkedChannels.map(c => ({
- // @ts-ignore
- /** @type {DiscordTypes.APIGuildChannel} */ channel: discord.channels.get(c.channel_id),
- ...c
- }))
- let removedUncachedChannels = dUtils.filterTo(linkedChannelsWithDetails, c => c.channel)
+ let linkedChannelsWithDetails = linkedChannels.map(c => ({channel: discord.channels.get(c.channel_id), ...c}))
+ let removedUncachedChannels = filterTo(linkedChannelsWithDetails, c => c.channel)
let linkedChannelIDs = linkedChannelsWithDetails.map(c => c.channel_id)
- linkedChannelsWithDetails.sort((a, b) => getPosition(a.channel, discord.channels) - getPosition(b.channel, discord.channels))
+ linkedChannelsWithDetails.sort((a, b) => getPosition(a.channel) - getPosition(b.channel))
let unlinkedChannelIDs = channelIDs.filter(c => !linkedChannelIDs.includes(c))
/** @type {DiscordTypes.APIGuildChannel[]} */ // @ts-ignore
let unlinkedChannels = unlinkedChannelIDs.map(c => discord.channels.get(c))
- let removedWrongTypeChannels = dUtils.filterTo(unlinkedChannels, c => c && [0, 5].includes(c.type))
- let removedPrivateChannels = dUtils.filterTo(unlinkedChannels, c => {
- const permissions = dUtils.getPermissions(guild.id, roles, guild.roles, botID, c["permission_overwrites"])
- return dUtils.hasSomePermissions(permissions, ["Administrator", "ViewChannel"])
+ let removedWrongTypeChannels = filterTo(unlinkedChannels, c => c && [0, 5].includes(c.type))
+ let removedPrivateChannels = filterTo(unlinkedChannels, c => {
+ const permissions = utils.getPermissions(roles, guild.roles, botID, c["permission_overwrites"])
+ return utils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.ViewChannel)
})
- unlinkedChannels.sort((a, b) => getPosition(a, discord.channels) - getPosition(b, discord.channels))
+ unlinkedChannels.sort((a, b) => getPosition(a) - getPosition(b))
let linkedRoomIDs = linkedChannels.map(c => c.room_id)
let unlinkedRooms = [...rooms]
- let removedLinkedRooms = dUtils.filterTo(unlinkedRooms, r => !linkedRoomIDs.includes(r.room_id))
- let removedWrongTypeRooms = dUtils.filterTo(unlinkedRooms, r => !r.room_type)
- let removedEncryptedRooms = dUtils.filterTo(unlinkedRooms, r => !r.encryption && !r["im.nheko.summary.encryption"])
+ let removedLinkedRooms = filterTo(unlinkedRooms, r => !linkedRoomIDs.includes(r.room_id))
+ let removedWrongTypeRooms = filterTo(unlinkedRooms, r => !r.room_type)
// https://discord.com/developers/docs/topics/threads#active-archived-threads
// need to filter out linked archived threads from unlinkedRooms, will just do that by comparing against the name
- let removedArchivedThreadRooms = dUtils.filterTo(unlinkedRooms, r => r.name && !r.name.match(/^\[(🔒)?⛓️\]/))
+ let removedArchivedThreadRooms = filterTo(unlinkedRooms, r => r.name && !r.name.match(/^\[(🔒)?⛓️\]/))
return {
linkedChannelsWithDetails, unlinkedChannels, unlinkedRooms,
- removedUncachedChannels, removedWrongTypeChannels, removedPrivateChannels, removedLinkedRooms, removedWrongTypeRooms, removedArchivedThreadRooms, removedEncryptedRooms
+ removedUncachedChannels, removedWrongTypeChannels, removedPrivateChannels, removedLinkedRooms, removedWrongTypeRooms, removedArchivedThreadRooms
}
}
-/**
- * @param {string} mxid
- */
-function getInviteTargetSpaces(mxid) {
- /** @type {{room_id: string, mxid: string, type: string, name: string, topic: string?, avatar: string?}[]} */
- const spaces =
- // invited spaces
- db.prepare("SELECT room_id, invite.mxid, type, name, topic, avatar FROM invite LEFT JOIN guild_space ON invite.room_id = guild_space.space_id WHERE mxid = ? AND space_id IS NULL AND type = 'm.space'").all(mxid)
- // moderated spaces
- .concat(db.prepare("SELECT room_id, invite.mxid, type, name, topic, avatar FROM invite LEFT JOIN guild_space ON invite.room_id = guild_space.space_id INNER JOIN member_cache USING (room_id) WHERE member_cache.mxid = ? AND power_level >= 50 AND space_id IS NULL AND type = 'm.space'").all(mxid))
- const seen = new Set(spaces.map(s => s.room_id))
- return spaces.filter(s => seen.delete(s.room_id))
-}
-
as.router.get("/guild", defineEventHandler(async event => {
const {guild_id} = await getValidatedQuery(event, schema.guild.parse)
const session = await auth.useSession(event)
@@ -163,7 +136,7 @@ as.router.get("/guild", defineEventHandler(async event => {
// Self-service guild that hasn't been linked yet - needs a special page encouraging the link flow
if (!row.space_id && row.autocreate === 0) {
- const spaces = session.data.mxid ? getInviteTargetSpaces(session.data.mxid) : []
+ const spaces = db.prepare("SELECT room_id, type, name, topic, avatar FROM invite LEFT JOIN guild_space ON invite.room_id = guild_space.space_id WHERE mxid = ? AND space_id IS NULL AND type = 'm.space'").all(session.data.mxid)
return pugSync.render(event, "guild_not_linked.pug", {guild, guild_id, spaces})
}
@@ -255,7 +228,7 @@ as.router.post("/api/invite", defineEventHandler(async event => {
( parsedBody.permissions === "admin" ? 100
: parsedBody.permissions === "moderator" ? 50
: 0)
- if (powerLevel) await mxUtils.setUserPowerCascade(spaceID, parsedBody.mxid, powerLevel, api)
+ if (powerLevel) await api.setUserPowerCascade(spaceID, parsedBody.mxid, powerLevel)
if (parsedBody.guild_id) {
setResponseHeader(event, "HX-Refresh", true)
@@ -264,6 +237,3 @@ as.router.post("/api/invite", defineEventHandler(async event => {
return sendRedirect(event, "/ok?msg=User has been invited.", 302)
}
}))
-
-module.exports._getPosition = getPosition
-module.exports.getInviteTargetSpaces = getInviteTargetSpaces
diff --git a/src/web/routes/guild.test.js b/src/web/routes/guild.test.js
index 06b604b..ea59173 100644
--- a/src/web/routes/guild.test.js
+++ b/src/web/routes/guild.test.js
@@ -1,10 +1,8 @@
// @ts-check
-const DiscordTypes = require("discord-api-types/v10")
-const {tryToCatch} = require("try-to-catch")
+const tryToCatch = require("try-to-catch")
const {router, test} = require("../../../test/web")
const {MatrixServerError} = require("../../matrix/mreq")
-const {_getPosition} = require("./guild")
let nonce
@@ -91,7 +89,7 @@ test("web guild: unbridged self-service guild shows available spaces", async t =
})
t.has(html, `Data Horde`)
t.has(html, `here is the space topic `)
- t.has(html, `
`)
+ t.has(html, `
`)
t.notMatch(html, /some room<\/strong>/)
t.notMatch(html, /somebody else's space<\/strong>/)
})
@@ -103,6 +101,12 @@ test("web guild: can view bridged guild when logged in with discord", async t =>
managedGuilds: ["112760669178241024"]
},
api: {
+ async getStateEvent(roomID, type, key) {
+ return {}
+ },
+ async getMembers(roomID, membership) {
+ return {chunk: []}
+ },
async getFullHierarchy(roomID) {
return []
}
@@ -117,6 +121,12 @@ test("web guild: can view bridged guild when logged in with matrix", async t =>
mxid: "@cadence:cadence.moe"
},
api: {
+ async getStateEvent(roomID, type, key) {
+ return {}
+ },
+ async getMembers(roomID, membership) {
+ return {chunk: []}
+ },
async getFullHierarchy(roomID) {
return []
}
@@ -180,66 +190,21 @@ test("api invite: can invite with valid nonce", async t => {
api: {
async getStateEvent(roomID, type, key) {
called++
- if (type === "m.room.member" && key === "@cadence:cadence.moe") {
- throw new Error("event not found")
- } else if (type === "m.room.power_levels" && key === "") {
- return {}
- }
- /* c8 ignore next */
- t.fail(`unexpected getStateEvent call. roomID: ${roomID}, type: ${type}, key: ${key}`)
- },
- async getStateEventOuter(roomID, type, key) {
- called++
- return {
- type: "m.room.create",
- state_key: "",
- sender: "@_ooye_bot:cadence.moe",
- event_id: "$create",
- origin_server_ts: 0,
- room_id: roomID,
- content: {
- room_version: "11"
- }
- }
+ return {membership: "leave"}
},
async inviteToRoom(roomID, mxidToInvite, mxid) {
- called++
t.equal(roomID, "!jjmvBegULiLucuWEHU:cadence.moe")
- },
- async *generateFullHierarchy(spaceID) {
called++
- yield {
- room_id: "!hierarchy",
- children_state: [],
- guest_can_join: false,
- num_joined_members: 2,
- }
},
- async sendState(roomID, type, key, content) {
+ async setUserPowerCascade(roomID, mxid, power) {
+ t.equal(power, 50) // moderator
called++
- t.ok(["!hierarchy", "!jjmvBegULiLucuWEHU:cadence.moe"].includes(roomID), `expected room ID to be in hierarchy, but was ${roomID}`)
- t.equal(type, "m.room.power_levels")
- t.equal(key, "")
- t.deepEqual(content, {
- users: {"@cadence:cadence.moe": 50}
- })
- return "$updated"
}
}
})
)
t.notOk(error)
- /*
- 1. get membership
- 2. invite to room
- set power:
- 3. generate hierarchy
- 4-5. calculate powers
- 6. send state
- 7-8. calculate powers
- 9. send state
- */
- t.equal(called, 9) // get membership +
+ t.equal(called, 3)
})
test("api invite: access denied when nonce has been used", async t => {
@@ -270,63 +235,21 @@ test("api invite: can invite to a moderated guild", async t => {
api: {
async getStateEvent(roomID, type, key) {
called++
- if (type === "m.room.member" && key === "@cadence:cadence.moe") {
- return {membership: "leave"}
- } else if (type === "m.room.power_levels" && key === "") {
- return {}
- }
- /* c8 ignore next */
- t.fail(`unexpected getStateEvent call. roomID: ${roomID}, type: ${type}, key: ${key}`)
- },
- async getStateEventOuter(roomID, type, key) {
- called++
- return {
- type: "m.room.create",
- state_key: "",
- sender: "@_ooye_bot:cadence.moe",
- event_id: "$create",
- origin_server_ts: 0,
- room_id: roomID,
- content: {
- room_version: "11"
- }
- }
+ throw new MatrixServerError({errcode: "M_NOT_FOUND", error: "Event not found or something"})
},
async inviteToRoom(roomID, mxidToInvite, mxid) {
- called++
t.equal(roomID, "!jjmvBegULiLucuWEHU:cadence.moe")
- },
- async *generateFullHierarchy(spaceID) {
called++
- yield {
- room_id: "!hierarchy",
- children_state: [],
- guest_can_join: false,
- num_joined_members: 2,
- }
- yield {
- room_id: spaceID,
- children_state: [],
- guest_can_join: false,
- num_joined_members: 2,
- room_type: "m.space"
- }
},
- async sendState(roomID, type, key, content) {
+ async setUserPowerCascade(roomID, mxid, power) {
+ t.equal(power, 100) // moderator
called++
- t.ok(["!hierarchy", "!jjmvBegULiLucuWEHU:cadence.moe"].includes(roomID), `expected room ID to be in hierarchy, but was ${roomID}`)
- t.equal(type, "m.room.power_levels")
- t.equal(key, "")
- t.deepEqual(content, {
- users: {"@cadence:cadence.moe": 100}
- })
- return "$updated"
}
}
})
)
t.notOk(error)
- t.equal(called, 9)
+ t.equal(called, 3)
})
test("api invite: does not reinvite joined users", async t => {
@@ -352,45 +275,3 @@ test("api invite: does not reinvite joined users", async t => {
t.notOk(error)
t.equal(called, 1)
})
-
-
-test("position sorting: sorts like discord does", t => {
- const channelsList = [{
- type: DiscordTypes.ChannelType.GuildText,
- id: "first",
- position: 0
- }, {
- type: DiscordTypes.ChannelType.PublicThread,
- id: "thread",
- parent_id: "first",
- }, {
- type: DiscordTypes.ChannelType.GuildText,
- id: "second",
- position: 1
- }, {
- type: DiscordTypes.ChannelType.GuildVoice,
- id: "voice",
- position: 0
- }, {
- type: DiscordTypes.ChannelType.GuildCategory,
- id: "category",
- position: 0
- }, {
- type: DiscordTypes.ChannelType.GuildText,
- id: "category-first",
- parent_id: "category",
- position: 0
- }, {
- type: DiscordTypes.ChannelType.GuildText,
- id: "category-second",
- parent_id: "category",
- position: 1
- }, {
- type: DiscordTypes.ChannelType.PublicThread,
- id: "category-second-thread",
- parent_id: "category-second",
- }].reverse()
- const channels = new Map(channelsList.map(c => [c.id, c]))
- const sortedChannelIDs = [...channels.values()].sort((a, b) => _getPosition(a, channels) - _getPosition(b, channels)).map(c => c.id)
- t.deepEqual(sortedChannelIDs, ["first", "thread", "second", "voice", "category", "category-first", "category-second", "category-second-thread"])
-})
diff --git a/src/web/routes/info.js b/src/web/routes/info.js
index e83bf89..0ccdeca 100644
--- a/src/web/routes/info.js
+++ b/src/web/routes/info.js
@@ -4,8 +4,8 @@ const {z} = require("zod")
const {defineEventHandler, getValidatedQuery, H3Event} = require("h3")
const {as, from, sync, select} = require("../../passthrough")
-/** @type {import("../../matrix/utils")} */
-const mUtils = sync.require("../../matrix/utils")
+/** @type {import("../../m2d/converters/utils")} */
+const mUtils = sync.require("../../m2d/converters/utils")
/**
* @param {H3Event} event
@@ -26,28 +26,16 @@ as.router.get("/api/message", defineEventHandler(async event => {
const api = getAPI(event)
const {message_id} = await getValidatedQuery(event, schema.message.parse)
- const metadatas = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index").where({message_id})
- .select("event_id", "event_type", "event_subtype", "part", "reaction_part", "reference_channel_id", "room_id", "source").and("ORDER BY part ASC, reaction_part DESC").all()
+ const metadatas = from("event_message").join("message_channel", "message_id").join("channel_room", "channel_id").where({message_id})
+ .select("event_id", "event_type", "event_subtype", "part", "reaction_part", "room_id", "source").and("ORDER BY part ASC, reaction_part DESC").all()
if (metadatas.length === 0) {
return new Response("Message not found", {status: 404, statusText: "Not Found"})
}
- const current_room_id = select("channel_room", "room_id", {channel_id: metadatas[0].reference_channel_id}).pluck().get()
const events = await Promise.all(metadatas.map(metadata =>
api.getEvent(metadata.room_id, metadata.event_id).then(raw => ({
- metadata: {
- event_id: metadata.event_id,
- event_type: metadata.event_type,
- event_subtype: metadata.event_subtype,
- part: metadata.part,
- reaction_part: metadata.reaction_part,
- channel_id: metadata.reference_channel_id,
- room_id: metadata.room_id,
- source: metadata.source,
- sender: raw.sender,
- current_room_id: current_room_id
- },
+ metadata: Object.assign({sender: raw.sender}, metadata),
raw
}))
))
@@ -68,7 +56,8 @@ as.router.get("/api/message", defineEventHandler(async event => {
}
}
if (!matrix_author.displayname) matrix_author.displayname = mxid
- matrix_author.avatar_url = mUtils.getPublicUrlForMxc(matrix_author.avatar_url) || null
+ if (matrix_author.avatar_url) matrix_author.avatar_url = mUtils.getPublicUrlForMxc(matrix_author.avatar_url)
+ else matrix_author.avatar_url = null
matrix_author["mxid"] = mxid
}
diff --git a/src/web/routes/info.test.js b/src/web/routes/info.test.js
index 39b2c00..28dac3b 100644
--- a/src/web/routes/info.test.js
+++ b/src/web/routes/info.test.js
@@ -57,16 +57,14 @@ test("web info: returns data for a matrix message and profile", async t => {
},
events: [{
metadata: {
- event_id: "$51gH61p_eJc2RylOdE2lAr4-ogP7dS0WJI62lCFzBvk",
- event_subtype: "m.text",
- event_type: "m.room.message",
- part: 0,
- reaction_part: 0,
- room_id: "!qzDBLKlildpzrrOnFZ:cadence.moe",
- channel_id: "176333891320283136",
- current_room_id: "!qzDBLKlildpzrrOnFZ:cadence.moe",
- sender: "@cadence:cadence.moe",
- source: 0
+ event_id: "$51gH61p_eJc2RylOdE2lAr4-ogP7dS0WJI62lCFzBvk",
+ event_subtype: "m.text",
+ event_type: "m.room.message",
+ part: 0,
+ reaction_part: 0,
+ room_id: "!qzDBLKlildpzrrOnFZ:cadence.moe",
+ sender: "@cadence:cadence.moe",
+ source: 0
},
raw
}]
@@ -115,16 +113,14 @@ test("web info: returns data for a matrix message without profile", async t => {
},
events: [{
metadata: {
- event_id: "$51gH61p_eJc2RylOdE2lAr4-ogP7dS0WJI62lCFzBvk",
- event_subtype: "m.text",
- event_type: "m.room.message",
- part: 0,
- reaction_part: 0,
- room_id: "!qzDBLKlildpzrrOnFZ:cadence.moe",
- channel_id: "176333891320283136",
- current_room_id: "!qzDBLKlildpzrrOnFZ:cadence.moe",
- sender: "@cadence:cadence.moe",
- source: 0
+ event_id: "$51gH61p_eJc2RylOdE2lAr4-ogP7dS0WJI62lCFzBvk",
+ event_subtype: "m.text",
+ event_type: "m.room.message",
+ part: 0,
+ reaction_part: 0,
+ room_id: "!qzDBLKlildpzrrOnFZ:cadence.moe",
+ sender: "@cadence:cadence.moe",
+ source: 0
},
raw
}]
@@ -195,16 +191,14 @@ test("web info: returns data for a discord message", async t => {
matrix_author: undefined,
events: [{
metadata: {
- event_id: "$AfrB8hzXkDMvuoWjSZkDdFYomjInWH7jMBPkwQMN8AI",
- event_subtype: "m.text",
- event_type: "m.room.message",
- part: 0,
- reaction_part: 1,
- room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
- channel_id: "112760669178241024",
- current_room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
- sender: "@_ooye_accavish:cadence.moe",
- source: 1
+ event_id: "$AfrB8hzXkDMvuoWjSZkDdFYomjInWH7jMBPkwQMN8AI",
+ event_subtype: "m.text",
+ event_type: "m.room.message",
+ part: 0,
+ reaction_part: 1,
+ room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
+ sender: "@_ooye_accavish:cadence.moe",
+ source: 1
},
raw: raw1
}, {
@@ -215,8 +209,6 @@ test("web info: returns data for a discord message", async t => {
part: 1,
reaction_part: 0,
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
- channel_id: "112760669178241024",
- current_room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
sender: "@_ooye_accavish:cadence.moe",
source: 1
},
diff --git a/src/web/routes/link.js b/src/web/routes/link.js
index 772a19c..0afbc49 100644
--- a/src/web/routes/link.js
+++ b/src/web/routes/link.js
@@ -1,6 +1,5 @@
// @ts-check
-const assert = require("assert").strict
const {z} = require("zod")
const {defineEventHandler, createError, readValidatedBody, setResponseHeader, H3Event} = require("h3")
const Ty = require("../../types")
@@ -9,10 +8,9 @@ const DiscordTypes = require("discord-api-types/v10")
const {discord, db, as, sync, select, from} = require("../../passthrough")
/** @type {import("../auth")} */
const auth = sync.require("../auth")
-/** @type {import("../../matrix/utils")}*/
-const utils = sync.require("../../matrix/utils")
-/** @type {import("./guild")}*/
-const guildRoute = sync.require("./guild")
+/** @type {import("../../matrix/mreq")} */
+const mreq = sync.require("../../matrix/mreq")
+const {reg} = require("../../matrix/read-registration")
/**
* @param {H3Event} event
@@ -41,15 +39,6 @@ function getCreateSpace(event) {
return event.context.createSpace || sync.require("../../d2m/actions/create-space")
}
-/**
- * @param {H3Event} event
- * @returns {import("snowtransfer").SnowTransfer}
- */
-function getSnow(event) {
- /* c8 ignore next */
- return event.context.snow || discord.snow
-}
-
const schema = {
linkSpace: z.object({
guild_id: z.string(),
@@ -63,37 +52,7 @@ const schema = {
unlink: z.object({
guild_id: z.string(),
channel_id: z.string()
- }),
- unlinkSpace: z.object({
- guild_id: z.string(),
- }),
-}
-
-/**
- * @param {H3Event} event
- * @param {string} channel_id
- * @param {string} guild_id
- */
-async function validateAndUnbridgeChannel(event, channel_id, guild_id) {
- const createRoom = getCreateRoom(event)
-
- // Check channel is currently bridged
- const row = select("channel_room", "channel_id", {channel_id: channel_id}).get()
- if (!row) throw createError({status: 400, message: "Bad Request", data: `Channel ID ${channel_id} is not currently bridged`})
-
- // Check that the channel (if it exists) is part of this guild
- /** @type {any} */
- let channel = discord.channels.get(channel_id)
- if (channel) {
- if (!("guild_id" in channel) || channel.guild_id !== guild_id) throw createError({status: 400, message: "Bad Request", data: `Channel ID ${channel_id} is not part of guild ${guild_id}`})
- } else {
- // Otherwise, if the channel isn't cached, it must have been deleted.
- // There's no other authentication here - it's okay for anyone to unlink a deleted channel just by knowing its ID.
- channel = {id: channel_id}
- }
-
- // Do it
- await createRoom.unbridgeChannel(channel, guild_id)
+ })
}
as.router.post("/api/link-space", defineEventHandler(async event => {
@@ -109,18 +68,16 @@ as.router.post("/api/link-space", defineEventHandler(async event => {
// Check space ID
if (!session.data.mxid) throw createError({status: 403, message: "Forbidden", data: "Can't link with your Matrix space if you aren't logged in to Matrix"})
const spaceID = parsedBody.space_id
+ const inviteType = select("invite", "type", {mxid: session.data.mxid, room_id: spaceID}).pluck().get()
+ if (inviteType !== "m.space") throw createError({status: 403, message: "Forbidden", data: "You personally must invite OOYE to that space on Matrix"})
// Check they are not already bridged
const existing = select("guild_space", "guild_id", {}, "WHERE guild_id = ? OR space_id = ?").get(guildID, spaceID)
if (existing) throw createError({status: 400, message: "Bad Request", data: `Guild ID ${guildID} or space ID ${spaceID} are already bridged and cannot be reused`})
- // Check space ID is a valid invite target
- const inviteRow = guildRoute.getInviteTargetSpaces(session.data.mxid).find(s => s.room_id === spaceID)
- if (!inviteRow) throw createError({status: 403, message: "Forbidden", data: "You personally must invite OOYE to that space on Matrix"})
-
- const inviteServer = inviteRow.mxid.match(/:(.*)/)?.[1]
- assert(inviteServer)
- const via = [inviteServer]
+ const inviteSender = select("invite", "mxid", {mxid: session.data.mxid, room_id: spaceID}).pluck().get()
+ const inviteSenderServer = inviteSender?.match(/:(.*)/)?.[1]
+ const via = [inviteSenderServer || ""]
// Check space exists and bridge is joined
try {
@@ -130,11 +87,18 @@ as.router.post("/api/link-space", defineEventHandler(async event => {
}
// Check bridge has PL 100
- const {powerLevels, powers: {[utils.bot]: selfPowerLevel, [session.data.mxid]: invitingPowerLevel}} = await utils.getEffectivePower(spaceID, [utils.bot, session.data.mxid], api)
- if (selfPowerLevel < (powerLevels?.state_default ?? 50) || selfPowerLevel < 100) throw createError({status: 400, message: "Bad Request", data: "OOYE needs power level 100 (admin) in the target Matrix space"})
+ const me = `@${reg.sender_localpart}:${reg.ooye.server_name}`
+ /** @type {Ty.Event.M_Power_Levels?} */
+ let powerLevelsStateContent = null
+ try {
+ powerLevelsStateContent = await api.getStateEvent(spaceID, "m.room.power_levels", "")
+ } catch (e) {}
+ const selfPowerLevel = powerLevelsStateContent?.users?.[me] ?? powerLevelsStateContent?.users_default ?? 0
+ if (selfPowerLevel < (powerLevelsStateContent?.state_default ?? 50) || selfPowerLevel < 100) throw createError({status: 400, message: "Bad Request", data: "OOYE needs power level 100 (admin) in the target Matrix space"})
// Check inviting user is a moderator in the space
- if (invitingPowerLevel < (powerLevels?.state_default ?? 50)) throw createError({status: 403, message: "Forbidden", data: `You need to be at least power level 50 (moderator) in the target Matrix space to set up OOYE, but you are currently power level ${invitingPowerLevel}.`})
+ const invitingPowerLevel = powerLevelsStateContent?.users?.[session.data.mxid] ?? powerLevelsStateContent?.users_default ?? 0
+ if (invitingPowerLevel < (powerLevelsStateContent?.state_default ?? 50)) throw createError({status: 403, message: "Forbidden", data: `You need to be at least power level 50 (moderator) in the target Matrix space to set up OOYE, but you are currently power level ${invitingPowerLevel}.`})
// Insert database entry
db.transaction(() => {
@@ -204,24 +168,21 @@ as.router.post("/api/link", defineEventHandler(async event => {
throw createError({status: 403, message: e.errcode, data: `${e.errcode} - ${e.message}`})
}
- // Check room is not encrypted
- const encryption = await api.getStateEvent(parsedBody.matrix, "m.room.encryption", "").catch(() => null)
- if (encryption) {
- throw createError({status: 400, message: "Bad Request", data: "Encrypted rooms are not supported for bridging. Please replace it with an unencrypted room."})
- }
-
// Check bridge has PL 100
- const {powerLevels, powers: {[utils.bot]: selfPowerLevel}} = await utils.getEffectivePower(parsedBody.matrix, [utils.bot], api)
- if (selfPowerLevel < (powerLevels?.state_default ?? 50) || selfPowerLevel < 100) throw createError({status: 400, message: "Bad Request", data: "OOYE needs power level 100 (admin) in the target Matrix room"})
+ const me = `@${reg.sender_localpart}:${reg.ooye.server_name}`
+ /** @type {Ty.Event.M_Power_Levels?} */
+ let powerLevelsStateContent = null
+ try {
+ powerLevelsStateContent = await api.getStateEvent(parsedBody.matrix, "m.room.power_levels", "")
+ } catch (e) {}
+ const selfPowerLevel = powerLevelsStateContent?.users?.[me] ?? powerLevelsStateContent?.users_default ?? 0
+ if (selfPowerLevel < (powerLevelsStateContent?.state_default ?? 50) || selfPowerLevel < 100) throw createError({status: 400, message: "Bad Request", data: "OOYE needs power level 100 (admin) in the target Matrix room"})
// Insert database entry, but keep the room's existing properties if they are set
const nick = await api.getStateEvent(parsedBody.matrix, "m.room.name", "").then(content => content.name || null).catch(() => null)
const avatar = await api.getStateEvent(parsedBody.matrix, "m.room.avatar", "").then(content => content.url || null).catch(() => null)
const topic = await api.getStateEvent(parsedBody.matrix, "m.room.topic", "").then(content => content.topic || null).catch(() => null)
- db.transaction(() => {
- db.prepare("INSERT INTO channel_room (channel_id, room_id, name, guild_id, nick, custom_avatar, custom_topic) VALUES (?, ?, ?, ?, ?, ?, ?)").run(channel.id, parsedBody.matrix, channel.name, guildID, nick, avatar, topic)
- db.prepare("INSERT INTO historical_channel_room (reference_channel_id, room_id, upgraded_timestamp) VALUES (?, ?, 0)").run(channel.id, parsedBody.matrix)
- })()
+ db.prepare("INSERT INTO channel_room (channel_id, room_id, name, guild_id, nick, custom_avatar, custom_topic) VALUES (?, ?, ?, ?, ?, ?, ?)").run(channel.id, parsedBody.matrix, channel.name, guildID, nick, avatar, topic)
// Sync room data and space child
await createRoom.syncRoom(parsedBody.discord)
@@ -241,6 +202,7 @@ as.router.post("/api/link", defineEventHandler(async event => {
as.router.post("/api/unlink", defineEventHandler(async event => {
const {channel_id, guild_id} = await readValidatedBody(event, schema.unlink.parse)
const managed = await auth.getManagedGuilds(event)
+ const createRoom = getCreateRoom(event)
// Check guild ID or nonce
if (!managed.has(guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"})
@@ -249,56 +211,24 @@ as.router.post("/api/unlink", defineEventHandler(async event => {
const guild = discord.guilds.get(guild_id)
if (!guild) throw createError({status: 400, message: "Bad Request", data: "Discord guild does not exist or bot has not joined it"})
- await validateAndUnbridgeChannel(event, channel_id, guild_id)
+ // Check that the channel (if it exists) is part of this guild
+ /** @type {any} */
+ let channel = discord.channels.get(channel_id)
+ if (channel) {
+ if (!("guild_id" in channel) || channel.guild_id !== guild_id) throw createError({status: 400, message: "Bad Request", data: `Channel ID ${channel_id} is not part of guild ${guild_id}`})
+ } else {
+ // Otherwise, if the channel isn't cached, it must have been deleted.
+ // There's no other authentication here - it's okay for anyone to unlink a deleted channel just by knowing its ID.
+ channel = {id: channel_id}
+ }
+
+ // Check channel is currently bridged
+ const row = select("channel_room", "channel_id", {channel_id: channel_id}).get()
+ if (!row) throw createError({status: 400, message: "Bad Request", data: `Channel ID ${channel_id} is not currently bridged`})
+
+ // Do it
+ await createRoom.unbridgeDeletedChannel(channel, guild_id)
setResponseHeader(event, "HX-Refresh", "true")
return null // 204
}))
-
-as.router.post("/api/unlink-space", defineEventHandler(async event => {
- const {guild_id} = await readValidatedBody(event, schema.unlinkSpace.parse)
- const managed = await auth.getManagedGuilds(event)
- const api = getAPI(event)
- const snow = getSnow(event)
-
- // Check guild ID or nonce
- if (!managed.has(guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"})
-
- // Check guild exists
- const guild = discord.guilds.get(guild_id)
- if (!guild) throw createError({status: 400, message: "Bad Request", data: "Discord guild does not exist or bot has not joined it"})
-
- const active = select("guild_active", "guild_id", {guild_id: guild_id}).get()
- if (!active) {
- throw createError({status: 400, message: "Bad Request", data: "Discord guild has not been considered for bridging"})
- }
-
- // Check if there are Matrix resources
- const spaceID = select("guild_space", "space_id", {guild_id: guild_id}).pluck().get()
- if (spaceID) {
- // Unlink all rooms
- const linkedChannels = select("channel_room", ["channel_id", "room_id", "name", "nick"], {guild_id: guild_id}).all()
- for (const channel of linkedChannels) {
- await validateAndUnbridgeChannel(event, channel.channel_id, guild_id)
- }
-
- // Verify all rooms were unlinked
- const remainingLinkedChannels = select("channel_room", ["channel_id", "room_id", "name", "nick"], {guild_id: guild_id}).all()
- if (remainingLinkedChannels.length) {
- throw createError({status: 500, message: "Internal Server Error", data: "Failed to unlink some rooms. Please try doing it manually, or report a bug. The space will not be unlinked until all rooms are."})
- }
-
- // Unlink space
- await utils.setUserPower(spaceID, utils.bot, 0, api)
- await api.leaveRoom(spaceID)
- db.prepare("DELETE FROM guild_space WHERE guild_id = ? AND space_id = ?").run(guild_id, spaceID)
- db.prepare("DELETE FROM invite WHERE room_id = ?").run(spaceID)
- }
-
- // Mark as not considered for bridging
- db.prepare("DELETE FROM guild_active WHERE guild_id = ?").run(guild_id)
- await snow.user.leaveGuild(guild_id)
-
- setResponseHeader(event, "HX-Redirect", "/")
- return null
-}))
diff --git a/src/web/routes/link.test.js b/src/web/routes/link.test.js
index 0182093..ffe4e5e 100644
--- a/src/web/routes/link.test.js
+++ b/src/web/routes/link.test.js
@@ -1,6 +1,6 @@
// @ts-check
-const {tryToCatch} = require("try-to-catch")
+const tryToCatch = require("try-to-catch")
const {router, test} = require("../../../test/web")
const {MatrixServerError} = require("../../matrix/mreq")
const {select, db} = require("../../passthrough")
@@ -81,6 +81,63 @@ test("web link space: check that OOYE is joined", async t => {
t.equal(called, 1)
})
+test("web link space: check that OOYE has PL 100 (not missing)", async t => {
+ let called = 0
+ const [error] = await tryToCatch(() => router.test("post", "/api/link-space", {
+ sessionData: {
+ managedGuilds: ["665289423482519565"],
+ mxid: "@cadence:cadence.moe"
+ },
+ body: {
+ space_id: "!zTMspHVUBhFLLSdmnS:cadence.moe",
+ guild_id: "665289423482519565"
+ },
+ api: {
+ async joinRoom(roomID) {
+ called++
+ return roomID
+ },
+ async getStateEvent(roomID, type, key) {
+ called++
+ t.equal(roomID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
+ t.equal(type, "m.room.power_levels")
+ throw new MatrixServerError({errcode: "M_NOT_FOUND", error: "what if I told you that power levels never existed"})
+ }
+ }
+ }))
+ t.equal(error.data, "OOYE needs power level 100 (admin) in the target Matrix space")
+ t.equal(called, 2)
+})
+
+test("web link space: check that OOYE has PL 100 (not users_default)", async t => {
+ let called = 0
+ const [error] = await tryToCatch(() => router.test("post", "/api/link-space", {
+ sessionData: {
+ managedGuilds: ["665289423482519565"],
+ mxid: "@cadence:cadence.moe"
+ },
+ body: {
+ space_id: "!zTMspHVUBhFLLSdmnS:cadence.moe",
+ guild_id: "665289423482519565"
+ },
+ api: {
+ async joinRoom(roomID) {
+ called++
+ return roomID
+ },
+ async getStateEvent(roomID, type, key) {
+ called++
+ t.equal(roomID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
+ t.equal(type, "m.room.power_levels")
+ t.equal(key, "")
+ return {}
+ }
+ }
+ }))
+ t.equal(error.data, "OOYE needs power level 100 (admin) in the target Matrix space")
+ t.equal(called, 2)
+})
+
test("web link space: check that OOYE has PL 100 (not 50)", async t => {
let called = 0
const [error] = await tryToCatch(() => router.test("post", "/api/link-space", {
@@ -103,28 +160,11 @@ test("web link space: check that OOYE has PL 100 (not 50)", async t => {
t.equal(type, "m.room.power_levels")
t.equal(key, "")
return {users: {"@_ooye_bot:cadence.moe": 50}}
- },
- async getStateEventOuter(roomID, type, key) {
- called++
- t.equal(roomID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
- t.equal(type, "m.room.create")
- t.equal(key, "")
- return {
- type: "m.room.create",
- state_key: "",
- sender: "@creator:cadence.moe",
- room_id: "!zTMspHVUBhFLLSdmnS:cadence.moe",
- event_id: "$create",
- origin_server_ts: 0,
- content: {
- room_version: "11"
- }
- }
}
}
}))
t.equal(error.data, "OOYE needs power level 100 (admin) in the target Matrix space")
- t.equal(called, 3)
+ t.equal(called, 2)
})
test("web link space: check that inviting user has PL 50", async t => {
@@ -148,29 +188,12 @@ test("web link space: check that inviting user has PL 50", async t => {
t.equal(roomID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
t.equal(type, "m.room.power_levels")
t.equal(key, "")
- return {users: {"@_ooye_bot:cadence.moe": 100}, events: {"m.room.tombstone": 150}}
- },
- async getStateEventOuter(roomID, type, key) {
- called++
- t.equal(roomID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
- t.equal(type, "m.room.create")
- t.equal(key, "")
- return {
- type: "m.room.create",
- state_key: "",
- sender: "@creator:cadence.moe",
- room_id: "!zTMspHVUBhFLLSdmnS:cadence.moe",
- event_id: "$create",
- origin_server_ts: 0,
- content: {
- room_version: "12"
- }
- }
+ return {users: {"@_ooye_bot:cadence.moe": 100}}
}
}
}))
t.equal(error.data, "You need to be at least power level 50 (moderator) in the target Matrix space to set up OOYE, but you are currently power level 0.")
- t.equal(called, 3)
+ t.equal(called, 2)
})
test("web link space: successfully adds entry to database and loads page", async t => {
@@ -194,28 +217,11 @@ test("web link space: successfully adds entry to database and loads page", async
t.equal(roomID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
t.equal(type, "m.room.power_levels")
t.equal(key, "")
- return {users: {"@cadence:cadence.moe": 50}}
- },
- async getStateEventOuter(roomID, type, key) {
- called++
- t.equal(roomID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
- t.equal(type, "m.room.create")
- t.equal(key, "")
- return {
- type: "m.room.create",
- state_key: "",
- sender: "@_ooye_bot:cadence.moe",
- room_id: "!zTMspHVUBhFLLSdmnS:cadence.moe",
- event_id: "$create",
- origin_server_ts: 0,
- content: {
- room_version: "12"
- }
- }
+ return {users: {"@_ooye_bot:cadence.moe": 100, "@cadence:cadence.moe": 50}}
}
}
})
- t.equal(called, 3)
+ t.equal(called, 2)
// check that the entry was added to the database
t.equal(select("guild_space", "privacy_level", {guild_id: "665289423482519565", space_id: "!zTMspHVUBhFLLSdmnS:cadence.moe"}).pluck().get(), 0)
@@ -435,48 +441,7 @@ test("web link room: check that bridge can join room (uses via for join attempt)
t.equal(called, 2)
})
-test("web link room: check that room is not encrypted", async t => {
-let called = 0
- const [error] = await tryToCatch(() => router.test("post", "/api/link", {
- sessionData: {
- managedGuilds: ["665289423482519565"]
- },
- body: {
- discord: "665310973967597573",
- matrix: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
- guild_id: "665289423482519565"
- },
- api: {
- async joinRoom(roomID) {
- called++
- return roomID
- },
- async *generateFullHierarchy(spaceID) {
- called++
- t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
- yield {
- room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
- children_state: [],
- guest_can_join: false,
- num_joined_members: 2
- }
- /* c8 ignore next */
- },
- async getStateEvent(roomID, type, key) {
- called++
- t.equal(roomID, "!NDbIqNpJyPvfKRnNcr:cadence.moe")
- if (type === "m.room.encryption" && key === "") {
- return {algorithm: "m.megolm.v1.aes-sha2"}
- }
- throw new Error("Unknown state event")
- }
- }
- }))
- t.equal(error.data, "Encrypted rooms are not supported for bridging. Please replace it with an unencrypted room.")
- t.equal(called, 3)
-})
-
-test("web link room: check that bridge has PL 100 in target room", async t => {
+test("web link room: check that bridge has PL 100 in target room (event missing)", async t => {
let called = 0
const [error] = await tryToCatch(() => router.test("post", "/api/link", {
sessionData: {
@@ -506,32 +471,54 @@ test("web link room: check that bridge has PL 100 in target room", async t => {
async getStateEvent(roomID, type, key) {
called++
t.equal(roomID, "!NDbIqNpJyPvfKRnNcr:cadence.moe")
- if (type === "m.room.power_levels" && key === "") {
- return {users_default: 50}
- }
- throw new Error("Unknown state event")
- },
- async getStateEventOuter(roomID, type, key) {
- called++
- t.equal(roomID, "!NDbIqNpJyPvfKRnNcr:cadence.moe")
- t.equal(type, "m.room.create")
+ t.equal(type, "m.room.power_levels")
t.equal(key, "")
- return {
- type: "m.room.create",
- state_key: "",
- sender: "@creator:cadence.moe",
- room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
- event_id: "$create",
- origin_server_ts: 0,
- content: {
- room_version: "11"
- }
- }
+ throw new MatrixServerError({errcode: "M_NOT_FOUND", error: "what if I told you there's no such thing as power levels"})
}
}
}))
t.equal(error.data, "OOYE needs power level 100 (admin) in the target Matrix room")
- t.equal(called, 5)
+ t.equal(called, 3)
+})
+
+test("web link room: check that bridge has PL 100 in target room (users default)", async t => {
+ let called = 0
+ const [error] = await tryToCatch(() => router.test("post", "/api/link", {
+ sessionData: {
+ managedGuilds: ["665289423482519565"]
+ },
+ body: {
+ discord: "665310973967597573",
+ matrix: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
+ guild_id: "665289423482519565"
+ },
+ api: {
+ async joinRoom(roomID) {
+ called++
+ return roomID
+ },
+ async *generateFullHierarchy(spaceID) {
+ called++
+ t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
+ yield {
+ room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
+ children_state: [],
+ guest_can_join: false,
+ num_joined_members: 2
+ }
+ /* c8 ignore next */
+ },
+ async getStateEvent(roomID, type, key) {
+ called++
+ t.equal(roomID, "!NDbIqNpJyPvfKRnNcr:cadence.moe")
+ t.equal(type, "m.room.power_levels")
+ t.equal(key, "")
+ return {users_default: 50}
+ }
+ }
+ }))
+ t.equal(error.data, "OOYE needs power level 100 (admin) in the target Matrix room")
+ t.equal(called, 3)
})
test("web link room: successfully calls createRoom", async t => {
@@ -581,23 +568,6 @@ test("web link room: successfully calls createRoom", async t => {
return {}
}
},
- async getStateEventOuter(roomID, type, key) {
- called++
- t.equal(roomID, "!NDbIqNpJyPvfKRnNcr:cadence.moe")
- t.equal(type, "m.room.create")
- t.equal(key, "")
- return {
- type: "m.room.create",
- state_key: "",
- sender: "@creator:cadence.moe",
- room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
- event_id: "$create",
- origin_server_ts: 0,
- content: {
- room_version: "11"
- }
- }
- },
async sendEvent(roomID, type, content) {
called++
t.equal(roomID, "!NDbIqNpJyPvfKRnNcr:cadence.moe")
@@ -614,7 +584,7 @@ test("web link room: successfully calls createRoom", async t => {
}
}
})
- t.equal(called, 9)
+ t.equal(called, 8)
})
// *****
@@ -655,7 +625,7 @@ test("web unlink room: checks that the channel is part of the guild", async t =>
t.equal(error.data, "Channel ID 112760669178241024 is not part of guild 665289423482519565")
})
-test("web unlink room: successfully calls unbridgeChannel when the channel does exist", async t => {
+test("web unlink room: successfully calls unbridgeDeletedChannel when the channel does exist", async t => {
let called = 0
await router.test("post", "/api/unlink", {
sessionData: {
@@ -666,7 +636,7 @@ test("web unlink room: successfully calls unbridgeChannel when the channel does
guild_id: "665289423482519565"
},
createRoom: {
- async unbridgeChannel(channel) {
+ async unbridgeDeletedChannel(channel) {
called++
t.equal(channel.id, "665310973967597573")
}
@@ -675,7 +645,7 @@ test("web unlink room: successfully calls unbridgeChannel when the channel does
t.equal(called, 1)
})
-test("web unlink room: successfully calls unbridgeChannel when the channel does not exist", async t => {
+test("web unlink room: successfully calls unbridgeDeletedChannel when the channel does not exist", async t => {
let called = 0
await router.test("post", "/api/unlink", {
sessionData: {
@@ -686,7 +656,7 @@ test("web unlink room: successfully calls unbridgeChannel when the channel does
guild_id: "112760669178241024"
},
createRoom: {
- async unbridgeChannel(channel) {
+ async unbridgeDeletedChannel(channel) {
called++
t.equal(channel.id, "489237891895768942")
}
@@ -696,9 +666,7 @@ test("web unlink room: successfully calls unbridgeChannel when the channel does
})
test("web unlink room: checks that the channel is bridged", async t => {
- const row = db.prepare("SELECT * FROM channel_room WHERE channel_id = '665310973967597573'").get()
db.prepare("DELETE FROM channel_room WHERE channel_id = '665310973967597573'").run()
-
const [error] = await tryToCatch(() => router.test("post", "/api/unlink", {
sessionData: {
managedGuilds: ["665289423482519565"]
@@ -709,179 +677,4 @@ test("web unlink room: checks that the channel is bridged", async t => {
}
}))
t.equal(error.data, "Channel ID 665310973967597573 is not currently bridged")
-
- db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar, last_bridged_pin_timestamp, speedbump_id, speedbump_checked, speedbump_webhook_id, guild_id, custom_topic) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(row.channel_id, row.room_id, row.name, row.nick, row.thread_parent, row.custom_avatar, row.last_bridged_pin_timestamp, row.speedbump_id, row.speedbump_checked, row.speedbump_webhook_id, row.guild_id, row.custom_topic)
- const new_row = db.prepare("SELECT * FROM channel_room WHERE channel_id = '665310973967597573'").get()
- t.deepEqual(row, new_row)
-})
-
-// *****
-
-test("web unlink space: access denied if not logged in to Discord", async t => {
- const [error] = await tryToCatch(() => router.test("post", "/api/unlink-space", {
- body: {
- guild_id: "665289423482519565"
- }
- }))
- t.equal(error.data, "Can't edit a guild you don't have Manage Server permissions in")
-})
-
-test("web unlink space: checks that guild exists", async t => {
- const [error] = await tryToCatch(() => router.test("post", "/api/unlink-space", {
- sessionData: {
- managedGuilds: ["2"]
- },
- body: {
- guild_id: "2"
- }
- }))
- t.equal(error.data, "Discord guild does not exist or bot has not joined it")
-})
-
-test("web unlink space: checks that a space is linked to the guild before trying to unlink the space", async t => {
- db.exec("BEGIN TRANSACTION")
- db.prepare("DELETE FROM guild_active WHERE guild_id = '665289423482519565'").run()
-
- const [error] = await tryToCatch(() => router.test("post", "/api/unlink-space", {
- sessionData: {
- managedGuilds: ["665289423482519565"]
- },
- body: {
- guild_id: "665289423482519565"
- }
- }))
- t.equal(error.data, "Discord guild has not been considered for bridging")
-
- db.exec("ROLLBACK") // ぬ
-})
-
-test("web unlink space: correctly abort unlinking if some linked channels remain after trying to unlink them all", async t => {
- let unbridgedChannel = false
-
- const [error] = await tryToCatch(() => router.test("post", "/api/unlink-space", {
- sessionData: {
- managedGuilds: ["665289423482519565"]
- },
- body: {
- guild_id: "665289423482519565",
- },
- createRoom: {
- async unbridgeChannel(channel, guildID) {
- unbridgedChannel = true
- t.ok(["1438284564815548418", "665310973967597573"].includes(channel.id))
- t.equal(guildID, "665289423482519565")
- // Do not actually delete the link from DB, should trigger error later in check
- }
- },
- api: {
- async *generateFullHierarchy(spaceID) {
- t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
- yield {
- room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
- children_state: [],
- guest_can_join: false,
- num_joined_members: 2
- }
- /* c8 ignore next */
- },
- }
- }))
-
- t.equal(error.data, "Failed to unlink some rooms. Please try doing it manually, or report a bug. The space will not be unlinked until all rooms are.")
- t.equal(unbridgedChannel, true)
-})
-
-test("web unlink space: successfully calls unbridgeChannel on linked channels in space, self-downgrade power level, leave space, and delete link from DB", async t => {
- const {reg} = require("../../matrix/read-registration")
- const me = `@${reg.sender_localpart}:${reg.ooye.server_name}`
-
- const getLinkRowQuery = "SELECT * FROM guild_space WHERE guild_id = '665289423482519565'"
-
- const row = db.prepare(getLinkRowQuery).get()
- t.equal(row.space_id, "!zTMspHVUBhFLLSdmnS:cadence.moe")
-
- let unbridgedChannel = false
- let downgradedPowerLevel = false
- let leftRoom = false
- await router.test("post", "/api/unlink-space", {
- sessionData: {
- managedGuilds: ["665289423482519565"]
- },
- body: {
- guild_id: "665289423482519565",
- },
- createRoom: {
- async unbridgeChannel(channel, guildID) {
- unbridgedChannel = true
- t.ok(["1438284564815548418", "665310973967597573"].includes(channel.id))
- t.equal(guildID, "665289423482519565")
-
- // In order to not simulate channel deletion and not trigger the post unlink channels, pre-unlink space check
- db.prepare("DELETE FROM channel_room WHERE channel_id = ?").run(channel.id)
- }
- },
- snow: {
- user: {
- // @ts-ignore - snowtransfer or discord-api-types broken, 204 No Content should be mapped to void but is actually mapped to never
- async leaveGuild(guildID) {
- t.equal(guildID, "665289423482519565")
- }
- }
- },
- api: {
- async *generateFullHierarchy(spaceID) {
- t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
- yield {
- room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
- children_state: [],
- guest_can_join: false,
- num_joined_members: 2
- }
- /* c8 ignore next */
- },
-
- async getStateEvent(roomID, type, key) { // getting power levels from space to apply to room
- t.equal(type, "m.room.power_levels")
- t.equal(key, "")
- return {users: {"@_ooye_bot:cadence.moe": 100, "@example:matrix.org": 50}, events: {"m.room.tombstone": 100}}
- },
-
- async getStateEventOuter(roomID, type, key) {
- t.equal(roomID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
- t.equal(type, "m.room.create")
- t.equal(key, "")
- return {
- type: "m.room.create",
- state_key: "",
- sender: "@_ooye_bot:cadence.moe",
- room_id: "!zTMspHVUBhFLLSdmnS:cadence.moe",
- event_id: "$create",
- origin_server_ts: 0,
- content: {
- room_version: "11"
- }
- }
- },
-
- async sendState(roomID, type, key, content) {
- downgradedPowerLevel = true
- t.equal(roomID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
- t.equal(type, "m.room.power_levels")
- t.notOk(me in content.users, `got ${JSON.stringify(content)} but expected bot user to not be present`)
- return ""
- },
-
- async leaveRoom(spaceID) {
- leftRoom = true
- t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
- },
- }
- })
-
- t.equal(unbridgedChannel, true)
- t.equal(downgradedPowerLevel, true)
- t.equal(leftRoom, true)
-
- const missed_row = db.prepare(getLinkRowQuery).get()
- t.equal(missed_row, undefined)
})
diff --git a/src/web/routes/log-in-with-matrix.js b/src/web/routes/log-in-with-matrix.js
index d36d8fa..574c312 100644
--- a/src/web/routes/log-in-with-matrix.js
+++ b/src/web/routes/log-in-with-matrix.js
@@ -79,7 +79,30 @@ as.router.post("/api/log-in-with-matrix", defineEventHandler(async event => {
}
}
- const roomID = await api.usePrivateChat(mxid)
+ // Check if we have an existing DM
+ let roomID = select("direct", "room_id", {mxid}).pluck().get()
+ if (roomID) {
+ // Check that the person is/still in the room
+ try {
+ var member = await api.getStateEvent(roomID, "m.room.member", mxid)
+ } catch (e) {}
+
+ // Invite them back to the room if needed
+ if (!member || member.membership === "leave") {
+ await api.inviteToRoom(roomID, mxid)
+ }
+ }
+
+ // No existing DM, create a new room and invite
+ else {
+ roomID = await api.createRoom({
+ invite: [mxid],
+ is_direct: true,
+ preset: "trusted_private_chat"
+ })
+ // Store the newly created room in account data (Matrix doesn't do this for us automatically, sigh...)
+ db.prepare("REPLACE INTO direct (mxid, room_id) VALUES (?, ?)").run(mxid, roomID)
+ }
const token = randomUUID()
diff --git a/src/web/routes/log-in-with-matrix.test.js b/src/web/routes/log-in-with-matrix.test.js
index 2f9afcc..bc9c7e0 100644
--- a/src/web/routes/log-in-with-matrix.test.js
+++ b/src/web/routes/log-in-with-matrix.test.js
@@ -1,6 +1,6 @@
// @ts-check
-const {tryToCatch} = require("try-to-catch")
+const tryToCatch = require("try-to-catch")
const {router, test} = require("../../../test/web")
const {MatrixServerError} = require("../../matrix/mreq")
@@ -34,25 +34,23 @@ test("log in with matrix: checks if mxid domain format looks valid", async t =>
t.match(error.data.fieldErrors.mxid, /must match pattern/)
})
-test("log in with matrix: sends message to log in", async t => {
+test("log in with matrix: sends message when there is no existing dm room", async t => {
const event = {}
let called = 0
await router.test("post", "/api/log-in-with-matrix", {
body: {
- mxid: "@cadence:cadence.moe",
- next: "https://bridge.cadence.moe/guild?guild_id=123"
+ mxid: "@cadence:cadence.moe"
},
api: {
- async usePrivateChat(mxid) {
+ async createRoom() {
called++
- t.equal(mxid, "@cadence:cadence.moe")
return "!created:cadence.moe"
},
async sendEvent(roomID, type, content) {
called++
t.equal(roomID, "!created:cadence.moe")
t.equal(type, "m.room.message")
- token = content.body.match(/log-in-with-matrix\?token=([a-f0-9-]+)&next=/)[1]
+ token = content.body.match(/log-in-with-matrix\?token=([a-f0-9-]+)/)[1]
t.ok(token, "log in token not issued")
return ""
}
@@ -74,6 +72,65 @@ test("log in with matrix: does not send another message when a log in is in prog
t.match(event.node.res.getHeader("location"), /We already sent you a link on Matrix/)
})
+test("log in with matrix: reuses room from direct", async t => {
+ const event = {}
+ let called = 0
+ await router.test("post", "/api/log-in-with-matrix", {
+ body: {
+ mxid: "@user1:example.org"
+ },
+ api: {
+ async getStateEvent(roomID, type, key) {
+ called++
+ t.equal(roomID, "!existing:cadence.moe")
+ t.equal(type, "m.room.member")
+ t.equal(key, "@user1:example.org")
+ return {membership: "join"}
+ },
+ async sendEvent(roomID) {
+ called++
+ t.equal(roomID, "!existing:cadence.moe")
+ return ""
+ }
+ },
+ event
+ })
+ t.match(event.node.res.getHeader("location"), /Please check your inbox on Matrix/)
+ t.equal(called, 2)
+})
+
+test("log in with matrix: reuses room from direct, reinviting if user has left", async t => {
+ const event = {}
+ let called = 0
+ await router.test("post", "/api/log-in-with-matrix", {
+ body: {
+ mxid: "@user2:example.org"
+ },
+ api: {
+ async getStateEvent(roomID, type, key) {
+ called++
+ t.equal(roomID, "!existing:cadence.moe")
+ t.equal(type, "m.room.member")
+ t.equal(key, "@user2:example.org")
+ throw new MatrixServerError({errcode: "M_NOT_FOUND"})
+ },
+ async inviteToRoom(roomID, mxid) {
+ called++
+ t.equal(roomID, "!existing:cadence.moe")
+ t.equal(mxid, "@user2:example.org")
+ },
+ async sendEvent(roomID) {
+ called++
+ t.equal(roomID, "!existing:cadence.moe")
+ return ""
+ }
+ },
+ event
+ })
+ t.match(event.node.res.getHeader("location"), /Please check your inbox on Matrix/)
+ t.equal(called, 3)
+})
+
// ***** third request *****
diff --git a/src/web/routes/oauth.js b/src/web/routes/oauth.js
index f4bb61f..80765d6 100644
--- a/src/web/routes/oauth.js
+++ b/src/web/routes/oauth.js
@@ -2,13 +2,13 @@
const {z} = require("zod")
const {randomUUID} = require("crypto")
-const {defineEventHandler, getValidatedQuery, sendRedirect, createError, H3Event} = require("h3")
+const {defineEventHandler, getValidatedQuery, sendRedirect, createError} = require("h3")
const {SnowTransfer, tokenless} = require("snowtransfer")
const DiscordTypes = require("discord-api-types/v10")
const getRelativePath = require("get-relative-path")
-const {as, db, sync} = require("../../passthrough")
-const {id, permissions} = require("../../../addbot")
+const {discord, as, db, sync} = require("../../passthrough")
+const {id} = require("../../../addbot")
/** @type {import("../auth")} */
const auth = sync.require("../auth")
const {reg} = require("../../matrix/read-registration")
@@ -33,24 +33,6 @@ const schema = {
})
}
-/**
- * @param {H3Event} event
- * @returns {(string) => {user: {getGuilds: () => Promise}}}
- */
-function getClient(event) {
- /* c8 ignore next */
- return event.context.getClient || (accessToken => new SnowTransfer(`Bearer ${accessToken}`))
-}
-
-/**
- * @param {H3Event} event
- * @returns {typeof tokenless.getOauth2Token}
- */
-function getOauth2Token(event) {
- /* c8 ignore next */
- return event.context.getOauth2Token || tokenless.getOauth2Token
-}
-
as.router.get("/oauth", defineEventHandler(async event => {
const session = await auth.useSession(event)
let scope = "guilds"
@@ -69,7 +51,7 @@ as.router.get("/oauth", defineEventHandler(async event => {
async function tryAgain() {
const newState = randomUUID()
await session.update({state: newState})
- return sendRedirect(event, `https://discord.com/oauth2/authorize?client_id=${id}&scope=${scope}&permissions=${permissions}&response_type=code&redirect_uri=${redirect_uri}&state=${newState}`)
+ return sendRedirect(event, `https://discord.com/oauth2/authorize?client_id=${id}&scope=${scope}&permissions=1610883072&response_type=code&redirect_uri=${redirect_uri}&state=${newState}`)
}
const parsedQuery = await getValidatedQuery(event, schema.code.safeParse)
@@ -79,15 +61,21 @@ as.router.get("/oauth", defineEventHandler(async event => {
if (!savedState) throw createError({status: 400, message: "Missing state", data: "Missing saved state parameter. Please try again, and make sure you have cookies enabled."})
if (savedState != parsedQuery.data.state) return tryAgain()
- const oauthResult = await getOauth2Token(event)(id, redirect_uri, reg.ooye.discord_client_secret, parsedQuery.data.code)
- const parsedToken = schema.token.parse(oauthResult)
+ const oauthResult = await tokenless.getOauth2Token(id, redirect_uri, reg.ooye.discord_client_secret, parsedQuery.data.code)
+ const parsedToken = schema.token.safeParse(oauthResult)
+ if (!parsedToken.success) {
+ throw createError({status: 502, message: "Invalid token response", data: `Discord completed OAuth, but returned this instead of an OAuth access token: ${JSON.stringify(oauthResult)}`})
+ }
- const userID = Buffer.from(parsedToken.access_token.split(".")[0], "base64").toString()
- const client = getClient(event)(parsedToken.access_token)
-
- const guilds = await client.user.getGuilds()
- var managedGuilds = guilds.filter(g => BigInt(g.permissions) & DiscordTypes.PermissionFlagsBits.ManageGuild).map(g => g.id)
- await session.update({managedGuilds, userID, state: undefined})
+ const userID = Buffer.from(parsedToken.data.access_token.split(".")[0], "base64").toString()
+ const client = new SnowTransfer(`Bearer ${parsedToken.data.access_token}`)
+ try {
+ const guilds = await client.user.getGuilds()
+ var managedGuilds = guilds.filter(g => BigInt(g.permissions) & DiscordTypes.PermissionFlagsBits.ManageGuild).map(g => g.id)
+ await session.update({managedGuilds, userID, state: undefined})
+ } catch (e) {
+ throw createError({status: 502, message: "API call failed", data: e.message})
+ }
// Set auto-create for the guild
// @ts-ignore
diff --git a/src/web/routes/oauth.test.js b/src/web/routes/oauth.test.js
deleted file mode 100644
index 1a06e39..0000000
--- a/src/web/routes/oauth.test.js
+++ /dev/null
@@ -1,121 +0,0 @@
-// @ts-check
-
-const DiscordTypes = require("discord-api-types/v10")
-const {tryToCatch} = require("try-to-catch")
-const assert = require("assert/strict")
-const {router, test} = require("../../../test/web")
-
-test("web oauth: redirects to Discord on first visit (add easy)", async t => {
- let event = {}
- await router.test("get", "/oauth?action=add", {
- event,
- sessionData: {
- password: "password123"
- }
- })
- t.equal(event.node.res.statusCode, 302)
- t.match(event.node.res.getHeader("location"), /^https:\/\/discord.com\/oauth2\/authorize\?client_id=684280192553844747&scope=bot\+guilds&permissions=2251801424568320&response_type=code&redirect_uri=https:\/\/bridge\.example\.org\/oauth&state=/)
-})
-
-test("web oauth: redirects to Discord on first visit (add self service)", async t => {
- let event = {}
- await router.test("get", "/oauth?action=add-self-service", {
- event,
- sessionData: {
- password: "password123"
- }
- })
- t.equal(event.node.res.statusCode, 302)
- t.match(event.node.res.getHeader("location"), /^https:\/\/discord.com\/oauth2\/authorize\?client_id=684280192553844747&scope=bot\+guilds&permissions=2251801424568320&response_type=code&redirect_uri=https:\/\/bridge\.example\.org\/oauth&state=/)
-})
-
-test("web oauth: advises user about cookies if state is missing", async t => {
- let event = {}
- const [e] = await tryToCatch(() => router.test("get", "/oauth?state=693551d5-47c5-49e2-a433-3600abe3c15c&code=DISCORD_CODE&guild_id=9", {
- event
- }))
- t.equal(e.message, "Missing state")
-})
-
-test("web oauth: redirects to Discord again if state doesn't match", async t => {
- let event = {}
- await router.test("get", "/oauth?state=693551d5-47c5-49e2-a433-3600abe3c15c&code=DISCORD_CODE", {
- event,
- sessionData: {
- state: "438aa253-1311-4483-9aa2-c251e29e72c9",
- password: "password123"
- }
- })
- t.equal(event.node.res.statusCode, 302)
- t.match(event.node.res.getHeader("location"), /^https:\/\/discord\.com\/oauth2\/authorize/)
-})
-
-test("web oauth: uses returned state, logs in", async t => {
- let event = {}
- await router.test("get", "/oauth?state=693551d5-47c5-49e2-a433-3600abe3c15c&code=DISCORD_CODE", {
- event,
- sessionData: {
- state: "693551d5-47c5-49e2-a433-3600abe3c15c",
- selfService: false,
- password: "password123"
- },
- getOauth2Token() {
- return {
- token_type: "Bearer",
- access_token: "6qrZcUqja7812RVdnEKjpzOL4CvHBFG",
- expires_in: 604800,
- refresh_token: "D43f5y0ahjqew82jZ4NViEr2YafMKhue",
- scope: "bot+guilds"
- }
- },
- getClient(accessToken) {
- return {
- user: {
- async getGuilds() {
- return [{
- id: "9",
- permissions: DiscordTypes.PermissionFlagsBits.ManageGuild
- }]
- }
- }
- }
- }
- })
- t.equal(event.node.res.statusCode, 302)
- t.equal(event.node.res.getHeader("location"), "./")
-})
-
-test("web oauth: uses returned state, adds managed guild", async t => {
- let event = {}
- await router.test("get", "/oauth?state=693551d5-47c5-49e2-a433-3600abe3c15c&code=DISCORD_CODE&guild_id=9", {
- event,
- sessionData: {
- state: "693551d5-47c5-49e2-a433-3600abe3c15c",
- selfService: false,
- password: "password123"
- },
- getOauth2Token() {
- return {
- token_type: "Bearer",
- access_token: "6qrZcUqja7812RVdnEKjpzOL4CvHBFG",
- expires_in: 604800,
- refresh_token: "D43f5y0ahjqew82jZ4NViEr2YafMKhue",
- scope: "bot+guilds"
- }
- },
- getClient(accessToken) {
- return {
- user: {
- async getGuilds() {
- return [{
- id: "9",
- permissions: DiscordTypes.PermissionFlagsBits.ManageGuild
- }]
- }
- }
- }
- }
- })
- t.equal(event.node.res.statusCode, 302)
- t.equal(event.node.res.getHeader("location"), "guild?guild_id=9")
-})
diff --git a/src/web/routes/password.test.js b/src/web/routes/password.test.js
deleted file mode 100644
index fca4e70..0000000
--- a/src/web/routes/password.test.js
+++ /dev/null
@@ -1,16 +0,0 @@
-// @ts-check
-
-const {tryToCatch} = require("try-to-catch")
-const {test} = require("supertape")
-const {router} = require("../../../test/web")
-
-test("web password: stores password", async t => {
- const event = {}
- await router.test("post", "/api/password", {
- body: {
- password: "password123"
- },
- event
- })
- t.equal(event.node.res.statusCode, 302)
-})
diff --git a/src/web/server.js b/src/web/server.js
index 85fa1cb..7c8ed3e 100644
--- a/src/web/server.js
+++ b/src/web/server.js
@@ -4,23 +4,36 @@ const assert = require("assert")
const fs = require("fs")
const {join} = require("path")
const h3 = require("h3")
-const mimeTypes = require("mime-types")
-const {defineEventHandler, defaultContentType, getRequestHeader, setResponseHeader, handleCacheHeaders, serveStatic} = h3
+const {defineEventHandler, defaultContentType, getRequestHeader, setResponseHeader, handleCacheHeaders} = h3
const icons = require("@stackoverflow/stacks-icons")
const DiscordTypes = require("discord-api-types/v10")
const dUtils = require("../discord/utils")
const reg = require("../matrix/read-registration")
-const {sync, discord, as, select, from} = require("../passthrough")
+const {sync, discord, as, select} = require("../passthrough")
/** @type {import("./pug-sync")} */
const pugSync = sync.require("./pug-sync")
-/** @type {import("../matrix/utils")} */
-const mUtils = sync.require("../matrix/utils")
+/** @type {import("../m2d/converters/utils")} */
+const mUtils = sync.require("../m2d/converters/utils")
const {id} = require("../../addbot")
// Pug
-pugSync.addGlobals({id, h3, discord, select, from, DiscordTypes, dUtils, mUtils, icons, reg: reg.reg})
+pugSync.addGlobals({id, h3, discord, select, DiscordTypes, dUtils, mUtils, icons, reg: reg.reg})
+pugSync.createRoute(as.router, "/", "home.pug")
+pugSync.createRoute(as.router, "/ok", "ok.pug")
+
+// Routes
+
+sync.require("./routes/download-matrix")
+sync.require("./routes/download-discord")
+sync.require("./routes/guild-settings")
+sync.require("./routes/guild")
+sync.require("./routes/info")
+sync.require("./routes/link")
+sync.require("./routes/log-in-with-matrix")
+sync.require("./routes/oauth")
+sync.require("./routes/password")
// Files
@@ -52,86 +65,7 @@ as.router.get("/static/htmx.js", defineEventHandler({
}
}))
-as.router.get("/download/file/poll-star-avatar.png", defineEventHandler(event => {
- handleCacheHeaders(event, {maxAge: 86400})
- return fs.promises.readFile(join(__dirname, "../../docs/img/poll-star-avatar.png"))
-}))
-
-// Custom files
-
-const publicDir = "custom-webroot"
-
-/**
- * @param {h3.H3Event} event
- * @param {boolean} fallthrough
- */
-function tryStatic(event, fallthrough) {
- return serveStatic(event, {
- indexNames: ["/index.html", "/index.pug"],
- fallthrough,
- getMeta: async id => {
- // Check
- const stats = await fs.promises.stat(join(publicDir, id)).catch(() => {});
- if (!stats || !stats.isFile()) {
- return
- }
- // Pug
- if (id.match(/\.pug$/)) {
- defaultContentType(event, "text/html; charset=utf-8")
- return {}
- }
- // Everything else
- else {
- const mime = mimeTypes.lookup(id)
- if (typeof mime === "string") {
- if (mime.startsWith("text/")) {
- defaultContentType(event, mime + "; charset=utf-8") // usually wise
- } else {
- defaultContentType(event, mime)
- }
- }
- return {
- size: stats.size
- }
- }
- },
- getContents: id => {
- if (id.match(/\.pug$/)) {
- const path = join(publicDir, id)
- return pugSync.renderPath(event, path, {})
- } else {
- return fs.createReadStream(join(publicDir, id))
- }
- }
- })
-}
-
-as.router.get("/**", defineEventHandler(event => {
- return tryStatic(event, false)
-}))
-
-as.router.get("/", defineEventHandler(async event => {
- return (await tryStatic(event, true)) || pugSync.render(event, "home.pug", {})
-}))
-
-as.router.get("/icon.png", defineEventHandler(async event => {
- const s = await tryStatic(event, true)
- if (s) return s
+as.router.get("/icon.png", defineEventHandler(event => {
handleCacheHeaders(event, {maxAge: 86400})
return fs.promises.readFile(join(__dirname, "../../docs/img/icon.png"))
}))
-
-// Routes
-
-pugSync.createRoute(as.router, "/ok", "ok.pug")
-
-sync.require("./routes/agi")
-sync.require("./routes/download-matrix")
-sync.require("./routes/download-discord")
-sync.require("./routes/guild-settings")
-sync.require("./routes/guild")
-sync.require("./routes/info")
-sync.require("./routes/link")
-sync.require("./routes/log-in-with-matrix")
-sync.require("./routes/oauth")
-sync.require("./routes/password")
diff --git a/start.js b/start.js
index 39e8ea0..ca6212b 100755
--- a/start.js
+++ b/start.js
@@ -36,9 +36,5 @@ sync.require("./src/m2d/event-dispatcher")
sync.require("./src/web/server")
await power.applyPower()
- discord.cloud.once("ready", () => {
- as.listen()
- })
-
require("./src/stdin")
})()
diff --git a/test/addbot.test.js b/test/addbot.test.js
index 4130051..17c6dda 100644
--- a/test/addbot.test.js
+++ b/test/addbot.test.js
@@ -4,5 +4,5 @@ const {addbot} = require("../addbot")
const {test} = require("supertape")
test("addbot: returns message and invite link", t => {
- t.equal(addbot(), `Open this link to add the bot to a Discord server:\nhttps://discord.com/oauth2/authorize?client_id=684280192553844747&scope=bot&permissions=2251801424568320 `)
+ t.equal(addbot(), `Open this link to add the bot to a Discord server:\nhttps://discord.com/oauth2/authorize?client_id=684280192553844747&scope=bot&permissions=1610883072 `)
})
diff --git a/test/data.js b/test/data.js
index cc054cf..e64b9c2 100644
--- a/test/data.js
+++ b/test/data.js
@@ -19,26 +19,6 @@ module.exports = {
default_thread_rate_limit_per_user: 0,
guild_id: "112760669178241024"
},
- voice: {
- voice_background_display: null,
- version: 1774469910848,
- user_limit: 0,
- type: 2,
- theme_color: null,
- status: null,
- rtc_region: null,
- rate_limit_per_user: 0,
- position: 0,
- permission_overwrites: [],
- parent_id: "805261291908104252",
- nsfw: false,
- name: "🍞丨[8user] Piece",
- last_message_id: "1459912691098325137",
- id: "1036840786093953084",
- flags: 0,
- bitrate: 256000,
- guild_id: "112760669178241024"
- },
updates: {
type: 0,
topic: "Updates and release announcements for Out Of Your Element.",
@@ -121,7 +101,6 @@ module.exports = {
},
room: {
general: {
- "m.room.create/": {additional_creators: ["@test_auto_invite:example.org"]},
"m.room.name/": {name: "main"},
"m.room.topic/": {topic: "#collective-unconscious | https://docs.google.com/document/d/blah/edit | I spread, pipe, and whip because it is my will. :headstone:\n\nChannel ID: 112760669178241024\nGuild ID: 112760669178241024"},
"m.room.guest_access/": {guest_access: "can_join"},
@@ -147,12 +126,13 @@ module.exports = {
"m.room.redaction": 0
},
users: {
- "@test_auto_invite:example.org": 150
+ "@test_auto_invite:example.org": 100
},
notifications: {
room: 0
}
},
+ "chat.schildi.hide_ui/read_receipts": {},
"uk.half-shot.bridge/moe.cadence.ooye://discord/112760669178241024/112760669178241024": {
bridgebot: "@_ooye_bot:cadence.moe",
protocol: {
@@ -200,39 +180,6 @@ module.exports = {
afk_timeout: 300,
id: "112760669178241024",
icon: "a_f83622e09ead74f0c5c527fe241f8f8c",
- /** @type {DiscordTypes.APIGuildMember[]} */ // @ts-ignore
- members: [{
- user: {
- username: 'Matrix Bridge',
- public_flags: 0,
- primary_guild: null,
- id: '684280192553844747',
- global_name: null,
- display_name_styles: null,
- display_name: null,
- discriminator: '5728',
- collectibles: null,
- bot: true,
- avatar_decoration_data: null,
- avatar: '48ae3c24f2a6ec5c60c41bdabd904018'
- },
- roles: [
- '703457691342995528',
- '289671295359254529',
- '1040735082610167858',
- '114526764860047367'
- ],
- premium_since: null,
- pending: false,
- nick: 'Mother',
- mute: false,
- joined_at: '2020-04-25T04:09:43.253000+00:00',
- flags: 0,
- deaf: false,
- communication_disabled_until: null,
- banner: null,
- avatar: null
- }],
emojis: [
{
roles: [],
@@ -259,7 +206,7 @@ module.exports = {
unicode_emoji: null,
tags: {},
position: 0,
- permissions: '1122573558996672',
+ permissions: '559623605575360',
name: '@everyone',
mentionable: false,
managed: false,
@@ -1276,14 +1223,12 @@ module.exports = {
}
},
pins: {
- faked: {
- items: [
- {message: {id: "1126786462646550579"}},
- {message: {id: "1141501302736695316"}},
- {message: {id: "1106366167788044450"}},
- {message: {id: "1115688611186193400"}}
- ]
- }
+ faked: [
+ {id: "1126786462646550579"},
+ {id: "1141501302736695316"},
+ {id: "1106366167788044450"},
+ {id: "1115688611186193400"}
+ ]
},
message: {
// Display order is text content, attachments, then stickers
@@ -2712,47 +2657,6 @@ module.exports = {
flags: 0,
components: []
},
- large_file_from_matrix: {
- type: 0,
- content: "",
- attachments: [
- {
- id: "1439351589474140290",
- filename: "image.png",
- size: 5112701,
- url: "https://cdn.discordapp.com/attachments/1438284564815548418/1439351589474140290/image.png?ex=691cd720&is=691b85a0&hm=671d32324ce17acb9708057f9a532a3184d02343747b32b2ad8d330a277d8f65&",
- proxy_url: "https://media.discordapp.net/attachments/1438284564815548418/1439351589474140290/image.png?ex=691cd720&is=691b85a0&hm=671d32324ce17acb9708057f9a532a3184d02343747b32b2ad8d330a277d8f65&",
- width: 1930,
- height: 2522,
- content_type: "image/png",
- flags: 16,
- content_scan_version: 2,
- placeholder: "ZhgKDQJ6pIp2B5hmhndoZ0lgiwTJ",
- placeholder_version: 1,
- spoiler: false
- }
- ],
- embeds: [],
- timestamp: new Date().toISOString(),
- edited_timestamp: null,
- flags: 0,
- components: [],
- id: "1439351590262800565",
- channel_id: "1438284564815548418",
- author: {
- id: "1438286167706701958",
- username: "cibo",
- discriminator: "0000",
- avatar: "9c9e1d63ce093e76b9cdb99328c91201",
- bot: true
- },
- pinned: false,
- mentions: [],
- mention_roles: [],
- mention_everyone: false,
- tts: false,
- webhook_id: "1438286167706701958"
- },
simple_reply_to_reply_in_thread: {
type: 19,
tts: false,
@@ -3239,37 +3143,6 @@ module.exports = {
flags: 0,
components: []
},
- emojihax: {
- id: "1126733830494093453",
- type: 0,
- content: "I only violate the don't modify our console part of terms of service [troll~1](https://cdn.discordapp.com/emojis/1254940125948022915.webp?size=48&name=troll%7E1&lossless=true)",
- channel_id: "112760669178241024",
- author: {
- id: "111604486476181504",
- username: "kyuugryphon",
- avatar: "e4ce31267ca524d19be80e684d4cafa1",
- discriminator: "0",
- public_flags: 0,
- flags: 0,
- banner: null,
- accent_color: null,
- global_name: "KyuuGryphon",
- avatar_decoration: null,
- display_name: "KyuuGryphon",
- banner_color: null
- },
- attachments: [],
- embeds: [],
- mentions: [],
- mention_roles: [],
- pinned: false,
- mention_everyone: false,
- tts: false,
- timestamp: "2023-07-07T04:37:58.892000+00:00",
- edited_timestamp: null,
- flags: 0,
- components: []
- },
emoji_triple_long_name: {
id: "1156394116540805170",
type: 0,
@@ -3644,233 +3517,7 @@ module.exports = {
},
attachments: [],
guild_id: "286888431945252874"
- },
- poll_single_choice: {
- type: 0,
- content: "",
- mentions: [],
- mention_roles: [],
- attachments: [],
- embeds: [],
- timestamp: "2025-02-15T23:19:04.127000+00:00",
- edited_timestamp: null,
- flags: 0,
- components: [],
- id: "1340462414176718889",
- channel_id: "1340048919589158986",
- author: {
- id: "307894326028140546",
- username: "ellienyaa",
- avatar: "f98417a0a0b4aecc7d7667bece353b7e",
- discriminator: "0",
- public_flags: 128,
- flags: 128,
- banner: null,
- accent_color: null,
- global_name: "unambiguously boring username",
- avatar_decoration_data: null,
- banner_color: null,
- clan: null,
- primary_guild: null
- },
- pinned: false,
- mention_everyone: false,
- tts: false,
- position: 0,
- poll: {
- question: {
- text: "only one answer allowed!"
- },
- answers: [
- {
- answer_id: 1,
- poll_media: {
- text: "answer one",
- emoji: {
- id: null,
- name: "\ud83d\udc4d"
- }
- }
- },
- {
- answer_id: 2,
- poll_media: {
- text: "answer two",
- emoji: {
- id: null,
- name: "\ud83d\udc4e"
- }
- }
- },
- {
- answer_id: 3,
- poll_media: {
- text: "answer three"
- }
- }
- ],
- expiry: "2025-02-16T23:19:04.122364+00:00",
- allow_multiselect: false,
- layout_type: 1,
- results: {
- answer_counts: [],
- is_finalized: false
- }
- }
- },
- poll_multiple_choice: {
- type: 0,
- content: "",
- mentions: [],
- mention_roles: [],
- attachments: [],
- embeds: [],
- timestamp: "2025-02-16T00:47:12.310000+00:00",
- edited_timestamp: null,
- flags: 0,
- components: [],
- id: "1340484594423562300",
- channel_id: "1340048919589158986",
- author: {
- id: "307894326028140546",
- username: "ellienyaa",
- avatar: "f98417a0a0b4aecc7d7667bece353b7e",
- discriminator: "0",
- public_flags: 128,
- flags: 128,
- banner: null,
- accent_color: null,
- global_name: "unambiguously boring username",
- avatar_decoration_data: null,
- banner_color: null,
- clan: null,
- primary_guild: null
- },
- pinned: false,
- mention_everyone: false,
- tts: false,
- position: 0,
- poll: {
- question: {
- text: "more than one answer allowed"
- },
- answers: [
- {
- answer_id: 1,
- poll_media: {
- text: "no",
- emoji: {
- id: null,
- name: "😭"
- }
- }
- },
- {
- answer_id: 2,
- poll_media: {
- text: "oh no",
- emoji: {
- id: "891723675261366292",
- name: "this"
- }
- }
- },
- {
- answer_id: 3,
- poll_media: {
- text: "oh noooooo",
- emoji: {
- id: "964520120682680350",
- name: "disapprove"
- }
- }
- }
- ],
- expiry: "2025-02-17T00:47:12.307985+00:00",
- allow_multiselect: true,
- layout_type: 1,
- results: {
- answer_counts: [],
- is_finalized: false
- }
- }
- },
- poll_close: {
- type: 46,
- content: "",
- mentions: [
- {
- id: "307894326028140546",
- username: "ellienyaa",
- avatar: "f98417a0a0b4aecc7d7667bece353b7e",
- discriminator: "0",
- public_flags: 128,
- flags: 128,
- banner: null,
- accent_color: null,
- global_name: "unambiguously boring username",
- avatar_decoration_data: null,
- banner_color: null,
- clan: null,
- primary_guild: null
- }
- ],
- mention_roles: [],
- attachments: [],
- embeds: [
- {
- type: "poll_result",
- fields: [
- {
- name: "poll_question_text",
- value: "test poll that's being closed",
- inline: false
- },
- {
- name: "victor_answer_votes",
- value: "0",
- inline: false
- },
- {
- name: "total_votes",
- value: "0",
- inline: false
- }
- ],
- content_scan_version: 0
- }
- ],
- timestamp: "2025-02-20T23:07:12.178000+00:00",
- edited_timestamp: null,
- flags: 0,
- components: [],
- id: "1342271367374049351",
- channel_id: "1340048919589158986",
- author: {
- id: "307894326028140546",
- username: "ellienyaa",
- avatar: "f98417a0a0b4aecc7d7667bece353b7e",
- discriminator: "0",
- public_flags: 128,
- flags: 128,
- banner: null,
- accent_color: null,
- global_name: "unambiguously boring username",
- avatar_decoration_data: null,
- banner_color: null,
- clan: null,
- primary_guild: null
- },
- pinned: false,
- mention_everyone: false,
- tts: false,
- message_reference: {
- type: 0,
- channel_id: "1340048919589158986",
- message_id: "1342271353990021206"
- },
- position: 0
- }
+ }
},
pk_message: {
pk_reply_to_matrix: {
@@ -4637,7 +4284,7 @@ module.exports = {
flags: 0,
components: []
},
- extreme_html_escaping: {
+ escaping_crazy_html_tags: {
id: "1158894131322552391",
type: 0,
content: "",
@@ -4967,69 +4614,6 @@ module.exports = {
flags: 0,
components: []
},
- klipy_gif: {
- type: 0,
- content: "https://klipy.com/gifs/cute-15",
- mentions: [],
- mention_roles: [],
- attachments: [],
- embeds: [
- {
- type: "gifv",
- url: "https://klipy.com/gifs/cute-15",
- title: "Cute Corgi Waddle",
- provider: {
- name: "Klipy",
- url: "https://klipy.com"
- },
- thumbnail: {
- url: "https://static.klipy.com/ii/d7aec6f6f171607374b2065c836f92f4/5b/5b/xHVF6sVV.webp",
- proxy_url: "https://images-ext-1.discordapp.net/external/Z54QmlQflPPb6NoXikflBHGmttgRm3_jhzmcILXHhcA/https/static.klipy.com/ii/d7aec6f6f171607374b2065c836f92f4/5b/5b/xHVF6sVV.webp",
- width: 277,
- height: 498,
- placeholder: "3gcGDAJV+WZYl3RpZ2gGeFBxBw==",
- placeholder_version: 1,
- flags: 0
- },
- video: {
- url: "https://static.klipy.com/ii/d7aec6f6f171607374b2065c836f92f4/5b/5b/7ndEhcilPNKJ8O.mp4",
- proxy_url: "https://images-ext-1.discordapp.net/external/xZspzkQPUKBa74pBhJDpBf3v2d3d0lC943xaB9_JnoM/https/static.klipy.com/ii/d7aec6f6f171607374b2065c836f92f4/5b/5b/7ndEhcilPNKJ8O.mp4",
- width: 356,
- height: 640,
- placeholder: "3gcGDAJV+WZYl3RpZ2gGeFBxBw==",
- placeholder_version: 1,
- flags: 0
- },
- content_scan_version: 4
- }
- ],
- timestamp: "2026-02-03T11:11:50.070000+00:00",
- edited_timestamp: null,
- flags: 0,
- components: [],
- id: "1468202316233707613",
- channel_id: "1370776315266859131",
- author: {
- id: "304655299631906816",
- username: "witterson",
- avatar: "47ec94a1b2b4cc41ce0329b3575e9b66",
- discriminator: "0",
- public_flags: 0,
- flags: 0,
- banner: null,
- accent_color: null,
- global_name: "wit",
- avatar_decoration_data: null,
- collectibles: null,
- display_name_styles: null,
- banner_color: null,
- clan: null,
- primary_guild: null
- },
- pinned: false,
- mention_everyone: false,
- tts: false
- },
tenor_gif: {
type: 0,
content: "<@&1182745800661540927> get real https://tenor.com/view/get-real-gif-26176788",
@@ -5087,318 +4671,6 @@ module.exports = {
pinned: false,
mention_everyone: false,
tts: false
- },
- four_images: {
- type: 0,
- content: "",
- mentions: [],
- mention_roles: [],
- attachments: [],
- embeds: [],
- timestamp: "2026-03-12T18:00:50.737000+00:00",
- edited_timestamp: null,
- flags: 16384,
- components: [],
- id: "1481713598278533241",
- channel_id: "687028734322147344",
- author: {
- id: "112760500130975744",
- username: "minimus",
- avatar: "a_a354b9eaff512485b49c82b13691b941",
- discriminator: "0",
- public_flags: 512,
- flags: 512,
- banner: null,
- accent_color: null,
- global_name: "minimus",
- avatar_decoration_data: null,
- collectibles: null,
- display_name_styles: { font_id: 11, effect_id: 5, colors: [ 6106655 ] },
- banner_color: null,
- clan: null,
- primary_guild: null
- },
- pinned: false,
- mention_everyone: false,
- tts: false,
- message_reference: {
- type: 1,
- channel_id: "637339857118822430",
- message_id: "1481696763483258891",
- guild_id: "408573045540651009"
- },
- message_snapshots: [
- {
- message: {
- type: 0,
- content: "https://fixupx.com/i/status/2032003668787020046",
- mentions: [],
- mention_roles: [],
- attachments: [],
- embeds: [
- {
- type: "rich",
- url: "https://fixupx.com/i/status/2032003668787020046",
- description: "4chan owner Hiroyuki, Evangelion director Hideaki Anno and GACKT to participate in “humanity’s last non\\-AI made social network”\n" +
- "︀︀\n" +
- "︀︀[automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/](https://automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/)\n" +
- "\n" +
- "**[💬](https://x.com/intent/tweet?in_reply_to=2032003668787020046) 36 [🔁](https://x.com/intent/retweet?tweet_id=2032003668787020046) 212 [❤](https://x.com/intent/like?tweet_id=2032003668787020046) 3\\.0K 👁 131\\.7K **",
- color: 6513919,
- timestamp: "2026-03-12T08:00:02+00:00",
- author: {
- name: "AUTOMATON WEST (@AUTOMATON_ENG)",
- url: "https://x.com/AUTOMATON_ENG/status/2032003668787020046",
- icon_url: "https://pbs.twimg.com/profile_images/1353559126693961729/pz-WVnDc_200x200.jpg",
- proxy_icon_url: "https://images-ext-1.discordapp.net/external/1OzGhjvZTRstTxM38_7pqHXlmdbMddqh1F8R0-WrKqw/https/pbs.twimg.com/profile_images/1353559126693961729/pz-WVnDc_200x200.jpg"
- },
- image: {
- url: "https://pbs.twimg.com/media/HDMUyf6bQAM3yts.jpg?name=orig",
- proxy_url: "https://images-ext-1.discordapp.net/external/NkNgp2SyY1OCH9IdS8hqsUqbnbrp3A9oLNwYusVVCVQ/%3Fname%3Dorig/https/pbs.twimg.com/media/HDMUyf6bQAM3yts.jpg",
- width: 872,
- height: 886,
- content_type: "image/jpeg",
- placeholder: "6vcFFwL6R3lye2V3l1mIl5l3WPN5FZ8H",
- placeholder_version: 1,
- flags: 0
- },
- footer: {
- text: "FixupX",
- icon_url: "https://assets.fxembed.com/logos/fixupx64.png",
- proxy_icon_url: "https://images-ext-1.discordapp.net/external/LwQ70Uiqfu0OCN4ZbA4f482TGCgQa-xGsnUFYfhIgYA/https/assets.fxembed.com/logos/fixupx64.png"
- },
- content_scan_version: 4
- },
- {
- type: "rich",
- url: "https://fixupx.com/i/status/2032003668787020046",
- image: {
- url: "https://pbs.twimg.com/media/HDMUgxybQAE4FtJ.jpg?name=orig",
- proxy_url: "https://images-ext-1.discordapp.net/external/Rquh1ec-tG9hMqdHqIVSphO7zf5B5Fg_7yTWhCjlsek/%3Fname%3Dorig/https/pbs.twimg.com/media/HDMUgxybQAE4FtJ.jpg",
- width: 1114,
- height: 991,
- content_type: "image/jpeg",
- placeholder: "JQgKDoL3epZ8ZIdnlmmHZ4d4CIGmUEc=",
- placeholder_version: 1,
- flags: 0
- },
- content_scan_version: 4
- },
- {
- type: "rich",
- url: "https://fixupx.com/i/status/2032003668787020046",
- image: {
- url: "https://pbs.twimg.com/media/HDMUrPobgAAeb90.jpg?name=orig",
- proxy_url: "https://images-ext-1.discordapp.net/external/XrkhHNH3CvlZYvjkdykVnf-_xdz6HWX8uwesoAwwSfY/%3Fname%3Dorig/https/pbs.twimg.com/media/HDMUrPobgAAeb90.jpg",
- width: 944,
- height: 954,
- content_type: "image/jpeg",
- placeholder: "m/cJDwCbV0mfaoZzlihqeXdqCVN9A6oD",
- placeholder_version: 1,
- flags: 0
- },
- content_scan_version: 4
- },
- {
- type: "rich",
- url: "https://fixupx.com/i/status/2032003668787020046",
- image: {
- url: "https://pbs.twimg.com/media/HDMUuy5bgAAInj5.jpg?name=orig",
- proxy_url: "https://images-ext-1.discordapp.net/external/lO-5hBMU9bGH13Ax9xum2T2Mg0ATdv0b6BEx_VeVi80/%3Fname%3Dorig/https/pbs.twimg.com/media/HDMUuy5bgAAInj5.jpg",
- width: 1200,
- height: 630,
- content_type: "image/jpeg",
- placeholder: "tfcJDIK3mIl1eIiPdY23dX9b9w==",
- placeholder_version: 1,
- flags: 0
- },
- content_scan_version: 4
- }
- ],
- timestamp: "2026-03-12T16:53:57.009000+00:00",
- edited_timestamp: null,
- flags: 0,
- components: []
- }
- }
- ]
- }
- },
- message_with_components: {
- pk_question_mark_response: {
- type: 0,
- content: '',
- mentions: [],
- mention_roles: [],
- attachments: [],
- embeds: [],
- timestamp: '2026-01-30T01:20:07.488000+00:00',
- edited_timestamp: null,
- flags: 32768,
- author: {
- id: '466378653216014359',
- username: 'PluralKit',
- avatar: '466df0c98b1af1e1388f595b4c1ad1b9',
- discriminator: '0',
- public_flags: 0,
- flags: 0,
- bot: true,
- banner: null,
- accent_color: null,
- global_name: 'PluralKit',
- avatar_decoration_data: null,
- collectibles: null,
- display_name_styles: null,
- banner_color: null
- },
- components: [
- {
- type: 17,
- id: 1,
- accent_color: 1042150,
- components: [
- {
- type: 9,
- id: 2,
- components: [
- { type: 10, id: 3, content: '### Lillith (INX)' },
- {
- type: 10,
- id: 4,
- content: '**Display name:** Lillith (she/her)\n' +
- '**Pronouns:** She/Her\n' +
- '**Message count:** 3091'
- }
- ],
- accessory: {
- type: 11,
- id: 5,
- media: {
- id: '1466603856149610687',
- url: 'https://files.inx.moe/p/cdn/lillith.webp',
- proxy_url: 'https://images-ext-1.discordapp.net/external/Kn5b32mM4o8AAQbq0k39KOzp9-fy6D1tWKvK_XI27LI/https/files.inx.moe/p/cdn/lillith.webp',
- width: 256,
- height: 256,
- placeholder: 'KVoKJwSnt7lZl5ecj1mal5eGWjAHZXIA',
- placeholder_version: 1,
- content_scan_metadata: { version: 4, flags: 0 },
- content_type: 'image/webp',
- loading_state: 2,
- flags: 0
- },
- description: null,
- spoiler: false
- }
- },
- { type: 14, id: 6, spacing: 1, divider: true },
- {
- type: 10,
- id: 7,
- content: '**Proxy tags:**\n' +
- '``l;text``\n' +
- '``l:text``\n' +
- '``l.text``\n' +
- '``textl.``\n' +
- '``textl;``\n' +
- '``textl:``'
- }
- ],
- spoiler: false
- },
- {
- type: 9,
- id: 8,
- components: [
- {
- type: 10,
- id: 9,
- content: '-# System ID: `xffgnx` ∙ Member ID: `pphhoh`\n' +
- '-# Created: 2025-12-31 03:16:45 UTC'
- }
- ],
- accessory: {
- type: 2,
- id: 10,
- style: 5,
- label: 'View on dashboard',
- url: 'https://dash.pluralkit.me/profile/m/pphhoh'
- }
- },
- { type: 14, id: 11, spacing: 1, divider: true },
- {
- type: 17,
- id: 12,
- accent_color: null,
- components: [
- {
- type: 9,
- id: 13,
- components: [
- {
- type: 10,
- id: 14,
- content: '**System:** INX (`xffgnx`)\n' +
- '**Member:** Lillith (`pphhoh`)\n' +
- '**Sent by:** infinidoge1337 (<@197126718400626689>)\n' +
- '\n' +
- '**Account Roles (7)**\n' +
- '§b, !, ‼, Ears Port Ping, Ears Update Ping, Yttr Ping, unsup Ping'
- }
- ],
- accessory: {
- type: 11,
- id: 15,
- media: {
- id: '1466603856149610689',
- url: 'https://files.inx.moe/p/cdn/lillith.webp',
- proxy_url: 'https://images-ext-1.discordapp.net/external/Kn5b32mM4o8AAQbq0k39KOzp9-fy6D1tWKvK_XI27LI/https/files.inx.moe/p/cdn/lillith.webp',
- width: 256,
- height: 256,
- placeholder: 'KVoKJwSnt7lZl5ecj1mal5eGWjAHZXIA',
- placeholder_version: 1,
- content_scan_metadata: { version: 4, flags: 0 },
- content_type: 'image/webp',
- loading_state: 2,
- flags: 0
- },
- description: null,
- spoiler: false
- }
- },
- { type: 14, id: 16, spacing: 2, divider: true },
- { type: 10, id: 17, content: 'Same hat' },
- {
- type: 12,
- id: 18,
- items: [
- {
- media: {
- id: '1466603856149610690',
- url: 'https://cdn.discordapp.com/attachments/934955898965729280/1466556006527012987/image.png?ex=697d2c37&is=697bdab7&hm=09c5028be61ce01ebbdda5c79c42e4dc10d053ce0c4b12c9d84135a0708e9db6&',
- proxy_url: 'https://media.discordapp.net/attachments/934955898965729280/1466556006527012987/image.png?ex=697d2c37&is=697bdab7&hm=09c5028be61ce01ebbdda5c79c42e4dc10d053ce0c4b12c9d84135a0708e9db6&',
- width: 285,
- height: 126,
- placeholder: '0PcBA4BqSIl9t/dnn9f0rm0=',
- placeholder_version: 1,
- content_scan_metadata: { version: 4, flags: 0 },
- content_type: 'image/png',
- loading_state: 2,
- flags: 0
- },
- description: null,
- spoiler: false
- }
- ]
- }
- ],
- spoiler: false
- },
- {
- type: 10,
- id: 19,
- content: '-# Original Message ID: 1466556003645657118 · '
- }
- ]
}
},
message_update: {
@@ -5660,6 +4932,7 @@ module.exports = {
mention_roles: [],
mentions: [],
pinned: false,
+ timestamp: "2023-08-16T22:38:38.641000+00:00",
tts: false,
type: 0
},
@@ -5733,6 +5006,7 @@ module.exports = {
mention_roles: [],
mentions: [],
pinned: false,
+ timestamp: "2023-08-16T22:38:38.641000+00:00",
tts: false,
type: 0
},
@@ -5767,6 +5041,7 @@ module.exports = {
pinned: false,
mention_everyone: false,
tts: false,
+ timestamp: "2023-05-11T23:44:09.690000+00:00",
edited_timestamp: "2023-05-11T23:44:19.690000+00:00",
flags: 0,
components: [],
@@ -5807,6 +5082,7 @@ module.exports = {
pinned: false,
mention_everyone: false,
tts: false,
+ timestamp: "2023-05-11T23:44:09.690000+00:00",
edited_timestamp: "2023-05-11T23:44:19.690000+00:00",
flags: 0,
components: [],
@@ -5847,6 +5123,7 @@ module.exports = {
pinned: false,
mention_everyone: false,
tts: false,
+ timestamp: "2023-05-11T23:44:09.690000+00:00",
edited_timestamp: "2023-05-11T23:44:19.690000+00:00",
flags: 0,
components: [],
@@ -6019,36 +5296,6 @@ module.exports = {
guild_id: "112760669178241024",
id: "1210387798297682020"
},
- embed_generated_social_media_image_for_matrix_user: {
- channel_id: "112760669178241024",
- embeds: [
- {
- color: 8594767,
- description: "1v1 physical card game. Each player gets one standard deck of cards with a different backing to differentiate. Every turn proceeds as follows:\n\n * Both players draw eight cards\n * Both players may choose up to eight cards to discard, then draw that number of cards to put back in their hand\n * Both players present their best five-or-less-card pok...",
- provider: {
- name: "hthrflwrs on cohost"
- },
- thumbnail: {
- height: 1587,
- placeholder: "GpoKP5BJZphshnhwmmmYlmh3l7+m+mwJ",
- placeholder_version: 1,
- proxy_url: "https://images-ext-2.discordapp.net/external/9vTXIzlXU4wyUZvWfmlmQkck8nGLUL-A090W4lWsZ48/https/staging.cohostcdn.org/avatar/292-6b64b03c-4ada-42f6-8452-109275bfe68d-profile.png",
- url: "https://staging.cohostcdn.org/avatar/292-6b64b03c-4ada-42f6-8452-109275bfe68d-profile.png",
- width: 1644
- },
- title: "This post nerdsniped me, so here's some RULES FOR REAL-LIFE BALATRO",
- type: "link",
- url: "https://cohost.org/jkap/post/4794219-empty"
- }
- ],
- author: {
- name: "Matrix Bridge",
- id: "684280192553844747"
- },
- guild_id: "112760669178241024",
- id: "1128118177155526666",
- timestamp: "2025-01-01T00:00:00Z"
- },
embed_generated_on_reply: {
attachments: [],
author: {
@@ -6115,50 +5362,6 @@ module.exports = {
}
},
special_message: {
- emoji_added: {
- type: 63,
- content: '<:cx_marvelous:1437322787994992650>',
- mentions: [],
- mention_roles: [],
- attachments: [],
- embeds: [],
- timestamp: '2025-11-10T06:07:36.930000+00:00',
- edited_timestamp: null,
- flags: 0,
- components: [],
- id: '1437322788439457794',
- channel_id: '1100319550446252084',
- author: {
- id: '772659086046658620',
- username: 'cadence.worm',
- avatar: '466df0c98b1af1e1388f595b4c1ad1b9',
- discriminator: '0',
- public_flags: 0,
- flags: 0,
- banner: null,
- accent_color: null,
- global_name: 'cadence',
- avatar_decoration_data: null,
- collectibles: null,
- display_name_styles: null,
- banner_color: null,
- clan: {
- identity_guild_id: '532245108070809601',
- identity_enabled: true,
- tag: 'doll',
- badge: 'dba08126b4e810a0e096cc7cd5bc37f0'
- },
- primary_guild: {
- identity_guild_id: '532245108070809601',
- identity_enabled: true,
- tag: 'doll',
- badge: 'dba08126b4e810a0e096cc7cd5bc37f0'
- }
- },
- pinned: false,
- mention_everyone: false,
- tts: false
- },
thread_name_change: {
id: "1142391602799710298",
type: 4,
@@ -6190,37 +5393,6 @@ module.exports = {
components: [],
position: 12
},
- channel_follow_add: {
- type: 12,
- content: "PluralKit #downtime",
- attachments: [],
- embeds: [],
- timestamp: "2026-03-24T23:16:04.097Z",
- edited_timestamp: null,
- flags: 0,
- components: [],
- id: "1486141581047369888",
- channel_id: "1451125453082591314",
- author: {
- id: "154058479798059009",
- username: "exaptations",
- discriminator: "0",
- avatar: "57b5cfe09a48a5902f2eb8fa65bb1b80",
- bot: false,
- flags: 0,
- globalName: "Exa",
- },
- pinned: false,
- mentions: [],
- mention_roles: [],
- mention_everyone: false,
- tts: false,
- message_reference: {
- type: 0,
- channel_id: "1015204661701124206",
- guild_id: "466707357099884544"
- }
- },
updated_to_start_thread_from_here: {
t: "MESSAGE_UPDATE",
s: 19,
diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql
index 07f8c24..b31f2c3 100644
--- a/test/ooye-test-data.sql
+++ b/test/ooye-test-data.sql
@@ -3,30 +3,24 @@ BEGIN TRANSACTION;
INSERT INTO guild_active (guild_id, autocreate) VALUES
('112760669178241024', 1),
('66192955777486848', 1),
-('665289423482519565', 0),
-('1345641201902288987', 1);
+('665289423482519565', 0);
INSERT INTO guild_space (guild_id, space_id, privacy_level) VALUES
-('112760669178241024', '!jjmvBegULiLucuWEHU:cadence.moe', 0),
-('1345641201902288987', '!CvQMeeqXIkgedUpkzv:cadence.moe', 0);
+('112760669178241024', '!jjmvBegULiLucuWEHU:cadence.moe', 0);
-INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar, guild_id) VALUES
-('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main', NULL, NULL, '112760669178241024'),
-('687028734322147344', '!fGgIymcYWOqjbSRUdV:cadence.moe', 'slow-news-day', NULL, NULL, NULL, '112760669178241024'),
-('497161350934560778', '!CzvdIdUQXgUjDVKxeU:cadence.moe', 'amanda-spam', NULL, NULL, NULL, '66192955777486848'),
-('160197704226439168', '!hYnGGlPHlbujVVfktC:cadence.moe', 'the-stanley-parable-channel', 'bots', NULL, NULL, '112760669178241024'),
-('1100319550446252084', '!BnKuBPCvyfOkhcUjEu:cadence.moe', 'worm-farm', NULL, NULL, NULL, '66192955777486848'),
-('1162005314908999790', '!FuDZhlOAtqswlyxzeR:cadence.moe', 'Hey.', NULL, '1100319550446252084', NULL, '112760669178241024'),
-('297272183716052993', '!rEOspnYqdOalaIFniV:cadence.moe', 'general', NULL, NULL, NULL, '66192955777486848'),
-('122155380120748034', '!cqeGDbPiMFAhLsqqqq:cadence.moe', 'cadences-mind', 'coding', NULL, NULL, '112760669178241024'),
-('176333891320283136', '!qzDBLKlildpzrrOnFZ:cadence.moe', '🌈丨davids-horse_she-took-the-kids', 'wonderland', NULL, 'mxc://cadence.moe/EVvrSkKIRONHjtRJsMLmHWLS', '112760669178241024'),
-('489237891895768942', '!tnedrGVYKFNUdnegvf:tchncs.de', 'ex-room-doesnt-exist-any-more', NULL, NULL, NULL, '66192955777486848'),
-('1160894080998461480', '!TqlyQmifxGUggEmdBN:cadence.moe', 'ooyexperiment', NULL, NULL, NULL, '66192955777486848'),
-('1161864271370666075', '!mHmhQQPwXNananMUqq:cadence.moe', 'updates', NULL, NULL, NULL, '112760669178241024'),
-('1438284564815548418', '!MHxNpwtgVqWOrmyoTn:cadence.moe', 'sin-cave', NULL, NULL, NULL, '665289423482519565'),
-('598707048112193536', '!JBxeGYnzQwLnaooOLD:cadence.moe', 'winners', NULL, NULL, NULL, '1345641201902288987');
-
-INSERT INTO historical_channel_room (reference_channel_id, room_id, upgraded_timestamp) SELECT channel_id, room_id, 0 FROM channel_room;
+INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar) VALUES
+('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main', NULL, NULL),
+('687028734322147344', '!fGgIymcYWOqjbSRUdV:cadence.moe', 'slow-news-day', NULL, NULL, NULL),
+('497161350934560778', '!CzvdIdUQXgUjDVKxeU:cadence.moe', 'amanda-spam', NULL, NULL, NULL),
+('160197704226439168', '!hYnGGlPHlbujVVfktC:cadence.moe', 'the-stanley-parable-channel', 'bots', NULL, NULL),
+('1100319550446252084', '!BnKuBPCvyfOkhcUjEu:cadence.moe', 'worm-farm', NULL, NULL, NULL),
+('1162005314908999790', '!FuDZhlOAtqswlyxzeR:cadence.moe', 'Hey.', NULL, '1100319550446252084', NULL),
+('297272183716052993', '!rEOspnYqdOalaIFniV:cadence.moe', 'general', NULL, NULL, NULL),
+('122155380120748034', '!cqeGDbPiMFAhLsqqqq:cadence.moe', 'cadences-mind', 'coding', NULL, NULL),
+('176333891320283136', '!qzDBLKlildpzrrOnFZ:cadence.moe', '🌈丨davids-horse_she-took-the-kids', 'wonderland', NULL, 'mxc://cadence.moe/EVvrSkKIRONHjtRJsMLmHWLS'),
+('489237891895768942', '!tnedrGVYKFNUdnegvf:tchncs.de', 'ex-room-doesnt-exist-any-more', NULL, NULL, NULL),
+('1160894080998461480', '!TqlyQmifxGUggEmdBN:cadence.moe', 'ooyexperiment', NULL, NULL, NULL),
+('1161864271370666075', '!mHmhQQPwXNananMUqq:cadence.moe', 'updates', NULL, NULL, NULL);
INSERT INTO sim (user_id, username, sim_name, mxid) VALUES
('0', 'Matrix Bridge', 'bot', '@_ooye_bot:cadence.moe'),
@@ -38,30 +32,16 @@ INSERT INTO sim (user_id, username, sim_name, mxid) VALUES
('1109360903096369153', 'Amanda', 'amanda', '@_ooye_amanda:cadence.moe'),
('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '_pk_zoego', '_pk_zoego', '@_ooye__pk_zoego:cadence.moe'),
('320067006521147393', 'papiophidian', 'papiophidian', '@_ooye_papiophidian:cadence.moe'),
-('772659086046658620', 'cadence.worm', 'cadence', '@_ooye_cadence:cadence.moe'),
-('196188877885538304', 'ampflower', 'ampflower', '@_ooye_ampflower:cadence.moe'),
-('1458668878107381800', 'Evil Lillith (she/her)', 'evil_lillith_sheher', '@_ooye_evil_lillith_sheher:cadence.moe'),
-('197126718400626689', 'infinidoge1337', 'infinidoge1337', '@_ooye_infinidoge1337:cadence.moe');
-
+('772659086046658620', 'cadence.worm', 'cadence', '@_ooye_cadence:cadence.moe');
INSERT INTO sim_member (mxid, room_id, hashed_profile_content) VALUES
('@_ooye_bojack_horseman:cadence.moe', '!hYnGGlPHlbujVVfktC:cadence.moe', NULL),
-('@_ooye_cadence:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL),
-('@_ooye_cadence:cadence.moe', '!kLRqKKUQXcibIMtOpl:cadence.moe', NULL),
-('@_ooye_cadence:cadence.moe', '!fGgIymcYWOqjbSRUdV:cadence.moe', NULL),
-('@_ooye_ampflower:cadence.moe', '!qzDBLKlildpzrrOnFZ:cadence.moe', NULL),
-('@_ooye__pk_zoego:cadence.moe', '!qzDBLKlildpzrrOnFZ:cadence.moe', NULL),
-('@_ooye_infinidoge1337:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL),
-('@_ooye_evil_lillith_sheher:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL);
+('@_ooye_cadence:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL);
INSERT INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES
('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '196188877885538304', 'Azalea &flwr; 🌺');
-INSERT INTO app_user_install (guild_id, app_bot_id, user_id) VALUES
-('66192955777486848', '1458668878107381800', '197126718400626689');
-
-INSERT INTO message_room (message_id, historical_room_index)
-WITH a (message_id, channel_id) AS (VALUES
+INSERT INTO message_channel (message_id, channel_id) VALUES
('1106366167788044450', '122155380120748034'),
('1106366167788044451', '122155380120748034'),
('1106366167788044452', '122155380120748034'),
@@ -93,10 +73,7 @@ WITH a (message_id, channel_id) AS (VALUES
('1144874214311067708', '687028734322147344'),
('1339000288144658482', '176333891320283136'),
('1381212840957972480', '112760669178241024'),
-('1401760355339862066', '112760669178241024'),
-('1439351590262800565', '1438284564815548418'),
-('1404133238414376971', '112760669178241024'))
-SELECT message_id, max(historical_room_index) as historical_room_index FROM a INNER JOIN historical_channel_room ON historical_channel_room.reference_channel_id = a.channel_id GROUP BY message_id;
+('1401760355339862066', '112760669178241024');
INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES
('$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg', 'm.room.message', 'm.text', '1126786462646550579', 0, 0, 1),
@@ -111,8 +88,8 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part
('$oLyUTyZ_7e_SUzGNWZKz880ll9amLZvXGbArJCKai2Q', 'm.room.message', 'm.text', '1128084748338741392', 0, 0, 1),
('$FchUVylsOfmmbj-VwEs5Z9kY49_dt2zd0vWfylzy5Yo', 'm.room.message', 'm.text', '1143121514925928541', 0, 0, 1),
('$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qdFv4', 'm.room.message', 'm.text', '1106366167788044450', 0, 1, 1),
-('$Ijf1MFCD39ktrNHxrA-i2aKoRWNYdAV2ZXYQeiZIgEU', 'm.room.message', 'm.image', '1106366167788044450', 1, 1, 1),
-('$f9cjKiacXI9qPF_nUAckzbiKnJEi0LM399kOkhdd8f8', 'm.sticker', NULL, '1106366167788044450', 1, 0, 1),
+('$Ijf1MFCD39ktrNHxrA-i2aKoRWNYdAV2ZXYQeiZIgEU', 'm.room.message', 'm.image', '1106366167788044450', 1, 1, 0),
+('$f9cjKiacXI9qPF_nUAckzbiKnJEi0LM399kOkhdd8f8', 'm.sticker', NULL, '1106366167788044450', 1, 0, 0),
('$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qd999', 'm.room.message', 'm.text', '1106366167788044451', 0, 0, 1),
('$Ijf1MFCD39ktrNHxrA-i2aKoRWNYdAV2ZXYQeiZI999', 'm.room.message', 'm.image', '1106366167788044451', 0, 0, 1),
('$f9cjKiacXI9qPF_nUAckzbiKnJEi0LM399kOkhdd999', 'm.sticker', NULL, '1106366167788044451', 0, 0, 1),
@@ -140,10 +117,7 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part
('$51gH61p_eJc2RylOdE2lAr4-ogP7dS0WJI62lCFzBvk', 'm.room.message', 'm.text', '1339000288144658482', 0, 0, 0),
('$AfrB8hzXkDMvuoWjSZkDdFYomjInWH7jMBPkwQMN8AI', 'm.room.message', 'm.text', '1381212840957972480', 0, 1, 1),
('$43baKEhJfD-RlsFQi0LB16Zxd8yMqp0HSVL00TDQOqM', 'm.room.message', 'm.image', '1381212840957972480', 1, 0, 1),
-('$7P2O_VTQNHvavX5zNJ35DV-dbJB1Ag80tGQP_JzGdhk', 'm.room.message', 'm.text', '1401760355339862066', 0, 0, 0),
-('$ielAnR6geu0P1Tl5UXfrbxlIf-SV9jrNprxrGXP3v7M', 'm.room.message', 'm.image', '1439351590262800565', 0, 0, 0),
-('$uUKLcTQvik5tgtTGDKuzn0Ci4zcCvSoUcYn2X7mXm9I', 'm.room.message', 'm.text', '1404133238414376971', 0, 1, 1),
-('$LhmoWWvYyn5_AHkfb6FaXmLI6ZOC1kloql5P40YDmIk', 'm.room.message', 'm.notice', '1404133238414376971', 1, 0, 1);
+('$7P2O_VTQNHvavX5zNJ35DV-dbJB1Ag80tGQP_JzGdhk', 'm.room.message', 'm.text', '1401760355339862066', 0, 0, 0);
INSERT INTO file (discord_url, mxc_url) VALUES
('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'),
@@ -158,15 +132,13 @@ INSERT INTO file (discord_url, mxc_url) VALUES
('https://cdn.discordapp.com/emojis/230201364309868544.png', 'mxc://cadence.moe/qWmbXeRspZRLPcjseyLmeyXC'),
('https://cdn.discordapp.com/emojis/393635038903926784.gif', 'mxc://cadence.moe/WbYqNlACRuicynBfdnPYtmvc'),
('https://cdn.discordapp.com/attachments/176333891320283136/1157854643037163610/Screenshot_20231001_034036.jpg', 'mxc://cadence.moe/zAXdQriaJuLZohDDmacwWWDR'),
-('https://cdn.discordapp.com/emojis/1125827250609201255.webp', 'mxc://cadence.moe/pgdGTxAyEltccRgZKxdqzHHP'),
+('https://cdn.discordapp.com/emojis/1125827250609201255.png', 'mxc://cadence.moe/pgdGTxAyEltccRgZKxdqzHHP'),
('https://cdn.discordapp.com/avatars/320067006521147393/5fc4ad85c1ea876709e9a7d3374a78a1.png?size=1024', 'mxc://cadence.moe/JPzSmALLirnIprlSMKohSSoX'),
('https://cdn.discordapp.com/emojis/288858540888686602.png', 'mxc://cadence.moe/mwZaCtRGAQQyOItagDeCocEO'),
('https://cdn.discordapp.com/attachments/112760669178241024/1197621094786531358/Ins_1960637570.mp4', 'mxc://cadence.moe/kMqLycqMURhVpwleWkmASpnU'),
('https://cdn.discordapp.com/attachments/1099031887500034088/1112476845502365786/voice-message.ogg', 'mxc://cadence.moe/MRRPDggXQMYkrUjTpxQbmcxB'),
('https://cdn.discordapp.com/attachments/122155380120748034/1174514575220158545/the.yml', 'mxc://cadence.moe/HnQIYQmmlIKwOQsbFsIGpzPP'),
-('https://cdn.discordapp.com/attachments/112760669178241024/1296237494987133070/100km.gif', 'mxc://cadence.moe/qDAotmebTfEIfsAIVCEZptLh'),
-('https://cdn.discordapp.com/attachments/123/456/my_enemies.txt', 'mxc://cadence.moe/y89EOTRp2lbeOkgdsEleGOge'),
-('https://cdn.discordapp.com/emojis/1254940125948022915.webp', 'mxc://cadence.moe/bvVJFgOIyNcAknKCbmaHDktG');
+('https://cdn.discordapp.com/attachments/112760669178241024/1296237494987133070/100km.gif', 'mxc://cadence.moe/qDAotmebTfEIfsAIVCEZptLh');
INSERT INTO emoji (emoji_id, name, animated, mxc_url) VALUES
('230201364309868544', 'hippo', 0, 'mxc://cadence.moe/qWmbXeRspZRLPcjseyLmeyXC'),
@@ -176,8 +148,7 @@ INSERT INTO emoji (emoji_id, name, animated, mxc_url) VALUES
('551636841284108289', 'ae_botrac4r', 0, 'mxc://cadence.moe/skqfuItqxNmBYekzmVKyoLzs'),
('975572106295259148', 'brillillillilliant_move', 0, 'mxc://cadence.moe/scfRIDOGKWFDEBjVXocWYQHik'),
('606664341298872324', 'online', 0, 'mxc://cadence.moe/LCEqjStXCxvRQccEkuslXEyZ'),
-('288858540888686602', 'upstinky', 0, 'mxc://cadence.moe/mwZaCtRGAQQyOItagDeCocEO'),
-('1437322787994992650', 'cx_marvelous', 0, 'mxc://cadence.moe/TPZdosVUjTIopsLijkygIbti');
+('288858540888686602', 'upstinky', 0, 'mxc://cadence.moe/mwZaCtRGAQQyOItagDeCocEO');
INSERT INTO member_cache (room_id, mxid, displayname, avatar_url, power_level) VALUES
('!jjmvBegULiLucuWEHU:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL, 50),
@@ -192,14 +163,13 @@ INSERT INTO member_cache (room_id, mxid, displayname, avatar_url, power_level) V
('!TqlyQmifxGUggEmdBN:cadence.moe', '@ampflower:matrix.org', 'Ampflower 🌺', 'mxc://cadence.moe/PRfhXYBTOalvgQYtmCLeUXko', 0),
('!TqlyQmifxGUggEmdBN:cadence.moe', '@aflower:syndicated.gay', 'Rose', 'mxc://syndicated.gay/ZkBUPXCiXTjdJvONpLJmcbKP', 0),
('!TqlyQmifxGUggEmdBN:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL, 0),
-('!iSyXgNxQcEuXoXpsSn:pussthecat.org', '@austin:tchncs.de', 'Austin Huang', 'mxc://tchncs.de/090a2b5e07eed2f71e84edad5207221e6c8f8b8e', 0),
-('!zq94fae5bVKUubZLp7:agiadn.org', '@underscore_x:agiadn.org', 'underscore_x', NULL, 100);
+('!iSyXgNxQcEuXoXpsSn:pussthecat.org', '@austin:tchncs.de', 'Austin Huang', 'mxc://tchncs.de/090a2b5e07eed2f71e84edad5207221e6c8f8b8e', 0);
INSERT INTO reaction (hashed_event_id, message_id, encoded_emoji) VALUES
(5162930312280790092, '1141501302736695317', '%F0%9F%90%88');
INSERT INTO member_power (mxid, room_id, power_level) VALUES
-('@test_auto_invite:example.org', '*', 150);
+('@test_auto_invite:example.org', '*', 100);
INSERT INTO lottie (sticker_id, mxc_url) VALUES
('860171525772279849', 'mxc://cadence.moe/ZtvvVbwMIdUZeovWVyGVFCeR');
@@ -222,10 +192,4 @@ INSERT INTO direct (mxid, room_id) VALUES
('@user1:example.org', '!existing:cadence.moe'),
('@user2:example.org', '!existing:cadence.moe');
--- for cross-room reply test, in 'updates' room
-UPDATE historical_channel_room SET room_id = '!mHmhQQPwXNananaOLD:cadence.moe' WHERE room_id = '!mHmhQQPwXNananMUqq:cadence.moe';
-INSERT INTO historical_channel_room (reference_channel_id, room_id, upgraded_timestamp) VALUES ('1161864271370666075', '!mHmhQQPwXNananMUqq:cadence.moe', 1767922455991);
-INSERT INTO message_room (message_id, historical_room_index) SELECT '1458091145136443547', historical_room_index FROM historical_channel_room WHERE room_id = '!mHmhQQPwXNananaOLD:cadence.moe';
-INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES ('$pgzCQjq_y5sy8RvWOUuoF3obNHjs8iNvt9c-odrOCPY', 'm.room.message', 'm.image', '1458091145136443547', 0, 0, 0);
-
COMMIT;
diff --git a/test/test.js b/test/test.js
index 70625a0..b01f0ce 100644
--- a/test/test.js
+++ b/test/test.js
@@ -6,29 +6,28 @@ const sqlite = require("better-sqlite3")
const {Writable} = require("stream")
const migrate = require("../src/db/migrate")
const HeatSync = require("heatsync")
-const {test} = require("supertape")
+const {test, extend} = require("supertape")
const data = require("./data")
const {green} = require("ansi-colors")
-const mixin = require("@cloudrac3r/mixin-deep")
const passthrough = require("../src/passthrough")
const db = new sqlite(":memory:")
-const registration = require("../src/matrix/read-registration")
-registration.reg = mixin(registration.getTemplateRegistration("cadence.moe"), {
- id: "baby",
- url: "http://localhost:6693",
- as_token: "don't actually take authenticated actions on the server",
- hs_token: "don't actually take authenticated actions on the server",
- ooye: {
- server_origin: "https://matrix.cadence.moe",
- bridge_origin: "https://bridge.example.org",
- discord_token: "Njg0MjgwMTkyNTUzODQ0NzQ3.Xl3zlw.baby",
- discord_client_secret: "baby",
- web_password: "password123",
- time_zone: "Pacific/Auckland",
- }
-})
+const {reg} = require("../src/matrix/read-registration")
+reg.ooye.discord_token = "Njg0MjgwMTkyNTUzODQ0NzQ3.Xl3zlw.baby"
+reg.ooye.server_origin = "https://matrix.cadence.moe" // so that tests will pass even when hard-coded
+reg.ooye.server_name = "cadence.moe"
+reg.ooye.namespace_prefix = "_ooye_"
+reg.sender_localpart = "_ooye_bot"
+reg.id = "baby"
+reg.as_token = "don't actually take authenticated actions on the server"
+reg.hs_token = "don't actually take authenticated actions on the server"
+reg.namespaces = {
+ users: [{regex: "@_ooye_.*:cadence.moe", exclusive: true}],
+ aliases: [{regex: "#_ooye_.*:cadence.moe", exclusive: true}]
+}
+reg.ooye.bridge_origin = "https://bridge.example.org"
+reg.ooye.time_zone = "Pacific/Auckland"
const sync = new HeatSync({watchFS: false})
@@ -73,45 +72,47 @@ const file = sync.require("../src/matrix/file")
file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not allowed to upload files during testing.\nURL: ${url}`) }
;(async () => {
- /* c8 ignore start - download some more test files in slow mode */
- test("test files: download", async t => {
- /** @param {{url: string, to: string}[]} files */
- async function allReporter(files) {
- return new Promise(resolve => {
- let resolved = 0
- const report = files.map(file => file.to.split("/").slice(-1)[0][0])
- files.map(download).forEach((p, i) => {
- p.then(() => {
- report[i] = green(".")
- process.stderr.write("\r" + report.join(""))
- if (++resolved === files.length) resolve(null)
+ /* c8 ignore start - maybe download some more test files in slow mode */
+ if (process.argv.includes("--slow")) {
+ test("test files: download", async t => {
+ /** @param {{url: string, to: string}[]} files */
+ async function allReporter(files) {
+ return new Promise(resolve => {
+ let resolved = 0
+ const report = files.map(file => file.to.split("/").slice(-1)[0][0])
+ files.map(download).forEach((p, i) => {
+ p.then(() => {
+ report[i] = green(".")
+ process.stderr.write("\r" + report.join(""))
+ if (++resolved === files.length) resolve(null)
+ })
})
})
- })
- }
- async function download({url, to}) {
- if (await fs.existsSync(to)) return
- const res = await fetch(url)
- // @ts-ignore
- await res.body.pipeTo(Writable.toWeb(fs.createWriteStream(to, {encoding: "binary"})))
- }
- await allReporter([
- {url: "https://cadence.moe/friends/ooye_test/RLMgJGfgTPjIQtvvWZsYjhjy.png", to: "test/res/RLMgJGfgTPjIQtvvWZsYjhjy.png"},
- {url: "https://cadence.moe/friends/ooye_test/bZFuuUSEebJYXUMSxuuSuLTa.png", to: "test/res/bZFuuUSEebJYXUMSxuuSuLTa.png"},
- {url: "https://cadence.moe/friends/ooye_test/qWmbXeRspZRLPcjseyLmeyXC.png", to: "test/res/qWmbXeRspZRLPcjseyLmeyXC.png"},
- {url: "https://cadence.moe/friends/ooye_test/wcouHVjbKJJYajkhJLsyeJAA.png", to: "test/res/wcouHVjbKJJYajkhJLsyeJAA.png"},
- {url: "https://cadence.moe/friends/ooye_test/WbYqNlACRuicynBfdnPYtmvc.gif", to: "test/res/WbYqNlACRuicynBfdnPYtmvc.gif"},
- {url: "https://cadence.moe/friends/ooye_test/HYcztccFIPgevDvoaWNsEtGJ.png", to: "test/res/HYcztccFIPgevDvoaWNsEtGJ.png"},
- {url: "https://cadence.moe/friends/ooye_test/lHfmJpzgoNyNtYHdAmBHxXix.png", to: "test/res/lHfmJpzgoNyNtYHdAmBHxXix.png"},
- {url: "https://cadence.moe/friends/ooye_test/MtRdXixoKjKKOyHJGWLsWLNU.png", to: "test/res/MtRdXixoKjKKOyHJGWLsWLNU.png"},
- {url: "https://cadence.moe/friends/ooye_test/HXfFuougamkURPPMflTJRxGc.png", to: "test/res/HXfFuougamkURPPMflTJRxGc.png"},
- {url: "https://cadence.moe/friends/ooye_test/ikYKbkhGhMERAuPPbsnQzZiX.png", to: "test/res/ikYKbkhGhMERAuPPbsnQzZiX.png"},
- {url: "https://cadence.moe/friends/ooye_test/AYPpqXzVJvZdzMQJGjioIQBZ.png", to: "test/res/AYPpqXzVJvZdzMQJGjioIQBZ.png"},
- {url: "https://cadence.moe/friends/ooye_test/UVuzvpVUhqjiueMxYXJiFEAj.png", to: "test/res/UVuzvpVUhqjiueMxYXJiFEAj.png"},
- {url: "https://ezgif.com/images/format-demo/butterfly.gif", to: "test/res/butterfly.gif"},
- {url: "https://ezgif.com/images/format-demo/butterfly.png", to: "test/res/butterfly.png"},
- ])
- }, {timeout: 60000})
+ }
+ async function download({url, to}) {
+ if (await fs.existsSync(to)) return
+ const res = await fetch(url)
+ // @ts-ignore
+ await res.body.pipeTo(Writable.toWeb(fs.createWriteStream(to, {encoding: "binary"})))
+ }
+ await allReporter([
+ {url: "https://cadence.moe/friends/ooye_test/RLMgJGfgTPjIQtvvWZsYjhjy.png", to: "test/res/RLMgJGfgTPjIQtvvWZsYjhjy.png"},
+ {url: "https://cadence.moe/friends/ooye_test/bZFuuUSEebJYXUMSxuuSuLTa.png", to: "test/res/bZFuuUSEebJYXUMSxuuSuLTa.png"},
+ {url: "https://cadence.moe/friends/ooye_test/qWmbXeRspZRLPcjseyLmeyXC.png", to: "test/res/qWmbXeRspZRLPcjseyLmeyXC.png"},
+ {url: "https://cadence.moe/friends/ooye_test/wcouHVjbKJJYajkhJLsyeJAA.png", to: "test/res/wcouHVjbKJJYajkhJLsyeJAA.png"},
+ {url: "https://cadence.moe/friends/ooye_test/WbYqNlACRuicynBfdnPYtmvc.gif", to: "test/res/WbYqNlACRuicynBfdnPYtmvc.gif"},
+ {url: "https://cadence.moe/friends/ooye_test/HYcztccFIPgevDvoaWNsEtGJ.png", to: "test/res/HYcztccFIPgevDvoaWNsEtGJ.png"},
+ {url: "https://cadence.moe/friends/ooye_test/lHfmJpzgoNyNtYHdAmBHxXix.png", to: "test/res/lHfmJpzgoNyNtYHdAmBHxXix.png"},
+ {url: "https://cadence.moe/friends/ooye_test/MtRdXixoKjKKOyHJGWLsWLNU.png", to: "test/res/MtRdXixoKjKKOyHJGWLsWLNU.png"},
+ {url: "https://cadence.moe/friends/ooye_test/HXfFuougamkURPPMflTJRxGc.png", to: "test/res/HXfFuougamkURPPMflTJRxGc.png"},
+ {url: "https://cadence.moe/friends/ooye_test/ikYKbkhGhMERAuPPbsnQzZiX.png", to: "test/res/ikYKbkhGhMERAuPPbsnQzZiX.png"},
+ {url: "https://cadence.moe/friends/ooye_test/AYPpqXzVJvZdzMQJGjioIQBZ.png", to: "test/res/AYPpqXzVJvZdzMQJGjioIQBZ.png"},
+ {url: "https://cadence.moe/friends/ooye_test/UVuzvpVUhqjiueMxYXJiFEAj.png", to: "test/res/UVuzvpVUhqjiueMxYXJiFEAj.png"},
+ {url: "https://ezgif.com/images/format-demo/butterfly.gif", to: "test/res/butterfly.gif"},
+ {url: "https://ezgif.com/images/format-demo/butterfly.png", to: "test/res/butterfly.png"},
+ ])
+ }, {timeout: 60000})
+ }
/* c8 ignore stop */
const p = migrate.migrate(db)
@@ -131,41 +132,6 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
require("./addbot.test")
require("../src/db/orm.test")
require("../src/web/server.test")
- require("../src/discord/utils.test")
- require("../src/matrix/kstate.test")
- require("../src/matrix/api.test")
- require("../src/matrix/file.test")
- require("../src/matrix/mreq.test")
- require("../src/matrix/read-registration.test")
- require("../src/matrix/room-upgrade.test")
- require("../src/matrix/txnid.test")
- require("../src/matrix/utils.test")
- require("../src/d2m/actions/create-room.test")
- require("../src/d2m/actions/create-space.test")
- require("../src/d2m/actions/register-user.test")
- require("../src/d2m/converters/edit-to-changes.test")
- require("../src/d2m/converters/emoji-to-key.test")
- require("../src/d2m/converters/find-mentions.test")
- require("../src/d2m/converters/lottie.test")
- require("../src/d2m/converters/message-to-event.test")
- require("../src/d2m/converters/message-to-event.test.components")
- require("../src/d2m/converters/message-to-event.test.embeds")
- require("../src/d2m/converters/message-to-event.test.pk")
- require("../src/d2m/converters/pins-to-list.test")
- require("../src/d2m/converters/remove-member-mxids.test")
- require("../src/d2m/converters/remove-reaction.test")
- require("../src/d2m/converters/thread-to-announcement.test")
- require("../src/d2m/converters/user-to-mxid.test")
- require("../src/m2d/event-dispatcher.test")
- require("../src/m2d/converters/diff-pins.test")
- require("../src/m2d/converters/event-to-message.test")
- require("../src/m2d/converters/emoji.test")
- require("../src/m2d/converters/emoji-sheet.test")
- require("../src/discord/interactions/invite.test")
- require("../src/discord/interactions/matrix-info.test")
- require("../src/discord/interactions/permissions.test")
- require("../src/discord/interactions/privacy.test")
- require("../src/discord/interactions/reactions.test")
require("../src/web/routes/download-discord.test")
require("../src/web/routes/download-matrix.test")
require("../src/web/routes/guild.test")
@@ -173,7 +139,35 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
require("../src/web/routes/info.test")
require("../src/web/routes/link.test")
require("../src/web/routes/log-in-with-matrix.test")
- require("../src/web/routes/oauth.test")
- require("../src/web/routes/password.test")
- require("../src/agi/generator.test")
+ require("../src/discord/utils.test")
+ require("../src/matrix/kstate.test")
+ require("../src/matrix/api.test")
+ require("../src/matrix/file.test")
+ require("../src/matrix/mreq.test")
+ require("../src/matrix/read-registration.test")
+ require("../src/matrix/txnid.test")
+ require("../src/d2m/actions/create-room.test")
+ require("../src/d2m/actions/create-space.test")
+ require("../src/d2m/actions/register-user.test")
+ require("../src/d2m/converters/edit-to-changes.test")
+ require("../src/d2m/converters/emoji-to-key.test")
+ require("../src/d2m/converters/lottie.test")
+ require("../src/d2m/converters/message-to-event.test")
+ require("../src/d2m/converters/message-to-event.embeds.test")
+ require("../src/d2m/converters/message-to-event.pk.test")
+ require("../src/d2m/converters/pins-to-list.test")
+ require("../src/d2m/converters/remove-reaction.test")
+ require("../src/d2m/converters/thread-to-announcement.test")
+ require("../src/d2m/converters/user-to-mxid.test")
+ require("../src/m2d/event-dispatcher.test")
+ require("../src/m2d/converters/diff-pins.test")
+ require("../src/m2d/converters/event-to-message.test")
+ require("../src/m2d/converters/emoji.test")
+ require("../src/m2d/converters/utils.test")
+ require("../src/m2d/converters/emoji-sheet.test")
+ require("../src/discord/interactions/invite.test")
+ require("../src/discord/interactions/matrix-info.test")
+ require("../src/discord/interactions/permissions.test")
+ require("../src/discord/interactions/privacy.test")
+ require("../src/discord/interactions/reactions.test")
})()
diff --git a/test/web.js b/test/web.js
index 250694a..09af95b 100644
--- a/test/web.js
+++ b/test/web.js
@@ -51,7 +51,7 @@ class Router {
/**
* @param {string} method
* @param {string} inputUrl
- * @param {{event?: any, params?: any, body?: any, sessionData?: any, getOauth2Token?: any, getClient?: (string) => {user: {getGuilds: () => Promise}}, api?: Partial, snow?: {[k in keyof SnowTransfer]?: Partial}, createRoom?: Partial, createSpace?: Partial, mxcDownloader?: import("../src/m2d/actions/emoji-sheet")["getAndConvertEmoji"], headers?: any}} [options]
+ * @param {{event?: any, params?: any, body?: any, sessionData?: any, api?: Partial, snow?: {[k in keyof SnowTransfer]?: Partial}, createRoom?: Partial, createSpace?: Partial, headers?: any}} [options]
*/
async test(method, inputUrl, options = {}) {
const url = new URL(inputUrl, "http://a")
@@ -83,13 +83,10 @@ class Router {
},
context: {
api: options.api,
- mxcDownloader: options.mxcDownloader,
params: options.params,
snow: options.snow,
createRoom: options.createRoom,
createSpace: options.createSpace,
- getOauth2Token: options.getOauth2Token,
- getClient: options.getClient,
sessions: {
h3: {
id: "h3",