, 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: crazy html is all escaped", async t => { @@ -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,7 +124,7 @@ 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,12 +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'
- + ``, + 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": {} }]) }) @@ -299,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 7f77b81..93e120e 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -14,23 +14,20 @@ 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 => { @@ -39,8 +36,8 @@ function getDiscordParseCallbacks(message, guild, useHTML, spoilers = []) { const username = message.mentions?.find(ment => ment.id === node.id)?.username || message.referenced_message?.mentions?.find(ment => ment.id === node.id)?.username || (interaction?.user.id === node.id ? interaction.user.username : null) - || (message.author?.id === node.id ? message.author.username : null) - || "unknown-user" + || (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 {DiscordTypes.APIMessage} message
* @param {DiscordTypes.APIGuild} guild
@@ -273,7 +210,6 @@ function getFormattedInteraction(interaction, isThinkingInteraction) {
* @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 */
@@ -285,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.
@@ -335,8 +239,14 @@ async function messageToEvent(message, guild, options = {}, di) {
}
const interaction = message.interaction_metadata || message.interaction
- const isInteraction = message.type === DiscordTypes.MessageType.ChatInputCommand && !!interaction && "name" in interaction
- const isThinkingInteraction = isInteraction && !!((message.flags || 0) & DiscordTypes.MessageFlags.Loading)
+ 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
+ }
/**
@type {{room?: boolean, user_ids?: string[]}}
@@ -355,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
@@ -371,9 +280,9 @@ 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
}
@@ -385,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:
@@ -404,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
}
}
}
@@ -431,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.
@@ -470,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 && events.length === 0) { - const formattedInteraction = getFormattedInteraction(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) @@ -656,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 { @@ -711,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}` } @@ -727,42 +582,26 @@ async function messageToEvent(message, guild, options = {}, di) { events.push(...forwardedEvents) } - if (isThinkingInteraction) { - const formattedInteraction = getFormattedInteraction(interaction, true) - await addTextEvent(formattedInteraction.body, formattedInteraction.html, "m.notice") - } - // Then text content - if (message.content && !isOnlyKlipyGIF && !isThinkingInteraction) { + 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. - let content = message.content - if (options.scanTextForMentions !== false) { - const matches = [...content.matchAll(/(@ ?)([a-z0-9_.#$][^@\n]+)/gi)] - for (let i = matches.length; i--;) { - const m = matches[i] - const prefix = m[1] - const maximumWrittenSection = m[2].toLowerCase() - if (m.index > 0 && !content[m.index-1].match(/ |\(|\n/)) continue // must have space before it - if (maximumWrittenSection.match(/^everyone\b/) || maximumWrittenSection.match(/^here\b/)) continue // ignore @everyone/@here - - var roomID = roomID ?? select("channel_room", "room_id", {channel_id: message.channel_id}).pluck().get() - assert(roomID) - var pjr = pjr ?? findMentions.processJoined(Object.entries((await di.api.getJoinedMembers(roomID)).joined).map(([mxid, ev]) => ({mxid, displayname: ev.display_name}))) - - const found = findMentions.findMention(pjr, maximumWrittenSection, m.index, prefix, content) - if (found) { - addMention(found.mxid) - content = found.newContent + 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) } } } - // Scan the content for emojihax and replace them with real emojis - content = content.replaceAll(/\[([a-zA-Z0-9_-]{2,32})(?:~[0-9]+)?\]\(https:\/\/cdn\.discordapp\.com\/emojis\/([0-9]+)\.[^ \n)`]+\)/g, (_, name, id) => { - return `<:${name}:${id}>` - }) - - const {body, html} = await transformContent(content) + const {body, html} = await transformContent(message.content) await addTextEvent(body, html, msgtype) } @@ -811,125 +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. - let prev = events.at(-1) - for (const atch of attachmentEvents) { - if (atch.msgtype === "m.text" && prev?.body && prev?.formatted_body && ["m.text", "m.notice"].includes(prev?.msgtype)) { - prev.body = prev.body + "\n" + atch.body - prev.formatted_body = prev.formatted_body + "
${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) { - if (component.label) { - stack.msb.add(`[${component.label} ${component.url}] `, tag`${component.label} `) - } else { - stack.msb.add(component.url) - } - } - } - - // 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 @@ -943,43 +665,13 @@ 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() - 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") { if (embed.provider.url) { @@ -1076,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": { @@ -1086,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 1a73aea..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?"
📸 Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/123/456/SPOILER_secret.jpg (38 KB)` - + `
What's cooking, good looking?`, "m.mentions": {}, - msgtype: "m.text", + msgtype: "m.notice", }, { $type: "m.room.message", @@ -1361,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: { @@ -1378,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", @@ -1404,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" } ]) }) @@ -1499,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}, @@ -1549,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") @@ -1581,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 5a33c7c..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, part: 0}).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-reaction.js b/src/d2m/converters/remove-reaction.js index 4ca22b6..caa96d1 100644 --- a/src/d2m/converters/remove-reaction.js +++ b/src/d2m/converters/remove-reaction.js @@ -5,8 +5,8 @@ const DiscordTypes = require("discord-api-types/v10") const passthrough = require("../../passthrough") const {discord, sync, select} = passthrough -/** @type {import("../../matrix/utils")} */ -const utils = sync.require("../../matrix/utils") +/** @type {import("../../m2d/converters/utils")} */ +const utils = sync.require("../../m2d/converters/utils") /** * @typedef ReactionRemoveRequest diff --git a/src/d2m/converters/thread-to-announcement.js b/src/d2m/converters/thread-to-announcement.js index 575b3c5..98b8f12 100644 --- a/src/d2m/converters/thread-to-announcement.js +++ b/src/d2m/converters/thread-to-announcement.js @@ -4,8 +4,8 @@ const assert = require("assert").strict const passthrough = require("../../passthrough") const {discord, sync, db, select} = passthrough -/** @type {import("../../matrix/utils")} */ -const mxUtils = sync.require("../../matrix/utils") +/** @type {import("../../m2d/converters/utils")} */ +const mxUtils = sync.require("../../m2d/converters/utils") const {reg} = require("../../matrix/read-registration.js") const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex)) diff --git a/src/d2m/converters/thread-to-announcement.test.js b/src/d2m/converters/thread-to-announcement.test.js index 3286f62..3d5d1eb 100644 --- a/src/d2m/converters/thread-to-announcement.test.js +++ b/src/d2m/converters/thread-to-announcement.test.js @@ -2,7 +2,6 @@ const {test} = require("supertape") const {threadToAnnouncement} = require("./thread-to-announcement") const data = require("../../../test/data") const Ty = require("../../types") -const {mockGetEffectivePower} = require("../../matrix/utils.test") /** * @param {string} roomID @@ -31,7 +30,13 @@ function mockGetEvent(t, roomID_in, eventID_in, outer) { } const viaApi = { - getEffectivePower: mockGetEffectivePower(), + async getStateEvent(roomID, type, key) { + return { + users: { + "@_ooye_bot:cadence.moe": 100 + } + } + }, async getJoinedMembers(roomID) { return { joined: { diff --git a/src/d2m/converters/user-to-mxid.js b/src/d2m/converters/user-to-mxid.js index 7705aff..e0ab137 100644 --- a/src/d2m/converters/user-to-mxid.js +++ b/src/d2m/converters/user-to-mxid.js @@ -2,7 +2,6 @@ const assert = require("assert") const {reg} = require("../../matrix/read-registration") -const Ty = require("../../types") const passthrough = require("../../passthrough") const {select} = passthrough @@ -14,16 +13,16 @@ const SPECIAL_USER_MAPPINGS = new Map([ /** * Downcased and stripped username. Can only include a basic set of characters. * https://spec.matrix.org/v1.6/appendices/#user-identifiers - * @param {import("discord-api-types/v10").APIUser | Ty.WebhookAuthor} user + * @param {import("discord-api-types/v10").APIUser} user * @returns {string} localpart */ function downcaseUsername(user) { // First, try to convert the username to the set of allowed characters let downcased = user.username.toLowerCase() - // spaces and slashes to underscores... - .replace(/[ /]/g, "_") + // spaces to underscores... + .replace(/ /g, "_") // remove disallowed characters... - .replace(/[^a-z0-9._=-]*/g, "") + .replace(/[^a-z0-9._=/-]*/g, "") // remove leading and trailing dashes and underscores... .replace(/(?:^[_-]*|[_-]*$)/g, "") // If requested, also make the Discord user ID part of the username @@ -86,49 +85,4 @@ function userToSimName(user) { throw new Error(`Ran out of suggestions when generating sim name. downcased: "${downcased}"`) } -/** - * Webhooks have an ID specific to that webhook, but a single webhook can send messages with any user name. - * The point of this feature (gated by guild_space webhook_profile) is to create persistent Matrix accounts for individual webhook "users". - * This is convenient when using a bridge to a platform that does not assign persistent user IDs (e.g. IRC, Minecraft). - * In this case, webhook "users" are disambiguated by their username (downcased). - * @param {Ty.WebhookAuthor} author - * @returns {string} - */ -function webhookAuthorToFakeUserID(author) { - const downcased = downcaseUsername(author) - return `webhook_${downcased}` -} - -function isWebhookUserID(userID) { - return userID.match(/^webhook_[a-z90-9._=/-]+$/) -} - -/** - * @param {Ty.WebhookAuthor} author - * @returns {string} - */ -function webhookAuthorToSimName(author) { - assert(!SPECIAL_USER_MAPPINGS.has(author.id), "Special users should have followed the other code path.") - - // 1. Is sim user already registered? - const fakeUserID = webhookAuthorToFakeUserID(author) - const existing = select("sim", "user_id", {user_id: fakeUserID}).pluck().get() - assert.equal(existing, null, "Shouldn't try to create a new name for an existing sim") - - // 2. Register based on username (could be new or old format) - const downcased = "webhook_" + downcaseUsername(author) - - // Check for conflicts with already registered sims - const matches = select("sim", "sim_name", {}, "WHERE sim_name LIKE ? ESCAPE '@'").pluck().all(downcased + "%") - // Keep generating until we get a suggestion that doesn't conflict - for (const suggestion of generateLocalpartAlternatives([downcased])) { - if (!matches.includes(suggestion)) return suggestion - } - /* c8 ignore next */ - throw new Error(`Ran out of suggestions when generating sim name. downcased: "${downcased}"`) -} - module.exports.userToSimName = userToSimName -module.exports.webhookAuthorToFakeUserID = webhookAuthorToFakeUserID -module.exports.webhookAuthorToSimName = webhookAuthorToSimName -module.exports.isWebhookUserID = isWebhookUserID diff --git a/src/d2m/converters/user-to-mxid.test.js b/src/d2m/converters/user-to-mxid.test.js index f8cf16a..86f151b 100644 --- a/src/d2m/converters/user-to-mxid.test.js +++ b/src/d2m/converters/user-to-mxid.test.js @@ -2,7 +2,7 @@ const {test} = require("supertape") const tryToCatch = require("try-to-catch") const assert = require("assert") const data = require("../../../test/data") -const {userToSimName, webhookAuthorToSimName} = require("./user-to-mxid") +const {userToSimName} = require("./user-to-mxid") test("user2name: cannot create user for a webhook", async t => { const [error] = await tryToCatch(() => userToSimName({discriminator: "0000"})) @@ -21,12 +21,8 @@ test("user2name: works on single emoji at the end", t => { t.equal(userToSimName({username: "Melody 🎵", discriminator: "2192"}), "melody") }) -test("user2name: works on really weird name", t => { - t.equal(userToSimName({username: "*** D3 &W (89) _7//-", discriminator: "0001"}), "d3_w_89__7") -}) - -test("user2name: treats slashes", t => { - t.equal(userToSimName({username: "Evil Lillith (she/her)", discriminator: "5892"}), "evil_lillith_she_her") +test("user2name: works on crazy name", t => { + t.equal(userToSimName({username: "*** D3 &W (89) _7//-", discriminator: "0001"}), "d3_w_89__7//") }) test("user2name: adds discriminator if name is unavailable (old tag format)", t => { @@ -49,17 +45,10 @@ test("user2name: works on special user", t => { t.equal(userToSimName(data.user.clyde_ai), "clyde_ai") }) -test("webhook author: can generate sim names", t => { - t.equal(webhookAuthorToSimName({ - username: "Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆", - avatar: null, - id: "123" - }), "webhook_cadence_maid_of_creation_eye_of_clarity_empress_of_hope") -}) - test("user2name: includes ID if requested in config", t => { const {reg} = require("../../matrix/read-registration") reg.ooye.include_user_id_in_mxid = true t.equal(userToSimName({username: "Harry Styles!", discriminator: "0001", id: "123456"}), "123456_harry_styles") - t.equal(userToSimName({username: "f***", discriminator: "0001", id: "123456"}), "123456_f") + t.equal(userToSimName({username: "f***", discriminator: "0001", id: "123456"}), "123456_f") + reg.ooye.include_user_id_in_mxid = false }) diff --git a/src/d2m/discord-client.js b/src/d2m/discord-client.js index 7b0fcf8..b05d48f 100644 --- a/src/d2m/discord-client.js +++ b/src/d2m/discord-client.js @@ -20,19 +20,17 @@ class DiscordClient { * @param {string} listen "full", "half", "no" - whether to set up the event listeners for OOYE to operate */ constructor(discordToken, listen = "full") { - /** @type {import("cloudstorm").IClientOptions["intents"]} */ - const intents = [ - "DIRECT_MESSAGES", "DIRECT_MESSAGE_REACTIONS", "DIRECT_MESSAGE_TYPING", - "GUILDS", "GUILD_EMOJIS_AND_STICKERS", "GUILD_MESSAGES", "GUILD_MESSAGE_REACTIONS", "GUILD_MESSAGE_TYPING", "GUILD_WEBHOOKS", "GUILD_MESSAGE_POLLS", - "MESSAGE_CONTENT" - ] - if (reg.ooye.receive_presences !== false) intents.push("GUILD_PRESENCES") this.discordToken = discordToken this.snow = new SnowTransfer(discordToken) this.cloud = new CloudStorm(discordToken, { shards: [0], + reconnect: true, snowtransferInstance: this.snow, - intents, + intents: [ + "DIRECT_MESSAGES", "DIRECT_MESSAGE_REACTIONS", "DIRECT_MESSAGE_TYPING", + "GUILDS", "GUILD_EMOJIS_AND_STICKERS", "GUILD_MESSAGES", "GUILD_MESSAGE_REACTIONS", "GUILD_MESSAGE_TYPING", "GUILD_WEBHOOKS", + "MESSAGE_CONTENT", "GUILD_PRESENCES" + ], ws: { compress: false, encoding: "json" diff --git a/src/d2m/discord-packets.js b/src/d2m/discord-packets.js index 8cf2fde..017d50e 100644 --- a/src/d2m/discord-packets.js +++ b/src/d2m/discord-packets.js @@ -6,6 +6,10 @@ const DiscordTypes = require("discord-api-types/v10") const passthrough = require("../passthrough") const {sync, db} = passthrough +function populateGuildID(guildID, channelID) { + db.prepare("UPDATE channel_room SET guild_id = ? WHERE channel_id = ?").run(guildID, channelID) +} + const utils = { /** * @param {import("./discord-client")} client @@ -37,17 +41,18 @@ const utils = { channel.guild_id = message.d.id arr.push(channel.id) client.channels.set(channel.id, channel) + populateGuildID(message.d.id, channel.id) } for (const thread of message.d.threads || []) { // @ts-ignore thread.guild_id = message.d.id arr.push(thread.id) client.channels.set(thread.id, thread) + populateGuildID(message.d.id, thread.id) } if (listen === "full") { try { - interactions.registerInteractions() await eventDispatcher.checkMissedExpressions(message.d) await eventDispatcher.checkMissedPins(client, message.d) await eventDispatcher.checkMissedMessages(client, message.d) @@ -107,6 +112,7 @@ const utils = { } else if (message.t === "THREAD_CREATE") { client.channels.set(message.d.id, message.d) if (message.d["guild_id"]) { + populateGuildID(message.d["guild_id"], message.d.id) const channels = client.guildChannelMap.get(message.d["guild_id"]) if (channels && !channels.includes(message.d.id)) channels.push(message.d.id) } @@ -134,6 +140,7 @@ const utils = { } else if (message.t === "CHANNEL_CREATE") { client.channels.set(message.d.id, message.d) if (message.d["guild_id"]) { // obj[prop] notation can be used to access a property without typescript complaining that it doesn't exist on all values something can have + populateGuildID(message.d["guild_id"], message.d.id) const channels = client.guildChannelMap.get(message.d["guild_id"]) if (channels && !channels.includes(message.d.id)) channels.push(message.d.id) } diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index 01bbc67..1698317 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -32,12 +32,8 @@ const speedbump = sync.require("./actions/speedbump") const retrigger = sync.require("./actions/retrigger") /** @type {import("./actions/set-presence")} */ const setPresence = sync.require("./actions/set-presence") -/** @type {import("./actions/poll-vote")} */ -const vote = sync.require("./actions/poll-vote") /** @type {import("../m2d/event-dispatcher")} */ const matrixEventDispatcher = sync.require("../m2d/event-dispatcher") -/** @type {import("../discord/interactions/matrix-info")} */ -const matrixInfoInteraction = sync.require("../discord/interactions/matrix-info") const {Semaphore} = require("@chriscdn/promise-semaphore") const checkMissedPinsSema = new Semaphore() @@ -51,15 +47,13 @@ module.exports = { * @param {import("cloudstorm").IGatewayMessage} gatewayMessage */ async onError(client, e, gatewayMessage) { - if (gatewayMessage.t === "TYPING_START") return - - matrixEventDispatcher.printError(gatewayMessage.t, "Discord", e, gatewayMessage) - const channelID = gatewayMessage.d["channel_id"] if (!channelID) return const roomID = select("channel_room", "room_id", {channel_id: channelID}).pluck().get() if (!roomID) return + if (gatewayMessage.t === "TYPING_START") return + await matrixEventDispatcher.sendError(roomID, "Discord", gatewayMessage.t, e, gatewayMessage) }, @@ -73,7 +67,7 @@ module.exports = { async checkMissedMessages(client, guild) { if (guild.unavailable) return const bridgedChannels = select("channel_room", "channel_id").pluck().all() - const preparedExists = from("message_room").join("historical_channel_room", "historical_room_index").pluck("message_id").and("WHERE reference_channel_id = ? LIMIT 1").prepare() + const preparedExists = db.prepare("SELECT channel_id FROM message_channel WHERE channel_id = ? LIMIT 1") const preparedGet = select("event_message", "event_id", {}, "WHERE message_id = ?").pluck() /** @type {(DiscordTypes.APIChannel & {type: DiscordTypes.GuildChannelType})[]} */ let channels = [] @@ -93,7 +87,7 @@ module.exports = { const member = guild.members.find(m => m.user?.id === client.user.id) if (!member) return if (!("permission_overwrites" in channel)) continue - const permissions = dUtils.getPermissions(guild.id, member.roles, guild.roles, client.user.id, channel.permission_overwrites) + const permissions = dUtils.getPermissions(member.roles, guild.roles, client.user.id, channel.permission_overwrites) if (!dUtils.hasAllPermissions(permissions, ["ViewChannel", "ReadMessageHistory"])) continue // We don't have permission to look back in this channel /** More recent messages come first. */ @@ -152,7 +146,7 @@ module.exports = { const lastPin = updatePins.convertTimestamp(channel.last_pin_timestamp) // Permissions check - const permissions = dUtils.getPermissions(guild.id, member.roles, guild.roles, client.user.id, channel.permission_overwrites) + const permissions = dUtils.getPermissions(member.roles, guild.roles, client.user.id, channel.permission_overwrites) if (!dUtils.hasAllPermissions(permissions, ["ViewChannel", "ReadMessageHistory"])) continue // We don't have permission to look up the pins in this channel const row = select("channel_room", ["room_id", "last_bridged_pin_timestamp"], {channel_id: channel.id}).get() @@ -196,21 +190,6 @@ module.exports = { await createSpace.syncSpace(guild) }, - /** - * @param {import("./discord-client")} client - * @param {DiscordTypes.GatewayGuildRoleUpdateDispatchData} data - */ - async GUILD_ROLE_UPDATE(client, data) { - const guild = client.guilds.get(data.guild_id) - if (!guild) return - const spaceID = select("guild_space", "space_id", {guild_id: data.guild_id}).pluck().get() - if (!spaceID) return - - if (data.role.id === data.guild_id) { // @everyone role changed - find a way to do this more efficiently in the future to handle many role updates - await createSpace.syncSpaceFully(guild) - } - }, - /** * @param {import("./discord-client")} client * @param {DiscordTypes.GatewayChannelUpdateDispatchData} channelOrThread @@ -250,7 +229,7 @@ module.exports = { const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get() if (!roomID) return // channel wasn't being bridged in the first place // @ts-ignore - await createRoom.unbridgeChannel(channel, guildID) + await createRoom.unbridgeDeletedChannel(channel, guildID) }, /** @@ -291,7 +270,12 @@ module.exports = { // Based on looking at data they've sent me over the gateway, this is the best way to check for meaningful changes. // If the message content is a string then it includes all interesting fields and is meaningful. // Otherwise, if there are embeds, then the system generated URL preview embeds. - if (!(typeof data.content === "string" || "embeds" in data || "components" in data)) return + if (!(typeof data.content === "string" || "embeds" in data)) return + + if (data.webhook_id) { + const row = select("webhook", "webhook_id", {webhook_id: data.webhook_id}).pluck().get() + if (row) return // The message was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it. + } if (dUtils.isEphemeralMessage(data)) return // Ephemeral messages are for the eyes of the receiver only! @@ -299,10 +283,8 @@ module.exports = { const {affected, row} = await speedbump.maybeDoSpeedbump(data.channel_id, data.id) if (affected) return - if (!row) { - // Check that the sending-to room exists, and deal with Eventual Consistency(TM) - if (retrigger.eventNotFoundThenRetrigger(data.id, module.exports.MESSAGE_UPDATE, client, data)) return - } + // Check that the sending-to room exists, and deal with Eventual Consistency(TM) + if (retrigger.eventNotFoundThenRetrigger(data.id, module.exports.MESSAGE_UPDATE, client, data)) return /** @type {DiscordTypes.GatewayMessageCreateDispatchData} */ // @ts-ignore @@ -322,16 +304,7 @@ module.exports = { */ async MESSAGE_REACTION_ADD(client, data) { if (data.user_id === client.user.id) return // m2d reactions are added by the discord bot user - do not reflect them back to matrix. - if (data.emoji.name === "❓" && select("event_message", "message_id", {message_id: data.message_id, source: 0, part: 0}).get()) { // source 0 = matrix - const guild_id = data.guild_id ?? client.channels.get(data.channel_id)?.["guild_id"] - await Promise.all([ - client.snow.channel.deleteReaction(data.channel_id, data.message_id, data.emoji.name).catch(() => {}), - // @ts-ignore - this is all you need for it to do a matrix-side lookup - matrixInfoInteraction.dm({guild_id, data: {target_id: data.message_id}, member: {user: {id: data.user_id}}}) - ]) - } else { - await addReaction.addReaction(data) - } + await addReaction.addReaction(data) }, /** @@ -391,20 +364,6 @@ module.exports = { await createSpace.syncSpaceExpressions(data, false) }, - /** - * @param {import("./discord-client")} client - * @param {DiscordTypes.GatewayMessagePollVoteDispatchData} data - */ - async MESSAGE_POLL_VOTE_ADD(client, data) { - if (retrigger.eventNotFoundThenRetrigger(data.message_id, module.exports.MESSAGE_POLL_VOTE_ADD, client, data)) return - await vote.addVote(data) - }, - - async MESSAGE_POLL_VOTE_REMOVE(client, data) { - if (retrigger.eventNotFoundThenRetrigger(data.message_id, module.exports.MESSAGE_POLL_VOTE_REMOVE, client, data)) return - await vote.removeVote(data) - }, - /** * @param {import("./discord-client")} client * @param {DiscordTypes.GatewayPresenceUpdateDispatchData} data diff --git a/src/db/migrations/0017-analyze.sql b/src/db/migrations/0017-analyze.sql new file mode 100644 index 0000000..802fca2 --- /dev/null +++ b/src/db/migrations/0017-analyze.sql @@ -0,0 +1,225 @@ +-- https://www.sqlite.org/lang_analyze.html + +BEGIN TRANSACTION; + +ANALYZE sqlite_schema; + +DELETE FROM "sqlite_stat1"; +INSERT INTO "sqlite_stat1" ("tbl","idx","stat") VALUES ('sim','sim','625 1'), + ('reaction','reaction','3242 1'), + ('channel_room','channel_room','389 1'), + ('channel_room','sqlite_autoindex_channel_room_1','389 1'), + ('media_proxy','media_proxy','5068 1'), + ('sim_proxy','sim_proxy','36 1'), + ('webhook','webhook','155 1'), + ('member_cache','member_cache','784 3 1'), + ('member_power','member_power','1 1 1'), + ('file','file','21862 1'), + ('message_channel','message_channel','366884 1'), + ('lottie','lottie','19 1'), + ('event_message','event_message','382920 1 1'), + ('migration',NULL,'1'), + ('sim_member','sim_member','2871 7 1'), + ('guild_space','guild_space','32 1'), + ('guild_active','guild_active','34 1'), + ('emoji','emoji','2563 1'), + ('auto_emoji','auto_emoji','3 1'); + +DELETE FROM "sqlite_stat4"; +INSERT INTO "sqlite_stat4" ("tbl","idx","neq","nlt","ndlt","sample") VALUES ('sim','sim','1','69','69',X'0231313137363631373038303932333039353039'), + ('sim','sim','1','139','139',X'0231313530383936363934333439373931323332'), + ('sim','sim','1','209','209',X'0231323231383737363334373737323139303732'), + ('sim','sim','1','279','279',X'0231333039313431353735353334313136383636'), + ('sim','sim','1','349','349',X'0231333935343433383235363034313635363434'), + ('sim','sim','1','419','419',X'0231353335363239373830383338353134373030'), + ('sim','sim','1','489','489',X'0231363930333339333730353930363636383034'), + ('sim','sim','1','559','559',X'0231383535353736303637393137323137383133'), + ('reaction','reaction','1','360','360',X'020699d5faceefb5fb4f'), + ('reaction','reaction','1','721','721',X'0206b61095e98b6b2fb1'), + ('reaction','reaction','1','1082','1082',X'0206d1dcb418603a5eaa'), + ('reaction','reaction','1','1443','1443',X'0206ef9fc42b9df746ad'), + ('reaction','reaction','1','1804','1804',X'02060f38c1f98f130605'), + ('reaction','reaction','1','2165','2165',X'02062b53df6dab7b1067'), + ('reaction','reaction','1','2526','2526',X'020645dd7e7f60c4aac7'), + ('reaction','reaction','1','2887','2887',X'0206658d2fe735805979'), + ('channel_room','channel_room','1','43','43',X'023331313434393131333330393139333231363330'), + ('channel_room','channel_room','1','87','87',X'023331313835343033343830303934303335393738'), + ('channel_room','channel_room','1','131','131',X'023331323139353036353836343139303638393839'), + ('channel_room','channel_room','1','175','175',X'023331323336353538333034323331303334393630'), + ('channel_room','channel_room','1','219','219',X'023331323933373932323135333930323234343235'), + ('channel_room','channel_room','1','263','263',X'023331333333323139363936393333323038303937'), + ('channel_room','channel_room','1','307','307',X'0231343835363635393733363433333738363938'), + ('channel_room','channel_room','1','351','351',X'0231373039303432313039353632323234363731'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','6 6','6 6',X'034b3321416a6c4c49464e6248646474424a6d4d73503a636164656e63652e6d6f6531313531333434383735363139343833373233'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','34 34','34 34',X'034b3321474b4a63424a6b527a47634e4855686c50613a636164656e63652e6d6f6531303237393433323532323237323630343637'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','43 43','43 43',X'034b3121484b50534d62736d694673506d6268414f513a636164656e63652e6d6f65313931343837343839393433343034353434'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','58 58','58 58',X'034b33214a4479425a685545706874784f6e6f6569513a636164656e63652e6d6f6531323937323836373434353633313236333532'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','87 87','87 87',X'034b33214e544d724e686e715271695755654d494d523a636164656e63652e6d6f6531323235323434353738393939373031353536'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','108 108','108 108',X'034b332151444e44796656674e7657565345656876713a636164656e63652e6d6f6531313432333134303935353535363435343830'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','131 131','131 131',X'034b3121544171536b575752654b43506f584c6a75483a636164656e63652e6d6f65383737303730363531343733363631393532'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','175 175','175 175',X'034b3321594249486864714e697255585941587845563a636164656e63652e6d6f6531323335303831373939353936373639333730'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','177 177','177 177',X'034b3321594b46454e79716667696951686956496b533a636164656e63652e6d6f6531323934363237303431343530333933373034'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','186 186','186 186',X'034b3321596f54644f55766a53765349767266716c653a636164656e63652e6d6f6531323734313936373733383435333430323933'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','202 202','202 202',X'034b3121625877616673695372655647676470535a463a636164656e63652e6d6f65373339303137363739373936343336393932'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','208 208','208 208',X'034b3321634a4b6843764943795377717a47634551423a636164656e63652e6d6f6531323732363632303331323238373331343834'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','219 219','219 219',X'034b3121656455786a56647a6755765844554951434b3a636164656e63652e6d6f65343937313631333530393334353630373738'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','242 242','242 242',X'034b31216a4d746e6e6f51414e4278466a486458494d3a636164656e63652e6d6f65373634353135323932303539323731313939'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','263 263','263 263',X'034b31216c7a776870666a5a6e59797468656a7453483a636164656e63652e6d6f65383838343831373132383438343030343534'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','264 264','264 264',X'034b33216d454c5846716a426958726d7558796943723a636164656e63652e6d6f6531313936393134373631303430393234373432'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','268 268','268 268',X'034b33216d557765577571546761574a767769576a653a636164656e63652e6d6f6531323936373131393236333032333830303834'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','291 291','291 291',X'034b3321704761494e45534643587a634e42497a724e3a636164656e63652e6d6f6531303237343531333333333533313532353232'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','306 306','306 306',X'034b3321717646656248564f4b6876454e54494563763a636164656e63652e6d6f6531323737373238383139323232303230313436'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','307 307','307 307',X'034b332171767370666d716f476449634a66794c506c3a636164656e63652e6d6f6531323936393138333638393539343633343735'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','351 351','351 351',X'034b3321774e7a7741724a47796f4c5168426f544e4b3a636164656e63652e6d6f6531303238303436373930333435333739383930'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','368 368','368 368',X'034b3321794e6d504c7765654a69756570725a677a733a636164656e63652e6d6f6531323531393631373233373731313632363234'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','376 376','376 376',X'034b3121796a4879795772466f704c66646878564e423a636164656e63652e6d6f65333336313537353037303734353233313336'), + ('channel_room','sqlite_autoindex_channel_room_1','1 1','379 379','379 379',X'034b31217a4c4f6b62766b44587551465948594555673a636164656e63652e6d6f65393933383838313433373030393330363331'), + ('media_proxy','media_proxy','1','563','563',X'02069e6054680b610946'), + ('media_proxy','media_proxy','1','1127','1127',X'0206bb489b717c9320e4'), + ('media_proxy','media_proxy','1','1691','1691',X'0206d75f602775b7a27c'), + ('media_proxy','media_proxy','1','2255','2255',X'0206f2c705ddca4e2b14'), + ('media_proxy','media_proxy','1','2819','2819',X'02061060db7a5151967b'), + ('media_proxy','media_proxy','1','3383','3383',X'02062cc47366f7550d22'), + ('media_proxy','media_proxy','1','3947','3947',X'020647d275ec0d781fc7'), + ('media_proxy','media_proxy','1','4511','4511',X'02066402024a7ea38249'), + ('sim_proxy','sim_proxy','1','4','4',X'025531316564343731342d636635652d346333372d393331382d376136353266383732636634'), + ('sim_proxy','sim_proxy','1','9','9',X'025533346636333932642d323263372d346337382d393063372d326536323734313535613266'), + ('sim_proxy','sim_proxy','1','14','14',X'025535396662363131392d626133392d346565382d393738612d386432376366303631393633'), + ('sim_proxy','sim_proxy','1','19','19',X'025539373066366536332d646234632d346531342d383063362d336639343938643961363665'), + ('sim_proxy','sim_proxy','1','24','24',X'025561636231613335642d313336662d343362332d626365622d326566646634616265306436'), + ('sim_proxy','sim_proxy','1','29','29',X'025563316635623735392d336136342d343633342d623634632d643461656436316539656632'), + ('sim_proxy','sim_proxy','1','34','34',X'025566323230373135632d633436332d343532622d626233612d373662646662306365353537'), + ('webhook','webhook','1','17','17',X'023331313532383834313435343038373230393936'), + ('webhook','webhook','1','35','35',X'023331313939303936333434333830343631313138'), + ('webhook','webhook','1','53','53',X'023331323331383036353337373032353736313938'), + ('webhook','webhook','1','71','71',X'023331323933373836383939343238383036363536'), + ('webhook','webhook','1','89','89',X'023331333132363031353130363535353537373132'), + ('webhook','webhook','1','107','107',X'0231323937323734313733303636333133373339'), + ('webhook','webhook','1','125','125',X'0231353239313736313536333938363832313137'), + ('webhook','webhook','1','143','143',X'0231363837303238373334333232313437333434'), + ('member_cache','member_cache','4 1','73 74','48 74',X'034b3921496f4866536e67625a6762747061747a494e3a636164656e63652e6d6f65406875636b6c65746f6e3a636164656e63652e6d6f65'), + ('member_cache','member_cache','2 1','86 87','57 87',X'034b2d214b5169714663546e764f6f4f424475746a7a3a636164656e63652e6d6f6540726e6c3a636164656e63652e6d6f65'), + ('member_cache','member_cache','4 1','101 104','68 104',X'034b43214e446249714e704a795076664b526e4e63723a636164656e63652e6d6f6540776f756e6465645f77617272696f723a6d61747269782e6f7267'), + ('member_cache','member_cache','4 1','110 113','73 113',X'034b3b214f485844457370624d485348716c4445614f3a636164656e63652e6d6f6540717561647261646963616c3a6d61747269782e6f7267'), + ('member_cache','member_cache','5 1','171 175','111 175',X'034b3b215450616f6a5454444446444847776c7276743a636164656e63652e6d6f6540766962656973766572796f3a6d61747269782e6f7267'), + ('member_cache','member_cache','39 1','180 208','116 208',X'034b4d2154716c79516d69667847556767456d64424e3a636164656e63652e6d6f6540726f626c6b796f6772653a6372616674696e67636f6d72616465732e6e6574'), + ('member_cache','member_cache','4 1','231 231','126 231',X'034b3b2156624f77675559777146614e4c5345644e413a636164656e63652e6d6f654061666c6f7765723a73796e646963617465642e676179'), + ('member_cache','member_cache','9 1','262 263','141 263',X'034b3b21594b46454e79716667696951686956496b533a636164656e63652e6d6f654062656e6d61633a636861742e62656e6d61632e78797a'), + ('member_cache','member_cache','3 1','283 283','149 283',X'034b35215a615a4d78456f52724d6d4e49554d79446c3a636164656e63652e6d6f6540636164656e63653a636164656e63652e6d6f65'), + ('member_cache','member_cache','88 1','307 351','166 351',X'034b3b2163427874565278446c5a765356684a58564b3a636164656e63652e6d6f65406a61736b617274683a736c656570696e672e746f776e'), + ('member_cache','member_cache','11 1','408 415','177 415',X'034b5121654856655270706e6c6f57587177704a6e553a636164656e63652e6d6f65406a61636b736f6e6368656e3636363a6a61636b736f6e6368656e3636362e636f6d'), + ('member_cache','member_cache','7 1','423 424','181 424',X'034b4b2165724f7079584e465a486a48724568784e583a636164656e63652e6d6f6540616d796973636f6f6c7a3a6d61747269782e6174697573616d792e636f6d'), + ('member_cache','member_cache','96 1','436 439','187 439',X'034b4b21676865544b5a7451666c444e7070684c49673a636164656e63652e6d6f6540616c65783a73706163652e67616d65727374617665726e2e6f6e6c696e65'), + ('member_cache','member_cache','96 1','436 527','187 527',X'034b3121676865544b5a7451666c444e7070684c49673a636164656e63652e6d6f654078796c6f626f6c3a616d6265722e74656c'), + ('member_cache','member_cache','10 1','546 555','197 555',X'0351312169537958674e7851634575586f587073536e3a707573737468656361742e6f726740797562697175653a6e6f70652e63686174'), + ('member_cache','member_cache','13 1','594 601','224 601',X'034b2b216c7570486a715444537a774f744d59476d493a636164656e63652e6d6f6540656c6c69753a68617368692e7265'), + ('member_cache','member_cache','2 1','614 615','229 615',X'034b2f216d584978494644676c4861734e53427371773a636164656e63652e6d6f654077696e673a666561746865722e6f6e6c'), + ('member_cache','member_cache','4 1','616 619','230 619',X'034b2f216d616767455367755a427147425a74536e723a636164656e63652e6d6f654077696e673a666561746865722e6f6e6c'), + ('member_cache','member_cache','4 1','659 660','259 660',X'034b332172454f73706e5971644f414c4149466e69563a636164656e63652e6d6f6540656c797369613a636164656e63652e6d6f65'), + ('member_cache','member_cache','4 1','699 701','284 701',X'034b3521766e717a56767678534a586c5a504f5276533a636164656e63652e6d6f6540636164656e63653a636164656e63652e6d6f65'), + ('member_cache','member_cache','1 1','703 703','285 703',X'034b3521767165714c474851616842464a56566779483a636164656e63652e6d6f654063696465723a6361746769726c2e636c6f7564'), + ('member_cache','member_cache','4 1','705 705','287 705',X'034b35217750454472596b77497a6f744e66706e57503a636164656e63652e6d6f6540636164656e63653a636164656e63652e6d6f65'), + ('member_cache','member_cache','35 1','709 709','288 709',X'034b2d2177574f667376757356486f4e4e567242585a3a636164656e63652e6d6f654061613a6361747669626572732e6d65'), + ('member_cache','member_cache','14 1','747 749','291 749',X'034b3721776c534544496a44676c486d42474b7254703a636164656e63652e6d6f654062616461746e616d65733a62616461742e646576'), + ('member_power','member_power','1 1','0 0','0 0',X'03350f40636164656e63653a636164656e63652e6d6f652a'), + ('file','file','1','2429','2429',X'03815f68747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f313039393033313838373530303033343038382f313333313336303134333238333036303833372f50584c5f32303235303132315f3230323934323137372e6a7067'), + ('file','file','1','4859','4859',X'03817568747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f313134353832313533383832323637323436362f313330323232393131303834373936373331352f53637265656e73686f745f32303234313130325f3034313332365f5265646469742e6a7067'), + ('file','file','1','7289','7289',X'03815968747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f313231393439383932363436363636323433302f313239373634363930353038353636313234362f494d475f32303234313032305f3135323230302e6a7067'), + ('file','file','1','9719','9719',X'03814168747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f3135393136353731343139343735393638302f313236383537333933363531343337313635392f494d475f353433362e6a7067'), + ('file','file','1','12149','12149',X'03813b68747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f3236363736373539303634313233383032372f313237323430333931313939383730313630392f696d6167652e706e67'), + ('file','file','1','14579','14579',X'03816b68747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f3539383730363933323736303434343936392f313237373532343532343330383632373437372f45585445524e414c5f454449545f323032345f4d5f64726166745f322e646f6378'), + ('file','file','1','17009','17009',X'03815768747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f3635353231363137333639363238363734362f313333333630333132383634313036303938342f323032352d30312d32375f31372e30312e31352e706e67'), + ('file','file','1','19439','19439',X'027f68747470733a2f2f63646e2e646973636f72646170702e636f6d2f656d6f6a69732f313230323936303730343936313931323836322e706e67'), + ('message_channel','message_channel','1','40764','40764',X'023331313630333434353733303030383232383735'), + ('message_channel','message_channel','1','81529','81529',X'023331313830323437393130343238393837343333'), + ('message_channel','message_channel','1','122294','122294',X'023331313938303533383732383337363131363230'), + ('message_channel','message_channel','1','163059','163059',X'023331323237373739373330333839303738303537'), + ('message_channel','message_channel','1','203824','203824',X'023331323437303438333039303031313538373137'), + ('message_channel','message_channel','1','244589','244589',X'023331323635363939353734333034303830303034'), + ('message_channel','message_channel','1','285354','285354',X'023331323835343637363238323434313732383234'), + ('message_channel','message_channel','1','326119','326119',X'023331333038373932333935313031333736353732'), + ('lottie','lottie','1','2','2',X'0231373439303534363630373639323138363331'), + ('lottie','lottie','1','5','5',X'0231373534313037353339323030363731373635'), + ('lottie','lottie','1','8','8',X'0231373936313430363338303933343433303932'), + ('lottie','lottie','1','11','11',X'0231373936313431373032363935343835353030'), + ('lottie','lottie','1','14','14',X'0231383136303837373932323931323832393434'), + ('lottie','lottie','1','17','17',X'0231383233393736313032393736323930383636'), + ('event_message','event_message','11 1','14788 14796','14356 14796',X'03336531313532303033373639343137303839303434246d44714a474b3530424a715170394273684e534d64365f3768494354776e70362d793130786d6669766563'), + ('event_message','event_message','11 1','33806 33815','32914 33815',X'033365313135373630333638373035373836363736322459526f7139484b376e55677a397668796f6e3053424a49497978497a5750734e4f6e39756f765644664d45'), + ('event_message','event_message','1 1','42546 42546','41335 42546',X'03336531313630363236383737353835363938393237245033436e4f6d6a35462d6939454e4e79586b70796f5679306b74324a654464764276326e746d33566a4355'), + ('event_message','event_message','2 1','85093 85093','81999 85093',X'0333653131383039393939363531323930343831303424496748666562784533746e623047412d534f7176594a354e4e55385735706c68523159676854636a554734'), + ('event_message','event_message','11 1','116157 116165','111525 116165',X'03336531313933363837333730363137333237363536245a32305734766c737079566e387a6e2d526a6f64694a51745f5a7851644f5f33744e415169453755356477'), + ('event_message','event_message','1 1','127640 127640','122328 127640',X'0333653132303132353637313733373633303332323624702d626f415672476a4b45327a7158664f3738387a5a597a376a42624648717431334d386464705467476f'), + ('event_message','event_message','16 1','140270 140281','134379 140281',X'03336531323039333735363534373138343830343235246c374a4f7543526b7756306d627a69356e5843496b4538416a6951374f67473455456c2d7053445a516649'), + ('event_message','event_message','11 1','162065 162071','154933 162071',X'0333653132323434383135393033313937373537373424674c77513179796e4b6d5859496b5a597a4a55627a66557a55552d714c4b5f524f454e4250325f6e44766b'), + ('event_message','event_message','1 1','170187 170187','162530 170187',X'0333653132323937373533363839343532373038333424656c6c76416a544a5847627936767249767470677a555572787231716f5a75536e50525f474b4e35455945'), + ('event_message','event_message','11 1','178736 178736','170762 178736',X'03336531323333353238303533323338333337353537242d39304668552d36455373594b6435484d7237666d6a414a5f6a576149616e356c4776384e655436564959'), + ('event_message','event_message','10 1','180317 180325','172228 180325',X'03336531323334363237303030393333383130323637246a4679416449665a4f54432d2d735971715472473735374c7a50504f34386439657963485477686d797751'), + ('event_message','event_message','1 1','212734 212734','203278 212734',X'03336531323438303131373930303633373637363734246557493950456f4d576b614b4d33416a7030782d6d455f6c4b4a6c495f6d6d44686f6379464b5170534f59'), + ('event_message','event_message','11 1','228100 228103','217856 228103',X'033365313235333734353736363733373132313336322447326a7a746e5977716a3676304a7a4168576e30725950596470667a5f496f76426771574a4876434c516b'), + ('event_message','event_message','11 1','240172 240181','229123 240181',X'033365313235393239383538323233303630313735382472635679454c5a76647453547a37366f3669434a4f614a316a756f683835535778494d546c5072517a676f'), + ('event_message','event_message','10 1','240259 240264','229132 240264',X'0333653132353933303030343035333535373235323024535849376a465f696e71424d714c4f564b347659746852644b31724747502d435a5f355a354f6b774e3751'), + ('event_message','event_message','2 1','255280 255281','243230 255281',X'03336531323636373837343338353137343234313738246e6e4e576f526a54495757723441463770625454746b7a73784c4d336b312d6d6645665031496b43586e59'), + ('event_message','event_message','1 1','297828 297828','283436 297828',X'0333653132383637303937353334363834323032313024544342465970356f39767a2d4f6b2d33654f4e433772426d354966615934476b48536e58445257474f4767'), + ('event_message','event_message','1 1','340375 340375','323489 340375',X'033365313330393336363936343530353934303030392431664536386d2d50546e786d5474345458584c35754847594139353779396c76582d50797150496d395f30'), + ('event_message','event_message','11 1','343791 343799','326730 343799',X'03336531333131353731333337393331363537323737246731767241315269725951592d3933304f6973587142372d6961686e34684e492d6462374952714176616b'), + ('event_message','event_message','10 1','363207 363216','344868 363216',X'0333653133323434393732323633393438393834393524784f78636e4749364269545941734d7a4f7557316f526e68356b675378544e30466442386a3037695f4f67'), + ('event_message','event_message','10 1','363219 363219','344871 363219',X'033365313332343439373532363733323039393732352432586f7938567937643843375a47767472657966326b4c4f4159397546386b4755364c3642435f446b6d77'), + ('event_message','event_message','10 1','363340 363343','344918 363343',X'03336531333234353037313732333634373530393830244f46645731396649534176706d34646c36367a2d5543337236432d436354506e34752d63444f7036733345'), + ('event_message','event_message','11 1','369452 369456','350712 369456',X'0333653133323736353831313733343439323336383024574d774330644574417277375f4562554a534465546532577174506d3747584347774570646c4f79326d30'), + ('event_message','event_message','10 1','372353 372356','353425 372356',X'0333653133323933313737353039313234353035373224366259364d313472667163486d5854476349716d4a4366467471796839794472375a6a487463715a6d4f34'), + ('sim_member','sim_member','225 1','0 12','0 12',X'034b4721414956694e775a64636b4652764c4f4567433a636164656e63652e6d6f65405f6f6f79655f616b6972615f6e6965723a636164656e63652e6d6f65'), + ('sim_member','sim_member','2 1','319 319','14 319',X'034b43214456706f6e54524d56456570486378744c423a636164656e63652e6d6f65405f6f6f79655f656e746f6c6f6d613a636164656e63652e6d6f65'), + ('sim_member','sim_member','68 1','391 440','26 440',X'034b4921457a54624a496c496d45534f746b4e644e4a3a636164656e63652e6d6f65405f6f6f79655f73617475726461797465643a636164656e63652e6d6f65'), + ('sim_member','sim_member','8 1','638 639','59 639',X'034b3f21497a4f675169446e757346516977796d614c3a636164656e63652e6d6f65405f6f6f79655f636f6f6b69653a636164656e63652e6d6f65'), + ('sim_member','sim_member','31 1','743 771','86 771',X'034b49214d5071594e414a62576b72474f544a7461703a636164656e63652e6d6f65405f6f6f79655f746865666f6f6c323239343a636164656e63652e6d6f65'), + ('sim_member','sim_member','26 1','774 787','87 787',X'034b49214d687950614b4250506f496c7365794d6d743a636164656e63652e6d6f65405f6f6f79655f6b79757567727970686f6e3a636164656e63652e6d6f65'), + ('sim_member','sim_member','27 1','877 881','104 881',X'034b45215063734371724f466a48476f41424270414c3a636164656e63652e6d6f65405f6f6f79655f62696c6c795f626f623a636164656e63652e6d6f65'), + ('sim_member','sim_member','16 1','956 959','117 959',X'034b43215158526f4a777a63506d5047546d454b454d3a636164656e63652e6d6f65405f6f6f79655f626f7472616334723a636164656e63652e6d6f65'), + ('sim_member','sim_member','32 1','997 1012','121 1012',X'034b3f215170676c734e587a4c7751594d4c6c734f503a636164656e63652e6d6f65405f6f6f79655f6a75746f6d693a636164656e63652e6d6f65'), + ('sim_member','sim_member','7 1','1274 1279','157 1279',X'034b4121554d6f6e68556765644d47585a78466658753a636164656e63652e6d6f65405f6f6f79655f6d696e696d75733a636164656e63652e6d6f65'), + ('sim_member','sim_member','27 1','1415 1439','188 1439',X'034b3d21595868717249786d586e47736961796a59783a636164656e63652e6d6f65405f6f6f79655f73746161663a636164656e63652e6d6f65'), + ('sim_member','sim_member','16 1','1597 1599','217 1599',X'034b512163466a4479477274466d48796d794c6652453a636164656e63652e6d6f65405f6f6f79655f626f6a61636b5f686f7273656d616e3a636164656e63652e6d6f65'), + ('sim_member','sim_member','27 1','1758 1761','248 1761',X'034b3d2168665a74624d656f5355564e424850736a743a636164656e63652e6d6f65405f6f6f79655f617a7572653a636164656e63652e6d6f65'), + ('sim_member','sim_member','25 1','1865 1886','270 1886',X'034b3f216b4c52714b4b555158636962494d744f706c3a636164656e63652e6d6f65405f6f6f79655f7361796f72693a636164656e63652e6d6f65'), + ('sim_member','sim_member','19 1','1918 1919','276 1919',X'034b3d216b68497350756c465369736d43646c596e493a636164656e63652e6d6f65405f6f6f79655f617a7572653a636164656e63652e6d6f65'), + ('sim_member','sim_member','33 1','1986 2015','286 2015',X'034b3d216d5451744d736a534c4f646c576f7265594d3a636164656e63652e6d6f65405f6f6f79655f73746161663a636164656e63652e6d6f65'), + ('sim_member','sim_member','37 1','2027 2028','289 2028',X'034b4d216d616767455367755a427147425a74536e723a636164656e63652e6d6f65405f6f6f79655f2e7265616c2e706572736f6e2e3a636164656e63652e6d6f65'), + ('sim_member','sim_member','28 1','2117 2130','297 2130',X'034b3f216e4e595a794b6f4e70797859417a50466f733a636164656e63652e6d6f65405f6f6f79655f6a75746f6d693a636164656e63652e6d6f65'), + ('sim_member','sim_member','20 1','2230 2239','310 2239',X'034b41217046504c7270594879487a784e4c69594b413a636164656e63652e6d6f65405f6f6f79655f686578676f61743a636164656e63652e6d6f65'), + ('sim_member','sim_member','30 1','2381 2398','332 2398',X'034b3b21717a44626c4b6c69444c577a52524f6e465a3a636164656e63652e6d6f65405f6f6f79655f6d6e696b3a636164656e63652e6d6f65'), + ('sim_member','sim_member','38 1','2490 2518','344 2518',X'034b3d2173445250714549546e4f4e57474176496b423a636164656e63652e6d6f65405f6f6f79655f727974686d3a636164656e63652e6d6f65'), + ('sim_member','sim_member','11 1','2555 2559','358 2559',X'034b4321746751436d526b426e6474516362687150583a636164656e63652e6d6f65405f6f6f79655f6a6f7365707065793a636164656e63652e6d6f65'), + ('sim_member','sim_member','47 1','2633 2666','377 2666',X'034b4b217750454472596b77497a6f744e66706e57503a636164656e63652e6d6f65405f6f6f79655f6e61706f6c656f6e333038393a636164656e63652e6d6f65'), + ('sim_member','sim_member','52 1','2817 2837','414 2837',X'034b43217a66654e574d744b4f764f48766f727979563a636164656e63652e6d6f65405f6f6f79655f696e736f676e69613a636164656e63652e6d6f65'), + ('guild_space','guild_space','1','3','3',X'0231313132373630363639313738323431303234'), + ('guild_space','guild_space','1','7','7',X'023331313534383638343234373234343633363837'), + ('guild_space','guild_space','1','11','11',X'023331323139303338323637383430393235383138'), + ('guild_space','guild_space','1','15','15',X'023331323839353939363232353930383930313335'), + ('guild_space','guild_space','1','19','19',X'0231323733383737363437323234393935383431'), + ('guild_space','guild_space','1','23','23',X'0231353239313736313536333938363832313135'), + ('guild_space','guild_space','1','27','27',X'0231373535303134333534373334313533383138'), + ('guild_space','guild_space','1','31','31',X'0231393933383838313432343535323130303834'), + ('guild_active','guild_active','1','3','3',X'0231313132373630363639313738323431303234'), + ('guild_active','guild_active','1','7','7',X'023331313534383638343234373234343633363837'), + ('guild_active','guild_active','1','11','11',X'023331323139303338323637383430393235383138'), + ('guild_active','guild_active','1','15','15',X'023331323839353939363232353930383930313335'), + ('guild_active','guild_active','1','19','19',X'023331333333323139363936393333323038303934'), + ('guild_active','guild_active','1','23','23',X'0231343735353939303338353336373434393630'), + ('guild_active','guild_active','1','27','27',X'022f3636313932393535373737343836383438'), + ('guild_active','guild_active','1','31','31',X'0231383737303635303431393930353136373637'), + ('emoji','emoji','1','284','284',X'023331313132323031303430303637303339333332'), + ('emoji','emoji','1','569','569',X'023331323334393032303131393436383630363638'), + ('emoji','emoji','1','854','854',X'0231323735313734373438353034313935303732'), + ('emoji','emoji','1','1139','1139',X'0231333837343730383630303134393737303235'), + ('emoji','emoji','1','1424','1424',X'0231353435363639393734303130383838313932'), + ('emoji','emoji','1','1709','1709',X'0231363339313034333030393139393437323634'), + ('emoji','emoji','1','1994','1994',X'0231373532363932363230303638373832313231'), + ('emoji','emoji','1','2279','2279',X'0231383935343737353331303434363138323430'), + ('auto_emoji','auto_emoji','1','0','0',X'02114c31'), + ('auto_emoji','auto_emoji','1','1','1',X'02114c32'), + ('auto_emoji','auto_emoji','1','2','2',X'020f5f'); + +ANALYZE sqlite_schema; + +COMMIT; diff --git a/src/db/migrations/0025-add-webhook-profile-to-guild-space.sql b/src/db/migrations/0025-add-webhook-profile-to-guild-space.sql deleted file mode 100644 index e629fb8..0000000 --- a/src/db/migrations/0025-add-webhook-profile-to-guild-space.sql +++ /dev/null @@ -1,5 +0,0 @@ -BEGIN TRANSACTION; - -ALTER TABLE guild_space ADD COLUMN webhook_profile INTEGER NOT NULL DEFAULT 0; - -COMMIT; diff --git a/src/db/migrations/0026-make-rooms-historical.sql b/src/db/migrations/0026-make-rooms-historical.sql deleted file mode 100644 index ba4775e..0000000 --- a/src/db/migrations/0026-make-rooms-historical.sql +++ /dev/null @@ -1,63 +0,0 @@ -PRAGMA foreign_keys=OFF; -BEGIN TRANSACTION; - --- *** historical_channel_room *** - -CREATE TABLE "historical_channel_room" ( - "historical_room_index" INTEGER NOT NULL, - "reference_channel_id" TEXT NOT NULL, - "room_id" TEXT NOT NULL UNIQUE, - "upgraded_timestamp" INTEGER NOT NULL, - PRIMARY KEY("historical_room_index" AUTOINCREMENT), - FOREIGN KEY("reference_channel_id") REFERENCES "channel_room"("channel_id") ON DELETE CASCADE -); - -INSERT INTO historical_channel_room (reference_channel_id, room_id, upgraded_timestamp) SELECT channel_id, room_id, 0 FROM channel_room; - --- *** message_channel -> message_room *** - -CREATE TABLE "message_room" ( - "message_id" TEXT NOT NULL, - "historical_room_index" INTEGER NOT NULL, - PRIMARY KEY("message_id"), - FOREIGN KEY("historical_room_index") REFERENCES "historical_channel_room"("historical_room_index") ON DELETE CASCADE -) WITHOUT ROWID; -INSERT INTO message_room (message_id, historical_room_index) SELECT message_id, max(historical_room_index) as historical_room_index FROM message_channel INNER JOIN historical_channel_room ON historical_channel_room.reference_channel_id = message_channel.channel_id GROUP BY message_id; - --- *** event_message *** - -CREATE TABLE "new_event_message" ( - "event_id" TEXT NOT NULL, - "event_type" TEXT, - "event_subtype" TEXT, - "message_id" TEXT NOT NULL, - "part" INTEGER NOT NULL, - "reaction_part" INTEGER NOT NULL, - "source" INTEGER NOT NULL, - PRIMARY KEY("message_id","event_id"), - FOREIGN KEY("message_id") REFERENCES "message_room"("message_id") ON DELETE CASCADE -) WITHOUT ROWID; -INSERT INTO new_event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) SELECT event_id, event_type, event_subtype, message_id, part, reaction_part, source from event_message; -DROP TABLE event_message; -ALTER TABLE new_event_message RENAME TO event_message; - --- *** reaction *** - -CREATE TABLE "new_reaction" ( - "hashed_event_id" INTEGER NOT NULL, - "message_id" TEXT NOT NULL, - "encoded_emoji" TEXT NOT NULL, original_encoding TEXT, - PRIMARY KEY("hashed_event_id"), - FOREIGN KEY("message_id") REFERENCES "message_room"("message_id") ON DELETE CASCADE -) WITHOUT ROWID; -INSERT INTO new_reaction (hashed_event_id, message_id, encoded_emoji) SELECT hashed_event_id, message_id, encoded_emoji FROM reaction; -DROP TABLE reaction; -ALTER TABLE new_reaction RENAME TO reaction; - --- *** - -DROP TABLE message_channel; -PRAGMA foreign_key_check; - -COMMIT; -PRAGMA foreign_keys=ON; diff --git a/src/db/migrations/0027-analyze.sql b/src/db/migrations/0027-analyze.sql deleted file mode 100644 index f66e0c1..0000000 --- a/src/db/migrations/0027-analyze.sql +++ /dev/null @@ -1,250 +0,0 @@ --- https://www.sqlite.org/lang_analyze.html - -BEGIN TRANSACTION; - -ANALYZE sqlite_schema; - -DELETE FROM "sqlite_stat1"; -INSERT INTO "sqlite_stat1" ("tbl","idx","stat") VALUES ('reaction','reaction','4741 1'), -('event_message','event_message','537386 1 1'), -('message_room','message_room','510262 1'), -('historical_channel_room','sqlite_autoindex_historical_channel_room_1','991 1'), -('auto_emoji','auto_emoji','2 1'), -('sim','sim','1075 1'), -('webhook','webhook','205 1'), -('channel_room','channel_room','992 1'), -('channel_room','sqlite_autoindex_channel_room_1','992 1'), -('guild_active','guild_active','45 1'), -('media_proxy','media_proxy','19794 1'), -('sim_member','sim_member','5504 6 1'), -('emoji','emoji','3472 1'), -('guild_space','guild_space','43 1'), -('member_power','member_power','1 1 1'), -('sim_proxy','sim_proxy','213 1'), -('migration',NULL,'1'), -('member_cache','member_cache','1117 3 1'), -('file','file','36489 1'), -('lottie','lottie','22 1'); - -DELETE FROM "sqlite_stat4"; -INSERT INTO "sqlite_stat4" ("tbl","idx","neq","nlt","ndlt","sample") VALUES ('reaction','reaction','1','526','526',X'02069c21bd28f26ae025'), - ('reaction','reaction','1','1053','1053',X'0206b8866f4c30c2e1aa'), - ('reaction','reaction','1','1580','1580',X'0206d43fceca129b040e'), - ('reaction','reaction','1','2107','2107',X'0206f121f9a4fe54b557'), - ('reaction','reaction','1','2634','2634',X'020610299199abbd0e9c'), - ('reaction','reaction','1','3161','3161',X'02062be99961e7716037'), - ('reaction','reaction','1','3688','3688',X'020647b48fa5ee5a415c'), - ('reaction','reaction','1','4215','4215',X'020664fdc2d88c77dda3'), - ('event_message','event_message','11 1','14790 14792','14356 14792',X'03336531313532303033373639343137303839303434244a616d4c6d732d4b77454c6b47766866344d524f385576535536336a574a5a4c4474524c4c57664f775873'), - ('event_message','event_message','11 1','33809 33816','32914 33816',X'0333653131353736303336383730353738363637363224544b7141734f58566c6e67506f546f4a427565514e664444756d494a6d38384f486a76766f7949496e7130'), - ('event_message','event_message','1 1','59709 59709','57896 59709',X'033365313136363930353332323132303637353336392442794756564f6767326a416845624267463941755056486178377a34314459514e4459316e34435a4a4455'), - ('event_message','event_message','11 1','116172 116182','111525 116182',X'0333653131393336383733373036313733323736353624786a385f70696e784f624f4349666c70556832305542345973664a547642694b4164675f473168562d5334'), - ('event_message','event_message','1 1','119419 119419','114559 119419',X'03336531313935323132333038393839383730313732244f556670664d5054576c364774734943484d725459556d6464656c636232663374494a662d425769554355'), - ('event_message','event_message','16 1','140286 140287','134379 140287',X'0333653132303933373536353437313834383034323524346b61796e4d68422d336d6967417571347255745f726639353454636b6f657636664c5f3675394f455030'), - ('event_message','event_message','11 1','162080 162086','154932 162086',X'0333653132323434383135393033313937373537373424674c77513179796e4b6d5859496b5a597a4a55627a66557a55552d714c4b5f524f454e4250325f6e44766b'), - ('event_message','event_message','11 1','178659 178659','170672 178659',X'03336531323333353238303533323338333337353537242d39304668552d36455373594b6435484d7237666d6a414a5f6a576149616e356c4776384e655436564959'), - ('event_message','event_message','1 1','179129 179129','171083 179129',X'03336531323333393533373032373637383836343636245a446e5f42385a6b41674c645939495649767445516e47373369706a555a55447943634768697851673859'), - ('event_message','event_message','10 1','180049 180052','171954 180052',X'03336531323334363237303030393333383130323637245171504b7357795254734a49695449744646716a686e506d48764a6e5932584a6c595a506b424e372d766f'), - ('event_message','event_message','11 1','215266 215271','205302 215271',X'03336531323533373435373636373337313231333632244b4936672d57724f5a5757533463534c4c4f353950555176425066754b5f5446504b443233583130504759'), - ('event_message','event_message','11 1','224498 224499','213831 224499',X'0333653132353932393835383232333036303137353824356a573361764d37626d643661756c7367635650506f5257417552476e30503477324939786b5675326f6b'), - ('event_message','event_message','10 1','224519 224523','213833 224523',X'033365313235393239383739353234323635353839352452696c715a6862347a32526b594c596958504375445975546f6b6430544e365a784638737842745670346b'), - ('event_message','event_message','10 1','224615 224616','213843 224616',X'0333653132353933303036363636373831383139323024425a69396d4c73323034344c674a6e56673761557a614467484b4b5545787334587a467954474245585573'), - ('event_message','event_message','1 1','238839 238839','227061 238839',X'0333653132363839343934383836303535393336343224374d3633546d416c526947553847795f416164576f4d4f4e4a334b363441326235385f6e72385961652d51'), - ('event_message','event_message','1 1','298549 298549','283096 298549',X'03336531333037303536353132313931303337343532245830424a3954514e544d3041687554736c7258744b5836383376723749524355747a4b47524a4374493555'), - ('event_message','event_message','11 1','304605 304605','288785 304605',X'03336531333131353731333337393331363537323737242d674e75657465765a426169587949335859717437325743695438396549573269514761416266384f6455'), - ('event_message','event_message','11 1','327028 327037','309699 327037',X'0333653133323736353831313733343439323336383024715055786a61394c36694e756548683046335962304b524b67665730414356394769367a4147464b714973'), - ('event_message','event_message','10 1','329549 329550','312055 329550',X'033365313332393331373735303931323435303537322430325a4779526f33656133786e5356706b52487047325459415464373971684834536632506f4e7a614773'), - ('event_message','event_message','1 1','358259 358259','339179 358259',X'03336531333436303136333531313138313634303539243364757343667558596a506f715a3642774851755a48496e5163504f4e70766c64387476654a4d45685a38'), - ('event_message','event_message','1 1','417969 417969','395237 417969',X'0333653133363831333832343230383333393336363724537a7775656948304b696130376d67304e51322d58627751352d6a7653507649464e645053396464416655'), - ('event_message','event_message','11 1','422263 422270','399248 422270',X'033365313336393833343930353236313034373831382456754f5872464d593547734350377467425f6a763348486f426264666b3859464c4b4f6e48583732497677'), - ('event_message','event_message','10 1','424260 424266','401135 424266',X'03336531333730353132353138353938303939303637246c7268447950715458362d45497a3637552d616a75453839614655394c4151556f5a356d7363725072466f'), - ('event_message','event_message','1 1','477679 477679','451062 477679',X'0333653134303434353430323035313234383133333324524f454b6b5f726b3373344b7451337a75344552774c4b5069484964757676575f514d4b4e66306c385630'), - ('message_room','message_room','1','56695','56695',X'023331313636313031373337333834343630333739'), - ('message_room','message_room','1','113391','113391',X'023331313932353935303036363435363132353434'), - ('message_room','message_room','1','170087','170087',X'023331323331393439393133373937393535363335'), - ('message_room','message_room','1','226783','226783',X'023331323636313430343634333733383239373532'), - ('message_room','message_room','1','283479','283479',X'023331333034303933383132373833373130323539'), - ('message_room','message_room','1','340175','340175',X'023331333434383431363637333537393730343332'), - ('message_room','message_room','1','396871','396871',X'023331333637353035313132313333363638393134'), - ('message_room','message_room','1','453567','453567',X'023331343032363934353234333439373134343833'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','0 0','0 0',X'034b0221414355774c616c64303030303030303030303a636164656e63652e6d6f650288'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','24 24','24 24',X'034b02214255635a694c7a57303030303030303030303a636164656e63652e6d6f6501ab'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','110 110','110 110',X'034b022147486e4d47697875303030303030303030303a636164656e63652e6d6f6500c6'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','193 193','193 193',X'034b02214b4b535575717666303030303030303030303a636164656e63652e6d6f650350'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','221 221','221 221',X'034b02214c51715351594b73303030303030303030303a636164656e63652e6d6f6503af'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','319 319','319 319',X'034b02215170676c734e587a303030303030303030303a636164656e63652e6d6f650366'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','332 332','332 332',X'034b0221525a585a7064554f303030303030303030303a636164656e63652e6d6f65009f'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','351 351','351 351',X'034b0221534b6f6c6f636b77303030303030303030303a636164656e63652e6d6f65035f'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','443 443','443 443',X'034b0221576374435a494d73303030303030303030303a636164656e63652e6d6f650084'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','551 551','551 551',X'034b0221637779454c6c6b55303030303030303030303a636164656e63652e6d6f6501b0'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','554 554','554 554',X'034b0221644965496d615167303030303030303030303a636164656e63652e6d6f6503a0'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','560 560','560 560',X'034b0221645568456f756a71303030303030303030303a636164656e63652e6d6f650090'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','573 573','573 573',X'034b02216552517465644b67303030303030303030303a636164656e63652e6d6f650099'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','593 593','593 593',X'034b0221666764594e526d4e303030303030303030303a636164656e63652e6d6f65016b'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','624 624','624 624',X'034b0221687078416c4c6f71303030303030303030303a636164656e63652e6d6f650297'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','625 625','625 625',X'034b02216873414570464e47303030303030303030303a636164656e63652e6d6f6500be'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','665 665','665 665',X'034b01216a71484b51424476303030303030303030303a636164656e63652e6d6f653b'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','758 758','758 758',X'034b02216f6251554d424b75303030303030303030303a636164656e63652e6d6f6500f7'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','776 776','776 776',X'034b022170566e596b5a4f46303030303030303030303a636164656e63652e6d6f650232'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','781 781','781 781',X'034b01217065766e6542516e303030303030303030303a636164656e63652e6d6f6518'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','857 857','857 857',X'034b02217446564c65724b78303030303030303030303a636164656e63652e6d6f65024e'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','866 866','866 866',X'034b0221745a61474145557a303030303030303030303a636164656e63652e6d6f6501f8'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','887 887','887 887',X'034b022175727a464b754d61303030303030303030303a636164656e63652e6d6f65033b'), - ('historical_channel_room','sqlite_autoindex_historical_channel_room_1','1 1','921 921','921 921',X'034b022177574a5548445a74303030303030303030303a636164656e63652e6d6f65025c'), - ('auto_emoji','auto_emoji','1','0','0',X'02114c31'), - ('auto_emoji','auto_emoji','1','1','1',X'02114c32'), - ('sim','sim','1','119','119',X'025531316564343731342d636635652d346333372d393331382d376136353266383732636634'), - ('sim','sim','1','239','239',X'0231313439363932303632313634333230323536'), - ('sim','sim','1','359','359',X'025532323533323035312d633335332d346638662d383835362d653137383831323435303763'), - ('sim','sim','1','479','479',X'0231333036373839323436333237353836383136'), - ('sim','sim','1','599','599',X'0231343132383438323830343635313738363235'), - ('sim','sim','1','719','719',X'0231353638323430303837363238373735343234'), - ('sim','sim','1','839','839',X'0231373234383037393132373233313835373534'), - ('sim','sim','1','959','959',X'0231393431303333313033353936353835303630'), - ('webhook','webhook','1','22','22',X'023331313630383933333337303239353836393536'), - ('webhook','webhook','1','45','45',X'023331323139343938393236343636363632343330'), - ('webhook','webhook','1','68','68',X'023331323432383939363632343734373131303630'), - ('webhook','webhook','1','91','91',X'023331323937323836383730393534323833313533'), - ('webhook','webhook','1','114','114',X'023331333430353438363133363931393332373133'), - ('webhook','webhook','1','137','137',X'023331343034313334383236303530383436393331'), - ('webhook','webhook','1','160','160',X'0231333639373535303430343638303431373238'), - ('webhook','webhook','1','183','183',X'0231363035353930343336333230333738383930'), - ('channel_room','channel_room','1','110','110',X'023331313939353030313137393834363733393133'), - ('channel_room','channel_room','1','221','221',X'023331323734313935333432323131393430353434'), - ('channel_room','channel_room','1','332','332',X'023331333437303036333637393639343433383430'), - ('channel_room','channel_room','1','443','443',X'023331343035323432323838343138303632333636'), - ('channel_room','channel_room','1','554','554',X'023331343036373736363630393936333935323830'), - ('channel_room','channel_room','1','665','665',X'023331343039363536363537383835323635393830'), - ('channel_room','channel_room','1','776','776',X'023331343139353132333134363234383638343632'), - ('channel_room','channel_room','1','887','887',X'0231333734383732393736313738343133353639'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','23 23','23 23',X'034b3121425167434a4d4c78303030303030303030303a636164656e63652e6d6f65393631373335333036303032393732373432'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','96 96','96 96',X'034b332146514f654f667747303030303030303030303a636164656e63652e6d6f6531323137393638383531303939313839323738'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','110 110','110 110',X'034b332147486e4d47697875303030303030303030303a636164656e63652e6d6f6531323432323436333730303938363739383838'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','138 138','138 138',X'034b3321484a79705a6b6863303030303030303030303a636164656e63652e6d6f6531303237323933303239313633333335373130'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','161 161','161 161',X'034b33214962646466626172303030303030303030303a636164656e63652e6d6f6531323937373538303931373331303039353536'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','221 221','221 221',X'034b31214c51715351594b73303030303030303030303a636164656e63652e6d6f65373039303431393733353332363838343235'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','240 240','240 240',X'034b33214d5071594e414a62303030303030303030303a636164656e63652e6d6f6531323139303338323638323835323539393037'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','250 250','250 250',X'034b33214e414f484c4e444c303030303030303030303a636164656e63652e6d6f6531343037323332343832313338333934363634'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','325 325','325 325',X'034b33215178576669464359303030303030303030303a636164656e63652e6d6f6531343034353739343736363837323934363434'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','332 332','332 332',X'034b33215254735654767542303030303030303030303a636164656e63652e6d6f6531343037323235393932313935333432343237'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','430 430','430 430',X'034b33215673656a6b6b5a71303030303030303030303a636164656e63652e6d6f6531323235323636343030363838333431303833'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','443 443','443 443',X'034b3321576241744a736c6b303030303030303030303a636164656e63652e6d6f6531343230323635333433363931313332393339'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','552 552','552 552',X'034b3321637779454c6c6b55303030303030303030303a636164656e63652e6d6f6531343034393538363332363830303939393931'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','554 554','554 554',X'034b332164484e5378484a47303030303030303030303a636164656e63652e6d6f6531343035363439333331343335393939323932'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','565 565','565 565',X'034b3321646c584f50766944303030303030303030303a636164656e63652e6d6f6531323735353037363433323231343039393033'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','579 579','579 579',X'034b332165656c6c7a6a5370303030303030303030303a636164656e63652e6d6f6531343036373736363630393936333935323830'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','619 619','619 619',X'034b332168525179596e6d4e303030303030303030303a636164656e63652e6d6f6531343237323832333338303035353136343233'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','664 664','664 664',X'034b33216a6c566e54585747303030303030303030303a636164656e63652e6d6f6531323139353034373636333137373536343238'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','665 665','665 665',X'034b33216a6c6c4479666d76303030303030303030303a636164656e63652e6d6f6531303835303935353736383731333837313936'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','776 776','776 776',X'034b332170555653686b7978303030303030303030303a636164656e63652e6d6f6531323139343939353736363137303738383735'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','813 813','813 813',X'034b33217179416246555961303030303030303030303a636164656e63652e6d6f6531343035393130313436363731393732333833'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','887 887','887 887',X'034b33217571697357484575303030303030303030303a636164656e63652e6d6f6531323331383036353337373032353736313938'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','924 924','924 924',X'034b332177585242634d4851303030303030303030303a636164656e63652e6d6f6531343233373338343430363833363232353332'), - ('channel_room','sqlite_autoindex_channel_room_1','1 1','953 953','953 953',X'034b33217954757a6749556f303030303030303030303a636164656e63652e6d6f6531333338353537373531373232323530333130'), - ('guild_active','guild_active','1','5','5',X'023331313433333336323438373631363437313534'), - ('guild_active','guild_active','1','11','11',X'023331313630383933333336333234393331353834'), - ('guild_active','guild_active','1','17','17',X'023331323839353936343835343631323137333430'), - ('guild_active','guild_active','1','23','23',X'023331333338363530383035363233393834333030'), - ('guild_active','guild_active','1','29','29',X'023331343338363132393630393137353836313233'), - ('guild_active','guild_active','1','35','35',X'0231343937313539373236343535343535373534'), - ('guild_active','guild_active','1','41','41',X'0231383730313138363530373638363730373530'), - ('media_proxy','media_proxy','1','2199','2199',X'02069cb7709d83b92e22'), - ('media_proxy','media_proxy','1','4399','4399',X'0206b953cc685f0b68d2'), - ('media_proxy','media_proxy','1','6599','6599',X'0206d546a2d00310b6cc'), - ('media_proxy','media_proxy','1','8799','8799',X'0206f0d029ff71e1dae5'), - ('media_proxy','media_proxy','1','10999','10999',X'02060e4626697605710f'), - ('media_proxy','media_proxy','1','13199','13199',X'02062adc53c43825bc39'), - ('media_proxy','media_proxy','1','15399','15399',X'02064704c4b0f76fa5ff'), - ('media_proxy','media_proxy','1','17599','17599',X'02066338ce2423770613'), - ('sim_member','sim_member','225 1','14 80','4 80',X'034b4721414956694e775a64303030303030303030303a636164656e63652e6d6f65405f6f6f79655f66726f73745f313139323a636164656e63652e6d6f65'), - ('sim_member','sim_member','32 1','483 488','68 488',X'034b3b21455450534d664d69303030303030303030303a636164656e63652e6d6f65405f6f6f79655f653372613a636164656e63652e6d6f65'), - ('sim_member','sim_member','125 1','598 611','85 611',X'034b4921457a54624a496c49303030303030303030303a636164656e63652e6d6f65405f6f6f79655f61726a756e3034323236393a636164656e63652e6d6f65'), - ('sim_member','sim_member','35 1','818 851','107 851',X'034b472147486e4d47697875303030303030303030303a636164656e63652e6d6f65405f6f6f79655f76616e746164656c69613a636164656e63652e6d6f65'), - ('sim_member','sim_member','63 1','945 1005','141 1005',X'034b412148725979716b6f79303030303030303030303a636164656e63652e6d6f65405f6f6f79655f7669686f776c733a636164656e63652e6d6f65'), - ('sim_member','sim_member','48 1','1024 1025','149 1025',X'034b47214943566475566c64303030303030303030303a636164656e63652e6d6f65405f6f6f79655f5f706b5f6172656866723a636164656e63652e6d6f65'), - ('sim_member','sim_member','39 1','1205 1223','175 1223',X'034b41214a48614a71425870303030303030303030303a636164656e63652e6d6f65405f6f6f79655f6c6f6f6e656c613a636164656e63652e6d6f65'), - ('sim_member','sim_member','48 1','1734 1768','289 1768',X'034b47215074796952785161303030303030303030303a636164656e63652e6d6f65405f6f6f79655f6d6f6d6f7473756b692e3a636164656e63652e6d6f65'), - ('sim_member','sim_member','5 1','1832 1835','299 1835',X'034b4b2151544372636e6953303030303030303030303a636164656e63652e6d6f65405f6f6f79655f72616e646f6d6974796775793a636164656e63652e6d6f65'), - ('sim_member','sim_member','64 1','2097 2100','353 2100',X'034b4521536c7664497a734f303030303030303030303a636164656e63652e6d6f65405f6f6f79655f5f706b5f626369736c3a636164656e63652e6d6f65'), - ('sim_member','sim_member','81 1','2213 2240','361 2240',X'034b4721544f61794476734c303030303030303030303a636164656e63652e6d6f65405f6f6f79655f5f706b5f6f7a707a79633a636164656e63652e6d6f65'), - ('sim_member','sim_member','42 1','2368 2409','373 2409',X'034b49215468436b4b585743303030303030303030303a636164656e63652e6d6f65405f6f6f79655f776172736d6974686c69763a636164656e63652e6d6f65'), - ('sim_member','sim_member','36 1','2422 2447','380 2447',X'034b4b2154716c79516d6966303030303030303030303a636164656e63652e6d6f65405f6f6f79655f6a6f6b65726765726d616e793a636164656e63652e6d6f65'), - ('sim_member','sim_member','65 1','2689 2721','438 2721',X'034b472157755a5549494e74303030303030303030303a636164656e63652e6d6f65405f6f6f79655f5f706b5f77797a63686a3a636164656e63652e6d6f65'), - ('sim_member','sim_member','2 1','3058 3059','497 3059',X'034b3921616f764c6d776a67303030303030303030303a636164656e63652e6d6f65405f6f6f79655f726e6c3a636164656e63652e6d6f65'), - ('sim_member','sim_member','8 1','3666 3671','630 3671',X'034b39216966636d75794e6e303030303030303030303a636164656e63652e6d6f65405f6f6f79655f726e6c3a636164656e63652e6d6f65'), - ('sim_member','sim_member','43 1','3849 3874','668 3874',X'034b4f216b6b4b714249664c303030303030303030303a636164656e63652e6d6f65405f6f6f79655f656c656374726f6e6963353339313a636164656e63652e6d6f65'), - ('sim_member','sim_member','8 1','4280 4283','746 4283',X'034b3f216f705748554e6b46303030303030303030303a636164656e63652e6d6f65405f6f6f79655f636f6f6b69653a636164656e63652e6d6f65'), - ('sim_member','sim_member','158 1','4424 4465','770 4465',X'034b452170757146464b5948303030303030303030303a636164656e63652e6d6f65405f6f6f79655f5f706b5f6a666e747a3a636164656e63652e6d6f65'), - ('sim_member','sim_member','44 1','4810 4810','824 4810',X'034b4121734b4c6f784a4e62303030303030303030303a636164656e63652e6d6f65405f6f6f79655f313030626563733a636164656e63652e6d6f65'), - ('sim_member','sim_member','11 1','4892 4895','841 4895',X'034b45217443744769524448303030303030303030303a636164656e63652e6d6f65405f6f6f79655f646f6f74736b7972653a636164656e63652e6d6f65'), - ('sim_member','sim_member','73 1','5072 5089','888 5089',X'034b47217665764462756174303030303030303030303a636164656e63652e6d6f65405f6f6f79655f5f706b5f6b73706a75653a636164656e63652e6d6f65'), - ('sim_member','sim_member','59 1','5182 5236','903 5236',X'034b43217750454472596b77303030303030303030303a636164656e63652e6d6f65405f6f6f79655f74656368323334613a636164656e63652e6d6f65'), - ('sim_member','sim_member','52 1','5441 5469','968 5469',X'034b41217a66654e574d744b303030303030303030303a636164656e63652e6d6f65405f6f6f79655f6e6f766574746f3a636164656e63652e6d6f65'), - ('emoji','emoji','1','385','385',X'023331313035373039393137313237353737363733'), - ('emoji','emoji','1','771','771',X'023331323230353735323436303531303533353638'), - ('emoji','emoji','1','1157','1157',X'023331333530383339313335363836313033303730'), - ('emoji','emoji','1','1543','1543',X'0231333439373232393637383636393938373834'), - ('emoji','emoji','1','1929','1929',X'0231343933383437383237313138353535313436'), - ('emoji','emoji','1','2315','2315',X'0231363432353731303038313337373536373032'), - ('emoji','emoji','1','2701','2701',X'0231373738313036343330333034393434313238'), - ('emoji','emoji','1','3087','3087',X'0231393030383733373535343037303336343738'), - ('guild_space','guild_space','1','4','4',X'023331313333333135333632353636343535333336'), - ('guild_space','guild_space','1','9','9',X'023331313534383638343234373234343633363837'), - ('guild_space','guild_space','1','14','14',X'023331323139303338323637383430393235383138'), - ('guild_space','guild_space','1','19','19',X'023331323839363030383537323437303535383733'), - ('guild_space','guild_space','1','24','24',X'023331333435363431323031393032323838393837'), - ('guild_space','guild_space','1','29','29',X'0231323733383737363437323234393935383431'), - ('guild_space','guild_space','1','34','34',X'0231353239313736313536333938363832313135'), - ('guild_space','guild_space','1','39','39',X'0231383730313138363530373638363730373530'), - ('member_power','member_power','1 1','0 0','0 0',X'03350f40636164656e63653a636164656e63652e6d6f652a'), - ('sim_proxy','sim_proxy','1','23','23',X'025531363733363165392d656137652d343530392d623533302d356531613863613735336237'), - ('sim_proxy','sim_proxy','1','47','47',X'025532653561626332312d326332622d346133352d386237642d366432383162363036653932'), - ('sim_proxy','sim_proxy','1','71','71',X'025534383131393165322d393462302d346534632d623934352d336330323932623135356238'), - ('sim_proxy','sim_proxy','1','95','95',X'025536346331346631642d663834342d346535622d386665332d336162336163363239616230'), - ('sim_proxy','sim_proxy','1','119','119',X'025538376562363463322d363763352d346432352d383161642d666664333235663266303639'), - ('sim_proxy','sim_proxy','1','143','143',X'025561616630313539652d623165312d343231342d396266652d313334613536303738323231'), - ('sim_proxy','sim_proxy','1','167','167',X'025563396534393633372d663061352d343566352d383234382d366436393565643861316434'), - ('sim_proxy','sim_proxy','1','191','191',X'025565333734613634362d386231332d343365392d393635392d653233326366653866626265'), - ('member_cache','member_cache','4 1','98 99','66 99',X'034b35214a48614a71425870303030303030303030303a636164656e63652e6d6f6540657a7261637574653a6d61747269782e6f7267'), - ('member_cache','member_cache','4 1','119 122','80 122',X'034b43214c684978654c4d54303030303030303030303a636164656e63652e6d6f6540737461727368696e656c756e6163793a6d61747269782e6f7267'), - ('member_cache','member_cache','1 1','124 124','82 124',X'034b2d214d5071594e414a62303030303030303030303a636164656e63652e6d6f6540726e6c3a636164656e63652e6d6f65'), - ('member_cache','member_cache','5 1','128 131','85 131',X'034b3b214e446249714e704a303030303030303030303a636164656e63652e6d6f65406761627269656c766f6e643a6d61747269782e6f7267'), - ('member_cache','member_cache','4 1','138 139','90 139',X'034b3d214f48584445737062303030303030303030303a636164656e63652e6d6f6540616d693a7468652d61706f746865636172792e636c7562'), - ('member_cache','member_cache','5 1','207 209','135 209',X'034b51215450616f6a545444303030303030303030303a636164656e63652e6d6f65406a61636b736f6e6368656e3636363a6a61636b736f6e6368656e3636362e636f6d'), - ('member_cache','member_cache','76 1','216 249','140 249',X'034b2d2154716c79516d6966303030303030303030303a636164656e63652e6d6f65406963656d616e3a656e76732e6e6574'), - ('member_cache','member_cache','4 1','345 345','171 345',X'034b3521586f4c466b65786a303030303030303030303a636164656e63652e6d6f6540636164656e63653a636164656e63652e6d6f65'), - ('member_cache','member_cache','10 1','351 354','174 354',X'034b3521594b46454e797166303030303030303030303a636164656e63652e6d6f654066617269656c6c653a6d61747269782e6f7267'), - ('member_cache','member_cache','1 1','374 374','183 374',X'034b2d21596f54644f55766a303030303030303030303a636164656e63652e6d6f6540726e6c3a636164656e63652e6d6f65'), - ('member_cache','member_cache','152 1','405 499','205 499',X'034b45216342787456527844303030303030303030303a636164656e63652e6d6f65406d61726975733835313030303a6d617269757364617669642e6672'), - ('member_cache','member_cache','4 1','562 563','209 563',X'034b35216356514d45455158303030303030303030303a636164656e63652e6d6f6540657a7261637574653a6d61747269782e6f7267'), - ('member_cache','member_cache','8 1','582 586','223 586',X'034b3721654856655270706e303030303030303030303a636164656e63652e6d6f65406563686f3a66757272797265667567652e636f6d'), - ('member_cache','member_cache','7 1','594 600','227 600',X'034b3b2165724f7079584e46303030303030303030303a636164656e63652e6d6f6540766962656973766572796f3a6d61747269782e6f7267'), - ('member_cache','member_cache','165 1','613 624','235 624',X'034b4921676865544b5a7451303030303030303030303a636164656e63652e6d6f6540616d70666c6f7765723a7468652d61706f746865636172792e636c7562'), - ('member_cache','member_cache','165 1','613 749','235 749',X'034b4521676865544b5a7451303030303030303030303a636164656e63652e6d6f654073706c617473756e653a636861742e6e6575726172696f2e636f6d'), - ('member_cache','member_cache','6 1','778 782','236 782',X'034b3321676b6b686f756d42303030303030303030303a636164656e63652e6d6f65406b6162693a6361746769726c2e776f726b73'), - ('member_cache','member_cache','10 1','786 794','239 794',X'034b332168424a766e654e4f303030303030303030303a636164656e63652e6d6f65406d65636879613a636164656e63652e6d6f65'), - ('member_cache','member_cache','13 1','809 819','249 819',X'034b2b2169537958674e7851303030303030303030303a636164656e63652e6d6f65406d69646f753a656e76732e6e6574'), - ('member_cache','member_cache','4 1','856 858','273 858',X'034b3b216b73724f45554666303030303030303030303a636164656e63652e6d6f65406761627269656c766f6e643a6d61747269782e6f7267'), - ('member_cache','member_cache','12 1','865 874','279 874',X'034b35216c7570486a715444303030303030303030303a636164656e63652e6d6f654068656c6c63703a6f70656e737573652e6f7267'), - ('member_cache','member_cache','4 1','886 887','285 887',X'034b2d216d61676745536775303030303030303030303a636164656e63652e6d6f65406361743a6d61756e69756d2e6e6574'), - ('member_cache','member_cache','71 1','999 999','357 999',X'034b2d2177574f6673767573303030303030303030303a636164656e63652e6d6f654061613a6361747669626572732e6d65'), - ('member_cache','member_cache','14 1','1074 1085','361 1085',X'034b3721776c534544496a44303030303030303030303a636164656e63652e6d6f654073646f6d693a6861636b657273706163652e706c'), - ('file','file','1','4054','4054',X'03816568747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f3131323736303636393137383234313032342f313135313733303032323332383035373930372f53637265656e73686f745f32303233303931345f3036303333352e6a7067'), - ('file','file','1','8109','8109',X'03814168747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f3131393239333936393731353639313532322f313231383430393538323539343838373638302f494d475f343738322e6a7067'), - ('file','file','1','12164','12164',X'03813b68747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f3133343037373735333438353033333437322f313139393131323831343931373333333133332f696d6167652e706e67'), - ('file','file','1','16219','16219',X'03814168747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f313337363732343737393830353034383838322f313339323938363435323538343936303032312f707265766965772e706e67'), - ('file','file','1','20274','20274',X'03816568747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f3135393136353731343139343735393638302f313236353735383536303531323338313030392f53637265656e73686f745f32303234303732342d3135353232382e706e67'), - ('file','file','1','24329','24329',X'03813b68747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f3238383838323935333331343839333832352f313134373538383535363839343738313539332f696d6167652e706e67'), - ('file','file','1','28384','28384',X'03817168747470733a2f2f63646e2e646973636f72646170702e636f6d2f6174746163686d656e74732f3635353231363137333639363238363734362f313330383239363937343136333737353530392f31373331393932363837323838343136383737323137393036333831303733392e6a7067'), - ('file','file','1','32439','32439',X'027f68747470733a2f2f63646e2e646973636f72646170702e636f6d2f656d6f6a69732f313034323532383239323539363632313434322e706e67'), - ('lottie','lottie','1','2','2',X'0231373439303532393434363832353832303336'), - ('lottie','lottie','1','5','5',X'0231373531363036333739333430333635383634'), - ('lottie','lottie','1','8','8',X'0231373534313038373731383532323232353634'), - ('lottie','lottie','1','11','11',X'0231373936313430363338303933343433303932'), - ('lottie','lottie','1','14','14',X'0231373936313431373032363935343835353030'), - ('lottie','lottie','1','17','17',X'0231383136303837373932323931323832393434'), - ('lottie','lottie','1','20','20',X'0231383233393736313032393736323930383636'); - -ANALYZE sqlite_schema; - -COMMIT; diff --git a/src/db/migrations/0028-add-room-upgrade.sql b/src/db/migrations/0028-add-room-upgrade.sql deleted file mode 100644 index fed6f21..0000000 --- a/src/db/migrations/0028-add-room-upgrade.sql +++ /dev/null @@ -1,10 +0,0 @@ -BEGIN TRANSACTION; - -CREATE TABLE room_upgrade_pending ( - new_room_id TEXT NOT NULL, - old_room_id TEXT NOT NULL UNIQUE, - PRIMARY KEY (new_room_id), - FOREIGN KEY (old_room_id) REFERENCES channel_room (room_id) ON DELETE CASCADE -) WITHOUT ROWID; - -COMMIT; diff --git a/src/db/migrations/0029-force-guild-ids.js b/src/db/migrations/0029-force-guild-ids.js deleted file mode 100644 index 354bc6b..0000000 --- a/src/db/migrations/0029-force-guild-ids.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - a. If the bridge bot sim already has the correct ID: - - No rows updated. - - b. If the bridge bot sim has the wrong ID but there's no duplicate: - - One row updated. - - c. If the bridge bot sim has the wrong ID and there's a duplicate: - - One row updated (replaces an existing row). -*/ - -const {discord} = require("../../passthrough") - -const ones = "₀₁₂₃₄₅₆₇₈₉" -const tens = "0123456789" - -/* c8 ignore start */ - -module.exports = async function(db) { - /** @type {{name: string, channel_id: string, thread_parent: string | null}[]} */ - const rows = db.prepare("SELECT name, channel_id, thread_parent FROM channel_room WHERE guild_id IS NULL").all() - - /** @type {Map
🎞️ Uploaded file: 2022-10-18_16-49-46.mp4 (51 MB)
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.getPermissions(guild.id, [], guild.roles)
- 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)
-
- // 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 (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.getPermissions(guild.id, [], guild.roles, undefined, 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)
}
}
@@ -1013,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 629f2b8..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: () => []
@@ -273,287 +302,6 @@ test("event2message: markdown in link text does not attempt to be escaped becaus
)
})
-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"]
- }
- }]
- }
- )
-})
-
-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: ["users", "roles"]
- }
- }]
- }
- )
-})
-
test("event2message: basic html is converted to markdown", async t => {
t.deepEqual(
await eventToMessage({
@@ -656,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({
@@ -1433,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",
@@ -1483,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",
@@ -1558,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",
@@ -1641,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++
@@ -1691,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++
@@ -1742,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++
@@ -1787,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",
@@ -1838,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",
@@ -1887,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',
@@ -1976,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",
@@ -2061,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",
@@ -2113,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",
@@ -2163,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",
@@ -2213,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",
@@ -2263,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",
@@ -2313,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",
@@ -2373,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")
@@ -2454,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",
@@ -2509,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",
@@ -2571,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",
@@ -2630,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",
@@ -2686,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",
@@ -2741,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",
@@ -2803,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",
@@ -2854,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",
@@ -2915,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",
@@ -2966,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",
@@ -3024,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",
@@ -3052,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({
@@ -3347,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({
@@ -3536,7 +3021,7 @@ test("event2message: mentioning bridged rooms by alias works", async t => {
unsigned: {
age: 405299
}
- }, {}, {}, {
+ }, {}, {
api: {
async getAlias(alias) {
called++
@@ -3578,7 +3063,7 @@ test("event2message: mentioning bridged rooms by alias works (plaintext body)",
unsigned: {
age: 405299
}
- }, {}, {}, {
+ }, {}, {
api: {
async getAlias(alias) {
called++
@@ -3620,7 +3105,7 @@ test("event2message: mentioning bridged rooms by alias skips the link when alias
unsigned: {
age: 405299
}
- }, {}, {}, {
+ }, {}, {
api: {
async getAlias(alias) {
called++
@@ -3797,7 +3282,7 @@ test("event2message: mentioning unknown bridged events can approximate with time
unsigned: {
age: 405299
}
- }, {}, {}, {
+ }, {}, {
api: {
async getEvent(roomID, eventID) {
called++
@@ -3844,7 +3329,7 @@ test("event2message: mentioning events falls back to original link when server d
unsigned: {
age: 405299
}
- }, {}, {}, {
+ }, {}, {
api: {
async getEvent(roomID, eventID) {
called++
@@ -3890,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() {
@@ -4052,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++
@@ -4102,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++
@@ -4150,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++
@@ -4195,7 +3680,7 @@ test("event2message: overly long usernames are shifted into the message content"
unsigned: {
age: 405299
}
- }, {}, {}, {
+ }, {}, {
api: {
getStateEvent: async (roomID, type, stateKey) => {
called++
@@ -4556,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({
@@ -4768,7 +4093,7 @@ 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++
@@ -4811,7 +4136,7 @@ test("event2message: stickers with unknown mimetype are not allowed", async t =>
},
event_id: "$mL-eEVWCwOvFtoOiivDP7gepvf-fTYH6_ioK82bWDI0",
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
- }, {}, {}, {
+ }, {}, {
api: {
async getMedia(mxc, options) {
called++
@@ -4975,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) {
@@ -5028,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) {
@@ -5072,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 */
@@ -5141,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")
@@ -5154,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"
- }
- }
}
}
}),
@@ -5187,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",
@@ -5199,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, "")
@@ -5211,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"
- }
- }
}
}
}),
@@ -5244,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",
@@ -5256,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, "")
@@ -5270,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"
- }
- }
}
}
}),
@@ -5368,122 +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: 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/m2d/converters/utils.js b/src/m2d/converters/utils.js
new file mode 100644
index 0000000..41cb0af
--- /dev/null
+++ b/src/m2d/converters/utils.js
@@ -0,0 +1,241 @@
+// @ts-check
+
+const assert = require("assert").strict
+
+const passthrough = require("../../passthrough")
+const {db} = passthrough
+
+const {reg} = require("../../matrix/read-registration")
+const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex))
+
+/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore
+let hasher = null
+// @ts-ignore
+require("xxhash-wasm")().then(h => hasher = h)
+
+const BLOCK_ELEMENTS = [
+ "ADDRESS", "ARTICLE", "ASIDE", "AUDIO", "BLOCKQUOTE", "BODY", "CANVAS",
+ "CENTER", "DD", "DETAILS", "DIR", "DIV", "DL", "DT", "FIELDSET", "FIGCAPTION", "FIGURE",
+ "FOOTER", "FORM", "FRAMESET", "H1", "H2", "H3", "H4", "H5", "H6", "HEADER",
+ "HGROUP", "HR", "HTML", "ISINDEX", "LI", "MAIN", "MENU", "NAV", "NOFRAMES",
+ "NOSCRIPT", "OL", "OUTPUT", "P", "PRE", "SECTION", "SUMMARY", "TABLE", "TBODY", "TD",
+ "TFOOT", "TH", "THEAD", "TR", "UL"
+]
+const NEWLINE_ELEMENTS = BLOCK_ELEMENTS.concat(["BR"])
+
+/**
+ * Determine whether an event is the bridged representation of a discord message.
+ * Such messages shouldn't be bridged again.
+ * @param {string} sender
+ */
+function eventSenderIsFromDiscord(sender) {
+ // If it's from a user in the bridge's namespace, then it originated from discord
+ // This could include messages sent by the appservice's bot user, because that is what's used for webhooks
+ if (userRegex.some(x => sender.match(x))) {
+ return true
+ }
+
+ return false
+}
+
+/**
+ * Event IDs are really big and have more entropy than we need.
+ * If we want to store the event ID in the database, we can store a more compact version by hashing it with this.
+ * I choose a 64-bit non-cryptographic hash as only a 32-bit hash will see birthday collisions unreasonably frequently: https://en.wikipedia.org/wiki/Birthday_attack#Mathematics
+ * xxhash outputs an unsigned 64-bit integer.
+ * Converting to a signed 64-bit integer with no bit loss so that it can be stored in an SQLite integer field as-is: https://www.sqlite.org/fileformat2.html#record_format
+ * This should give very efficient storage with sufficient entropy.
+ * @param {string} eventID
+ */
+function getEventIDHash(eventID) {
+ assert(hasher, "xxhash is not ready yet")
+ if (eventID[0] === "$" && eventID.length >= 13) {
+ eventID = eventID.slice(1) // increase entropy per character to potentially help xxhash
+ }
+ const unsignedHash = hasher.h64(eventID)
+ const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range
+ return signedHash
+}
+
+class MatrixStringBuilder {
+ constructor() {
+ this.body = ""
+ this.formattedBody = ""
+ }
+
+ /**
+ * @param {string} body
+ * @param {string} [formattedBody]
+ * @param {any} [condition]
+ */
+ add(body, formattedBody, condition = true) {
+ if (condition) {
+ if (formattedBody == undefined) formattedBody = body
+ this.body += body
+ this.formattedBody += formattedBody
+ }
+ return this
+ }
+
+ /**
+ * @param {string} body
+ * @param {string} [formattedBody]
+ * @param {any} [condition]
+ */
+ addLine(body, formattedBody, condition = true) {
+ if (condition) {
+ 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*$/)
+ if (this.formattedBody.length && (!match || !NEWLINE_ELEMENTS.includes(match[1].toUpperCase()))) this.formattedBody += "
"
+ this.formattedBody += formattedBody
+ }
+ return this
+ }
+
+ /**
+ * @param {string} body
+ * @param {string} [formattedBody]
+ * @param {any} [condition]
+ */
+ addParagraph(body, formattedBody, condition = true) {
+ if (condition) {
+ if (formattedBody == undefined) formattedBody = body
+ if (this.body.length && this.body.slice(-1) !== "\n") this.body += "\n\n"
+ this.body += body
+ formattedBody = `${formattedBody}
`
+ this.formattedBody += formattedBody
+ }
+ return this
+ }
+
+ get() {
+ return {
+ msgtype: "m.text",
+ body: this.body,
+ format: "org.matrix.custom.html",
+ formatted_body: this.formattedBody
+ }
+ }
+}
+
+/**
+ * Context: Room IDs are not routable on their own. Room permalinks need a list of servers to try. The client is responsible for coming up with a list of servers.
+ * ASSUMPTION 1: The bridge bot is a member of the target room and can therefore access its power levels and member list for calculation.
+ * ASSUMPTION 2: Because the bridge bot is a member of the target room, the target room is bridged.
+ * 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" | "getJoinedMembers"]: import("../../matrix/api")[K]}} api
+ */
+async function getViaServers(roomID, api) {
+ const candidates = []
+ const {joined} = await api.getJoinedMembers(roomID)
+ // Candidate 0: The bot's own server name
+ 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
+ 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} */
+ const servers = new Map()
+ // We can get the most popular servers if we know the members, so let's process those...
+ Object.keys(joined)
+ .filter(mxid => !mxid.startsWith("@_")) // Quick check
+ .filter(mxid => !userRegex.some(r => mxid.match(r))) // Full check
+ .slice(0, 1000) // Just sample the first thousand real members
+ .map(mxid => {
+ const match = mxid.match(/:(.*)/)
+ assert(match)
+ return match[1]
+ })
+ .filter(server => !server.match(/([a-f0-9:]+:+)+[a-f0-9]+/)) // No IPv6 servers
+ .filter(server => !server.match(/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/)) // No IPv4 servers
+ // I don't care enough to check ACLs
+ .forEach(server => {
+ const existing = servers.get(server)
+ if (!existing) servers.set(server, 1)
+ else servers.set(server, existing + 1)
+ })
+ const serverList = [...servers.entries()].sort((a, b) => b[1] - a[1])
+ for (const server of serverList) {
+ if (!candidates.includes(server[0])) {
+ candidates.push(server[0])
+ if (candidates.length >= 4) break // Can have at most 4 candidate via servers
+ }
+ }
+ return candidates
+}
+
+/**
+ * Context: Room IDs are not routable on their own. Room permalinks need a list of servers to try. The client is responsible for coming up with a list of servers.
+ * ASSUMPTION 1: The bridge bot is a member of the target room and can therefore access its power levels and member list for calculation.
+ * ASSUMPTION 2: Because the bridge bot is a member of the target room, the target room is bridged.
+ * 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" | "getJoinedMembers"]: import("../../matrix/api")[K]}} api
+ * @returns {Promise}
+ */
+async function getViaServersQuery(roomID, api) {
+ const list = await getViaServers(roomID, api)
+ const qs = new URLSearchParams()
+ for (const server of list) {
+ qs.append("via", server)
+ }
+ return qs
+}
+
+/**
+ * Since the introduction of authenticated media, this can no longer just be the /_matrix/media/r0/download URL
+ * because Discord and Discord users cannot use those URLs. Media now has to be proxied through the bridge.
+ * To avoid the bridge acting as a proxy for *any* media, there is a list of permitted media stored in the database.
+ * (The other approach would be signing the URLs with a MAC (or similar) and adding the signature, but I'm not a
+ * cryptographer, so I don't want to.) To reduce database disk space usage, instead of storing each permitted URL,
+ * we just store its xxhash as a signed (as in +/-, not signature) 64-bit integer, which fits in an SQLite integer field.
+ * @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} mxc
+ * @returns {string | undefined}
+ */
+function getPublicUrlForMxc(mxc) {
+ assert(hasher, "xxhash is not ready yet")
+ const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
+ if (!mediaParts) return undefined
+
+ const serverAndMediaID = `${mediaParts[1]}/${mediaParts[2]}`
+ const unsignedHash = hasher.h64(serverAndMediaID)
+ 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 `${reg.ooye.bridge_origin}/download/matrix/${serverAndMediaID}`
+}
+
+module.exports.BLOCK_ELEMENTS = BLOCK_ELEMENTS
+module.exports.eventSenderIsFromDiscord = eventSenderIsFromDiscord
+module.exports.getPublicUrlForMxc = getPublicUrlForMxc
+module.exports.getEventIDHash = getEventIDHash
+module.exports.MatrixStringBuilder = MatrixStringBuilder
+module.exports.getViaServers = getViaServers
+module.exports.getViaServersQuery = getViaServersQuery
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 70e293b..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,58 +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?.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
+ 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 and/or an invite target if the bot itself left
- if (event.state_key === utils.bot) {
+ // 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)
- db.prepare("DELETE FROM invite WHERE room_id = ?").run(event.room_id)
}
}
- if (utils.eventSenderIsFromDiscord(event.state_key)) return
-
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
)
}))
@@ -454,35 +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)
}))
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 87bbf0c..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,97 +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?}>}
- */
-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")
- }
- }
-
- // 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")
- }
- }
- } 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
- }
- }
-
- 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
@@ -412,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)
}
}
@@ -460,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)
}
@@ -554,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
@@ -597,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
@@ -615,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
@@ -624,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 7bc1fec..2070a56 100644
--- a/src/matrix/file.js
+++ b/src/matrix/file.js
@@ -103,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 e382a32..93bc312 100644
--- a/src/matrix/matrix-command-handler.js
+++ b/src/matrix/matrix-command-handler.js
@@ -8,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")} */
@@ -58,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) {
@@ -114,7 +114,7 @@ const commands = [{
const guild = discord.guilds.get(guildID)
assert(guild)
const slots = getSlotCount(guild.premium_tier)
- const permissions = dUtils.getPermissions(guild.id, [], guild.roles)
+ 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...)
@@ -123,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",
@@ -174,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", {
@@ -250,7 +253,7 @@ const commands = [{
const guild = discord.guilds.get(guildID)
assert(guild)
- const permissions = dUtils.getPermissions(guild.id, [], guild.roles)
+ const permissions = dUtils.getPermissions([], guild.roles)
if (!(permissions & 0x800000000n)) { // CREATE_PUBLIC_THREADS
return api.sendEvent(event.room_id, "m.room.message", {
...ctx,
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 114bf75..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: []
}
}
}
diff --git a/src/matrix/room-upgrade.js b/src/matrix/room-upgrade.js
deleted file mode 100644
index 5a2606e..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)
-
- // 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
-
- return await roomUpgradeSema.request(async () => {
- // 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.js b/src/matrix/utils.js
deleted file mode 100644
index 9f5cb0f..0000000
--- a/src/matrix/utils.js
+++ /dev/null
@@ -1,415 +0,0 @@
-// @ts-check
-
-const assert = require("assert").strict
-const Ty = require("../types")
-const {tag} = require("@cloudrac3r/html-template-tag")
-const passthrough = require("../passthrough")
-const {db} = passthrough
-
-const {reg} = require("./read-registration")
-const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex))
-
-/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore
-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",
- "FOOTER", "FORM", "FRAMESET", "H1", "H2", "H3", "H4", "H5", "H6", "HEADER",
- "HGROUP", "HR", "HTML", "ISINDEX", "LI", "MAIN", "MENU", "NAV", "NOFRAMES",
- "NOSCRIPT", "OL", "OUTPUT", "P", "PRE", "SECTION", "SUMMARY", "TABLE", "TBODY", "TD",
- "TFOOT", "TH", "THEAD", "TR", "UL"
-]
-const NEWLINE_ELEMENTS = BLOCK_ELEMENTS.concat(["BR"])
-
-/**
- * Determine whether an event is the bridged representation of a discord message.
- * Such messages shouldn't be bridged again.
- * @param {string} sender
- */
-function eventSenderIsFromDiscord(sender) {
- // If it's from a user in the bridge's namespace, then it originated from discord
- // This could include messages sent by the appservice's bot user, because that is what's used for webhooks
- if (userRegex.some(x => sender.match(x))) {
- return true
- }
-
- return false
-}
-
-/**
- * Event IDs are really big and have more entropy than we need.
- * If we want to store the event ID in the database, we can store a more compact version by hashing it with this.
- * I choose a 64-bit non-cryptographic hash as only a 32-bit hash will see birthday collisions unreasonably frequently: https://en.wikipedia.org/wiki/Birthday_attack#Mathematics
- * xxhash outputs an unsigned 64-bit integer.
- * Converting to a signed 64-bit integer with no bit loss so that it can be stored in an SQLite integer field as-is: https://www.sqlite.org/fileformat2.html#record_format
- * This should give very efficient storage with sufficient entropy.
- * @param {string} eventID
- */
-function getEventIDHash(eventID) {
- assert(hasher, "xxhash is not ready yet")
- if (eventID[0] === "$" && eventID.length >= 13) {
- eventID = eventID.slice(1) // increase entropy per character to potentially help xxhash
- }
- const unsignedHash = hasher.h64(eventID)
- const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range
- 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 = ""
- this.formattedBody = ""
- }
-
- /**
- * @param {string} body
- * @param {string} [formattedBody]
- * @param {any} [condition]
- */
- add(body, formattedBody, condition = true) {
- if (condition) {
- if (formattedBody == undefined) formattedBody = tag`${body}`
- this.body += body
- this.formattedBody += formattedBody
- }
- return this
- }
-
- /**
- * @param {string} body
- * @param {string} [formattedBody]
- * @param {any} [condition]
- */
- addLine(body, formattedBody, condition = true) {
- if (condition) {
- if (formattedBody == undefined) formattedBody = tag`${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*$/)
- if (this.formattedBody.length && (!match || !NEWLINE_ELEMENTS.includes(match[1].toUpperCase()))) this.formattedBody += "
"
- this.formattedBody += formattedBody
- }
- return this
- }
-
- /**
- * @param {string} body
- * @param {string} [formattedBody]
- * @param {any} [condition]
- */
- addParagraph(body, formattedBody, condition = true) {
- if (condition) {
- if (formattedBody == undefined) formattedBody = tag`${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}
`
- this.formattedBody += formattedBody
- }
- return this
- }
-
- get() {
- return {
- msgtype: "m.text",
- body: this.body,
- format: "org.matrix.custom.html",
- formatted_body: this.formattedBody
- }
- }
-}
-
-/**
- * Context: Room IDs are not routable on their own. Room permalinks need a list of servers to try. The client is responsible for coming up with a list of servers.
- * ASSUMPTION 1: The bridge bot is a member of the target room and can therefore access its power levels and member list for calculation.
- * ASSUMPTION 2: Because the bridge bot is a member of the target room, the target room is bridged.
- * 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
- */
-async function getViaServers(roomID, api) {
- const candidates = []
- const {joined} = await api.getJoinedMembers(roomID)
- // Candidate 0: The bot's own server name
- 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
- }
- // Candidates 2-3: Most popular servers in the room
- /** @type {Map} */
- const servers = new Map()
- // We can get the most popular servers if we know the members, so let's process those...
- Object.keys(joined)
- .filter(mxid => !mxid.startsWith("@_")) // Quick check
- .filter(mxid => !userRegex.some(r => mxid.match(r))) // Full check
- .slice(0, 1000) // Just sample the first thousand real members
- .map(mxid => {
- const match = mxid.match(/:(.*)/)
- assert(match)
- return match[1]
- })
- .filter(server => !server.match(/([a-f0-9:]+:+)+[a-f0-9]+/)) // No IPv6 servers
- .filter(server => !server.match(/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/)) // No IPv4 servers
- // I don't care enough to check ACLs
- .forEach(server => {
- const existing = servers.get(server)
- if (!existing) servers.set(server, 1)
- else servers.set(server, existing + 1)
- })
- const serverList = [...servers.entries()].sort((a, b) => b[1] - a[1])
- for (const server of serverList) {
- if (!candidates.includes(server[0])) {
- candidates.push(server[0])
- if (candidates.length >= 4) break // Can have at most 4 candidate via servers
- }
- }
- return candidates
-}
-
-/**
- * Context: Room IDs are not routable on their own. Room permalinks need a list of servers to try. The client is responsible for coming up with a list of servers.
- * ASSUMPTION 1: The bridge bot is a member of the target room and can therefore access its power levels and member list for calculation.
- * ASSUMPTION 2: Because the bridge bot is a member of the target room, the target room is bridged.
- * 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
- * @returns {Promise}
- */
-async function getViaServersQuery(roomID, api) {
- const list = await getViaServers(roomID, api)
- const qs = new URLSearchParams()
- for (const server of list) {
- qs.append("via", server)
- }
- return qs
-}
-
-function generatePermittedMediaHash(mxc) {
- assert(hasher, "xxhash is not ready yet")
- const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
- if (!mediaParts) return undefined
-
- const serverAndMediaID = `${mediaParts[1]}/${mediaParts[2]}`
- const unsignedHash = hasher.h64(serverAndMediaID)
- 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
-}
-
-/**
- * Since the introduction of authenticated media, this can no longer just be the /_matrix/media/r0/download URL
- * because Discord and Discord users cannot use those URLs. Media now has to be proxied through the bridge.
- * To avoid the bridge acting as a proxy for *any* media, there is a list of permitted media stored in the database.
- * (The other approach would be signing the URLs with a MAC (or similar) and adding the signature, but I'm not a
- * cryptographer, so I don't want to.) To reduce database disk space usage, instead of storing each permitted URL,
- * we just store its xxhash as a signed (as in +/-, not signature) 64-bit integer, which fits in an SQLite integer field.
- * @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
- * @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
-
- const serverAndMediaID = `${mediaParts[1]}/${mediaParts[2]}`
- const unsignedHash = hasher.h64(serverAndMediaID)
- 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
-}
-
-/**
- * @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/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/types.d.ts b/src/types.d.ts
index a85907d..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
- }
-
- 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,11 +314,6 @@ export namespace Event {
}> & {
redacts: string
}
-
- export type M_Room_Tombstone = {
- body: string
- replacement_room: string
- }
}
export namespace R {
@@ -433,7 +357,6 @@ export namespace R {
guest_can_join: boolean
join_rule?: string
name?: string
- topic?: string
num_joined_members: number
room_id: string
room_type?: string
@@ -443,68 +366,12 @@ export namespace R {
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 f87550d..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})
@@ -97,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/guild.pug b/src/web/pug/guild.pug
index a9e770b..92ffa1b 100644
--- a/src/web/pug/guild.pug
+++ b/src/web/pug/guild.pug
@@ -13,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
@@ -75,7 +75,6 @@ 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 Privacy level
span#privacy-level-loading
.s-card
@@ -105,7 +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 Features
+ h2.mt48.fs-headline1 Features
.s-card.d-grid.px0.g16
form.d-flex.ai-center.g16
#url-preview-loading.p8
@@ -125,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
@@ -186,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
@@ -219,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
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/template.pug b/src/web/pug/includes/template.pug
index 9fe80aa..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.
@@ -68,8 +58,6 @@ html(lang="en")
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.
@@ -91,21 +79,13 @@ html(lang="en")
.s-btn__dropdown:has(+ :popover-open) {
background-color: var(--theme-topbar-item-background-hover, var(--black-200)) !important;
}
- @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
@@ -134,7 +114,9 @@ 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
@@ -147,13 +129,6 @@ html(lang="en")
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"))
//- Error dialog
aside.s-modal#server-error(aria-hidden="true")
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 e4f4ab4..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 {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 ccbcfdd..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 {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 63dd3ec..b640d36 100644
--- a/src/web/routes/guild-settings.js
+++ b/src/web/routes/guild-settings.js
@@ -74,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()
diff --git a/src/web/routes/guild.js b/src/web/routes/guild.js
index a5508c4..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,36 +77,43 @@ 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 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,
@@ -133,20 +121,6 @@ function getChannelRoomsLinks(guild, rooms, roles) {
}
}
-/**
- * @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)
@@ -162,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})
}
@@ -254,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)
@@ -263,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 aa17548..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 {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 43995fc..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(() => {
@@ -205,17 +169,20 @@ as.router.post("/api/link", defineEventHandler(async event => {
}
// 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)
@@ -235,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"})
@@ -243,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 e8473f8..ffe4e5e 100644
--- a/src/web/routes/link.test.js
+++ b/src/web/routes/link.test.js
@@ -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,7 +441,47 @@ 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 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: {
+ 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, "")
+ 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, 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: {
@@ -468,28 +514,11 @@ test("web link room: check that bridge has PL 100 in target room", async t => {
t.equal(type, "m.room.power_levels")
t.equal(key, "")
return {users_default: 50}
- },
- 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"
- }
- }
}
}
}))
t.equal(error.data, "OOYE needs power level 100 (admin) in the target Matrix room")
- t.equal(called, 4)
+ t.equal(called, 3)
})
test("web link room: successfully calls createRoom", async t => {
@@ -539,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")
@@ -572,7 +584,7 @@ test("web link room: successfully calls createRoom", async t => {
}
}
})
- t.equal(called, 9)
+ t.equal(called, 8)
})
// *****
@@ -613,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: {
@@ -624,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")
}
@@ -633,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: {
@@ -644,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")
}
@@ -654,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"]
@@ -667,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 830556e..bc9c7e0 100644
--- a/src/web/routes/log-in-with-matrix.test.js
+++ b/src/web/routes/log-in-with-matrix.test.js
@@ -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 2f3a791..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 aa60bd3..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 dc13cf0..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,79 +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") 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.promises.readFile(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/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 6a53cb0..e64b9c2 100644
--- a/test/data.js
+++ b/test/data.js
@@ -101,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"},
@@ -127,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: {
@@ -180,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: [],
@@ -239,7 +206,7 @@ module.exports = {
unicode_emoji: null,
tags: {},
position: 0,
- permissions: '1122573558996672',
+ permissions: '559623605575360',
name: '@everyone',
mentionable: false,
managed: false,
@@ -1256,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
@@ -2692,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,
@@ -3219,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,
@@ -3624,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: {
@@ -4947,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",
@@ -5069,183 +4673,6 @@ module.exports = {
tts: false
}
},
- 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: {
edit_by_webhook: {
application_id: "684280192553844747",
@@ -5505,6 +4932,7 @@ module.exports = {
mention_roles: [],
mentions: [],
pinned: false,
+ timestamp: "2023-08-16T22:38:38.641000+00:00",
tts: false,
type: 0
},
@@ -5578,6 +5006,7 @@ module.exports = {
mention_roles: [],
mentions: [],
pinned: false,
+ timestamp: "2023-08-16T22:38:38.641000+00:00",
tts: false,
type: 0
},
@@ -5612,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: [],
@@ -5652,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: [],
@@ -5692,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: [],
@@ -5864,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: {
@@ -5960,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,
diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql
index 1dd9dfe..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'),
@@ -47,8 +41,7 @@ INSERT INTO sim_member (mxid, room_id, hashed_profile_content) VALUES
INSERT INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES
('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '196188877885538304', 'Azalea &flwr; 🌺');
-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'),
@@ -80,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),
@@ -98,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),
@@ -127,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'),
@@ -145,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'),
@@ -163,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),
@@ -179,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');
@@ -209,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 e05b687..b01f0ce 100644
--- a/test/test.js
+++ b/test/test.js
@@ -28,9 +28,6 @@ reg.namespaces = {
}
reg.ooye.bridge_origin = "https://bridge.example.org"
reg.ooye.time_zone = "Pacific/Auckland"
-reg.ooye.max_file_size = 5000000
-reg.ooye.web_password = "password123"
-reg.ooye.include_user_id_in_mxid = false
const sync = new HeatSync({watchFS: false})
@@ -75,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)
@@ -133,26 +132,29 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
require("./addbot.test")
require("../src/db/orm.test")
require("../src/web/server.test")
+ require("../src/web/routes/download-discord.test")
+ require("../src/web/routes/download-matrix.test")
+ require("../src/web/routes/guild.test")
+ require("../src/web/routes/guild-settings.test")
+ require("../src/web/routes/info.test")
+ require("../src/web/routes/link.test")
+ require("../src/web/routes/log-in-with-matrix.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/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")
@@ -161,19 +163,11 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
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")
- require("../src/web/routes/download-discord.test")
- require("../src/web/routes/download-matrix.test")
- require("../src/web/routes/guild.test")
- require("../src/web/routes/guild-settings.test")
- 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")
})()
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",