From 505c41a35e260d6a7919a5a8d7e9277423f42c1b Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 10 Jan 2026 02:28:18 +1300 Subject: [PATCH] More code coverage --- package.json | 2 +- src/d2m/converters/edit-to-changes.test.js | 55 +++++++ src/d2m/converters/message-to-event.js | 2 +- src/d2m/converters/message-to-event.test.js | 80 +++++++++ src/d2m/converters/user-to-mxid.js | 7 +- src/d2m/converters/user-to-mxid.test.js | 10 +- src/discord/utils.test.js | 12 ++ src/m2d/event-dispatcher.js | 6 +- src/matrix/matrix-command-handler.js | 2 +- src/matrix/room-upgrade.js | 30 ++-- src/matrix/room-upgrade.test.js | 169 ++++++++++++++++++++ src/web/routes/download-discord.js | 2 + src/web/routes/download-discord.test.js | 47 ++++-- src/web/routes/guild.js | 53 ++++-- src/web/routes/guild.test.js | 73 +++++++-- src/web/routes/link.js | 8 +- src/web/routes/log-in-with-matrix.test.js | 5 +- src/web/routes/oauth.js | 44 +++-- src/web/routes/oauth.test.js | 121 ++++++++++++++ test/data.js | 77 +++++++++ test/ooye-test-data.sql | 25 ++- test/test.js | 3 + test/web.js | 4 +- 23 files changed, 735 insertions(+), 102 deletions(-) create mode 100644 src/matrix/room-upgrade.test.js create mode 100644 src/web/routes/oauth.test.js diff --git a/package.json b/package.json index 4a80f22..05a4220 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,6 @@ "addbot": "node addbot.js", "test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js | tap-dot", "test-slow": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js -- --slow | tap-dot", - "cover": "c8 -o test/coverage --skip-full -x db/migrations -x src/matrix/file.js -x src/matrix/api.js -x src/d2m/converters/rlottie-wasm.js -r html -r text supertape --no-check-assertions-count --format fail --no-worker test/test.js -- --slow" + "cover": "c8 -o test/coverage --skip-full -x db/migrations -x src/m2d/event-dispatcher.js -x src/matrix/file.js -x src/matrix/api.js -x src/d2m/converters/rlottie-wasm.js -r html -r text supertape --no-check-assertions-count --format fail --no-worker test/test.js -- --slow" } } diff --git a/src/d2m/converters/edit-to-changes.test.js b/src/d2m/converters/edit-to-changes.test.js index dfb286c..b252175 100644 --- a/src/d2m/converters/edit-to-changes.test.js +++ b/src/d2m/converters/edit-to-changes.test.js @@ -271,6 +271,61 @@ test("edit2changes: promotes the text event when multiple rows have part = 1 (sh ]) }) +test("edit2changes: promotes newly sent event", async t => { + const {eventsToReplace, eventsToRedact, eventsToSend, promotions} = await editToChanges({ + channel_id: "1160894080998461480", + id: "1404133238414376971", + content: "hi", + attachments: [{ + id: "1157854643037163610", + filename: "Screenshot_20231001_034036.jpg", + size: 51981, + url: "https://cdn.discordapp.com/attachments/176333891320283136/1157854643037163610/Screenshot_20231001_034036.jpg?ex=651a1faa&is=6518ce2a&hm=eb5ca80a3fa7add8765bf404aea2028a28a2341e4a62435986bcdcf058da82f3&", + proxy_url: "https://media.discordapp.net/attachments/176333891320283136/1157854643037163610/Screenshot_20231001_034036.jpg?ex=651a1faa&is=6518ce2a&hm=eb5ca80a3fa7add8765bf404aea2028a28a2341e4a62435986bcdcf058da82f3&", + width: 1080, + height: 1170, + content_type: "image/jpeg" + }], + author: { + username: "cadence.worm", + global_name: "Cadence" + } + }, data.guild.general, { + async getEvent(roomID, eventID) { + t.equal(eventID, "$uUKLcTQvik5tgtTGDKuzn0Ci4zcCvSoUcYn2X7mXm9I") + return { + type: "m.room.message", + sender: "@_ooye_cadence.worm:cadence.moe", + content: { + msgtype: "m.text", + body: "hi" + } + } + } + }) + t.deepEqual(eventsToRedact, ["$LhmoWWvYyn5_AHkfb6FaXmLI6ZOC1kloql5P40YDmIk"]) + t.deepEqual(eventsToReplace, []) + t.deepEqual(eventsToSend, [{ + $type: "m.room.message", + body: "Screenshot_20231001_034036.jpg", + external_url: "https://bridge.example.org/download/discordcdn/176333891320283136/1157854643037163610/Screenshot_20231001_034036.jpg", + filename: "Screenshot_20231001_034036.jpg", + info: { + mimetype: "image/jpeg", + size: 51981, + w: 1080, + h: 1170 + }, + url: "mxc://cadence.moe/zAXdQriaJuLZohDDmacwWWDR", + "m.mentions": {}, + msgtype: "m.image" + }]) + t.deepEqual(promotions, [ + {column: "reaction_part", nextEvent: true} + ]) + // assert that the event parts will be consistent in database after this +}) + test("edit2changes: generated embed", async t => { let called = 0 const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.embed_generated_social_media_image, data.guild.general, { diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 66717ad..4ff1f4d 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -178,7 +178,7 @@ async function attachmentToEvent(mentions, attachment) { info: { mimetype: attachment.content_type, size: attachment.size, - duration: attachment.duration_secs ? Math.round(attachment.duration_secs * 1000) : undefined + duration: attachment.duration_secs && Math.round(attachment.duration_secs * 1000) } } } else { diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 05ec5be..3303b27 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -1356,3 +1356,83 @@ 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, :cx_marvelous: :cx_marvelous:`, + "m.mentions": {} + } + ]) +}) + +test("message2event: cross-room reply", async t => { + let called = 0 + const events = await messageToEvent({ + type: 19, + message_reference: { + channel_id: "1161864271370666075", + guild_id: "1160893336324931584", + message_id: "1458091145136443547" + }, + referenced_message: { + channel_id: "1161864271370666075", + id: "1458091145136443547", + content: "", + attachments: [{ + filename: "image.png", + id: "1456813607693193478", + size: 104006, + content_type: "image/png", + url: "https://cdn.discordapp.com/attachments/1160893337029586956/1458790740338409605/image.png?ex=696194ff&is=6960437f&hm=923d0ef7d1b249470be49edbc37628cc4ff8a438f0ab12f54c045578135f7050" + }], + author: { + username: "Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆" + } + }, + content: "cross-room reply" + }, {}, {}, {api: { + async getEvent(roomID, eventID) { + called++ + t.equal(roomID, "!mHmhQQPwXNananaOLD:cadence.moe") + t.equal(eventID, "$pgzCQjq_y5sy8RvWOUuoF3obNHjs8iNvt9c-odrOCPY") + return { + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + "body": "image.png", + "info": { + "h": 738, + "mimetype": "image/png", + "org.matrix.msc4230.is_animated": false, + "size": 111189, + "w": 772, + "xyz.amorgan.blurhash": "L255Oa~qRPD$-pxuoJoLIUM{xuxu" + }, + "m.mentions": {}, + "msgtype": "m.image", + "url": "mxc://matrix.org/QbSujQjRLekzPknKlPsXbGDS" + } + } + } + }}) + t.deepEqual(events, [ + { + $type: "m.room.message", + msgtype: "m.text", + body: "> Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆: [Media]\n\ncross-room reply", + format: "org.matrix.custom.html", + formatted_body: `
In reply to Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆
[Media]
cross-room reply`, + "m.mentions": { + user_ids: [ + "@cadence:cadence.moe" + ] + } + } + ]) +}) diff --git a/src/d2m/converters/user-to-mxid.js b/src/d2m/converters/user-to-mxid.js index f7a49b1..12891c0 100644 --- a/src/d2m/converters/user-to-mxid.js +++ b/src/d2m/converters/user-to-mxid.js @@ -108,12 +108,7 @@ function isWebhookUserID(userID) { * @returns {string} */ function webhookAuthorToSimName(author) { - if (SPECIAL_USER_MAPPINGS.has(author.id)) { - const error = new Error("Special users should have followed the other code path.") - // @ts-ignore - error.author = author - throw error - } + 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) diff --git a/src/d2m/converters/user-to-mxid.test.js b/src/d2m/converters/user-to-mxid.test.js index 86f151b..2217a93 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} = require("./user-to-mxid") +const {userToSimName, webhookAuthorToSimName} = require("./user-to-mxid") test("user2name: cannot create user for a webhook", async t => { const [error] = await tryToCatch(() => userToSimName({discriminator: "0000"})) @@ -52,3 +52,11 @@ test("user2name: includes ID if requested in config", t => { t.equal(userToSimName({username: "f***", discriminator: "0001", id: "123456"}), "123456_f") reg.ooye.include_user_id_in_mxid = false }) + +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") +}) diff --git a/src/discord/utils.test.js b/src/discord/utils.test.js index 7900440..516bd2f 100644 --- a/src/discord/utils.test.js +++ b/src/discord/utils.test.js @@ -180,3 +180,15 @@ test("isEphemeralMessage: doesn't detect normal message", t => { test("getPublicUrlForCdn: no-op on non-discord URL", t => { t.equal(utils.getPublicUrlForCdn("https://cadence.moe"), "https://cadence.moe") }) + +test("how old: now", t => { + t.equal(utils.howOldUnbridgedMessage(new Date().toISOString(), new Date().toISOString()), "an unbridged message") +}) + +test("how old: hours", t => { + t.equal(utils.howOldUnbridgedMessage("2026-01-01T00:00:00", "2026-01-01T03:10:00"), "a 3-hour-old unbridged message") +}) + +test("how old: days", t => { + t.equal(utils.howOldUnbridgedMessage("2024-01-01", "2025-01-01"), "a 366-day-old unbridged message") +}) diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index 13c0af1..d910852 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -332,7 +332,7 @@ async event => { const bot = `@${reg.sender_localpart}:${reg.ooye.server_name}` if (event.state_key === bot) { - const upgraded = await roomUpgrade.onBotMembership(event) + const upgraded = await roomUpgrade.onBotMembership(event, api, createRoom) if (upgraded) return } @@ -406,7 +406,9 @@ sync.addTemporaryListener(as, "type:m.room.tombstone", guard("m.room.tombstone", * @param {Ty.Event.StateOuter} event */ async event => { - await roomUpgrade.onTombstone(event) + if (event.state_key !== "") return + if (!event.content.replacement_room) return + await roomUpgrade.onTombstone(event, api) })) module.exports.stringifyErrorStack = stringifyErrorStack diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index e30ae6f..721c3bd 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -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) +}, 10*60*1000).unref() /** @param {Ty.Event.Outer} event */ function onReactionAdd(event) { diff --git a/src/matrix/room-upgrade.js b/src/matrix/room-upgrade.js index 7f0d4e6..6c344cf 100644 --- a/src/matrix/room-upgrade.js +++ b/src/matrix/room-upgrade.js @@ -4,12 +4,8 @@ const assert = require("assert/strict") const Ty = require("../types") const {Semaphore} = require("@chriscdn/promise-semaphore") const {tag} = require("@cloudrac3r/html-template-tag") -const {discord, db, sync, as, select, from} = require("../passthrough") +const {db, sync, select, from} = require("../passthrough") -/** @type {import("./api")}) */ -const api = sync.require("./api") -/** @type {import("../d2m/actions/create-room")}) */ -const createRoom = sync.require("../d2m/actions/create-room") /** @type {import("./utils")}) */ const utils = sync.require("./utils") @@ -17,11 +13,12 @@ const roomUpgradeSema = new Semaphore() /** * @param {Ty.Event.StateOuter} event + * @param {import("./api")} api */ -async function onTombstone(event) { - // Validate - if (event.state_key !== "") return - if (!event.content.replacement_room) return +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 @@ -48,13 +45,21 @@ async function onTombstone(event) { /** * @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) { +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 + 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 @@ -65,9 +70,6 @@ async function onBotMembership(event) { await api.joinRoom(newRoomID) } - const channelRow = from("channel_room").join("guild_space", "guild_id").where({room_id: oldRoomID}).select("space_id", "guild_id", "channel_id").get() - assert(channelRow) - // 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 @@ -75,7 +77,7 @@ async function onBotMembership(event) { // 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) {} + } catch (e) { /* c8 ignore next */ } // Update database db.transaction(() => { diff --git a/src/matrix/room-upgrade.test.js b/src/matrix/room-upgrade.test.js new file mode 100644 index 0000000..3de1a8f --- /dev/null +++ b/src/matrix/room-upgrade.test.js @@ -0,0 +1,169 @@ +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/web/routes/download-discord.js b/src/web/routes/download-discord.js index 7c8c8e7..3c58a75 100644 --- a/src/web/routes/download-discord.js +++ b/src/web/routes/download-discord.js @@ -83,3 +83,5 @@ 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 a801c2c..e4f4ab4 100644 --- a/src/web/routes/download-discord.test.js +++ b/src/web/routes/download-discord.test.js @@ -1,9 +1,10 @@ // @ts-check +const assert = require("assert").strict const tryToCatch = require("try-to-catch") const {test} = require("supertape") const {router} = require("../../../test/web") -const {MatrixServerError} = require("../../matrix/mreq") +const {_cache} = require("./download-discord") test("web download discord: access denied if not a known attachment", async t => { const [error] = await tryToCatch(() => @@ -12,19 +13,6 @@ test("web download discord: access denied if not a known attachment", async t => channel_id: "1", attachment_id: "2", file_name: "image.png" - }, - 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)}` - })) - } - } - } } }) ) @@ -43,7 +31,7 @@ test("web download discord: works if a known attachment", async t => { snow: { channel: { async refreshAttachmentURLs(attachments) { - if (typeof attachments === "string") attachments = [attachments] + assert(Array.isArray(attachments)) return { refreshed_urls: attachments.map(a => ({ original: a, @@ -70,7 +58,7 @@ test("web download discord: uses cache", async t => { event, snow: { channel: { - // @ts-ignore + /* c8 ignore next 4 */ async refreshAttachmentURLs(attachments) { notCalled = false throw new Error("tried to refresh when it should be in cache") @@ -80,3 +68,30 @@ test("web download discord: uses cache", async t => { }) 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/guild.js b/src/web/routes/guild.js index 6b80e9d..9037d40 100644 --- a/src/web/routes/guild.js +++ b/src/web/routes/guild.js @@ -73,23 +73,47 @@ function filterTo(xs, fn) { return filtered } +/** + * @param {{type: number, parent_id?: string, position?: number}} channel + * @param {Map} channels + */ +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) { + foundCategory = channels.get(foundCategory.parent_id) + } + if (foundCategory.type === DiscordTypes.ChannelType.GuildCategory) position = (foundCategory.position + 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 +} + /** * @param {DiscordTypes.APIGuild} guild * @param {Ty.R.Hierarchy[]} rooms * @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) @@ -97,7 +121,7 @@ function getChannelRoomsLinks(guild, rooms, roles) { 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) - getPosition(b.channel)) + linkedChannelsWithDetails.sort((a, b) => getPosition(a.channel, discord.channels) - getPosition(b.channel, discord.channels)) let unlinkedChannelIDs = channelIDs.filter(c => !linkedChannelIDs.includes(c)) /** @type {DiscordTypes.APIGuildChannel[]} */ // @ts-ignore @@ -107,7 +131,7 @@ function getChannelRoomsLinks(guild, rooms, roles) { const permissions = dUtils.getPermissions(roles, guild.roles, botID, c["permission_overwrites"]) return dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.ViewChannel) }) - unlinkedChannels.sort((a, b) => getPosition(a) - getPosition(b)) + unlinkedChannels.sort((a, b) => getPosition(a, discord.channels) - getPosition(b, discord.channels)) let linkedRoomIDs = linkedChannels.map(c => c.room_id) let unlinkedRooms = [...rooms] @@ -239,3 +263,6 @@ as.router.post("/api/invite", defineEventHandler(async event => { return sendRedirect(event, "/ok?msg=User has been invited.", 302) } })) + +module.exports._getPosition = getPosition +module.exports._filterTo = filterTo diff --git a/src/web/routes/guild.test.js b/src/web/routes/guild.test.js index 45869b3..e0b26df 100644 --- a/src/web/routes/guild.test.js +++ b/src/web/routes/guild.test.js @@ -1,8 +1,10 @@ // @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, _filterTo} = require("./guild") let nonce @@ -101,12 +103,6 @@ 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 [] } @@ -121,12 +117,6 @@ 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 [] } @@ -191,12 +181,12 @@ test("api invite: can invite with valid nonce", async t => { async getStateEvent(roomID, type, key) { called++ if (type === "m.room.member" && key === "@cadence:cadence.moe") { - return {membership: "leave"} + throw new Error("event not found") } else if (type === "m.room.power_levels" && key === "") { return {} - } else { - t.fail(`unexpected getStateEvent call. roomID: ${roomID}, type: ${type}, key: ${key}`) } + /* c8 ignore next */ + t.fail(`unexpected getStateEvent call. roomID: ${roomID}, type: ${type}, key: ${key}`) }, async getStateEventOuter(roomID, type, key) { called++ @@ -284,9 +274,9 @@ test("api invite: can invite to a moderated guild", async t => { return {membership: "leave"} } else if (type === "m.room.power_levels" && key === "") { return {} - } else { - t.fail(`unexpected getStateEvent call. roomID: ${roomID}, type: ${type}, key: ${key}`) } + /* c8 ignore next */ + t.fail(`unexpected getStateEvent call. roomID: ${roomID}, type: ${type}, key: ${key}`) }, async getStateEventOuter(roomID, type, key) { called++ @@ -362,3 +352,52 @@ 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"]) +}) + +test("filterTo: works", t => { + const fruit = ["apple", "banana", "apricot"] + const rest = _filterTo(fruit, f => f[0] === "b") + t.deepEqual(fruit, ["banana"]) + t.deepEqual(rest, ["apple", "apricot"]) +}) diff --git a/src/web/routes/link.js b/src/web/routes/link.js index 5193ded..ce80fd4 100644 --- a/src/web/routes/link.js +++ b/src/web/routes/link.js @@ -70,16 +70,14 @@ 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"}) + const inviteRow = select("invite", ["mxid", "type"], {mxid: session.data.mxid, room_id: spaceID}).get() + if (!inviteRow || inviteRow.type !== "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`}) - const inviteSender = select("invite", "mxid", {mxid: session.data.mxid, room_id: spaceID}).pluck().get() - const inviteSenderServer = inviteSender?.match(/:(.*)/)?.[1] - const via = [inviteSenderServer || ""] + const via = [inviteRow.mxid.match(/:(.*)/)[1]] // Check space exists and bridge is joined try { diff --git a/src/web/routes/log-in-with-matrix.test.js b/src/web/routes/log-in-with-matrix.test.js index a403055..830556e 100644 --- a/src/web/routes/log-in-with-matrix.test.js +++ b/src/web/routes/log-in-with-matrix.test.js @@ -39,7 +39,8 @@ test("log in with matrix: sends message to log in", async t => { let called = 0 await router.test("post", "/api/log-in-with-matrix", { body: { - mxid: "@cadence:cadence.moe" + mxid: "@cadence:cadence.moe", + next: "https://bridge.cadence.moe/guild?guild_id=123" }, api: { async usePrivateChat(mxid) { @@ -51,7 +52,7 @@ test("log in with matrix: sends message to log in", async t => { 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-]+)/)[1] + token = content.body.match(/log-in-with-matrix\?token=([a-f0-9-]+)&next=/)[1] t.ok(token, "log in token not issued") return "" } diff --git a/src/web/routes/oauth.js b/src/web/routes/oauth.js index fe35230..f4bb61f 100644 --- a/src/web/routes/oauth.js +++ b/src/web/routes/oauth.js @@ -2,12 +2,12 @@ const {z} = require("zod") const {randomUUID} = require("crypto") -const {defineEventHandler, getValidatedQuery, sendRedirect, createError} = require("h3") +const {defineEventHandler, getValidatedQuery, sendRedirect, createError, H3Event} = require("h3") const {SnowTransfer, tokenless} = require("snowtransfer") const DiscordTypes = require("discord-api-types/v10") const getRelativePath = require("get-relative-path") -const {discord, as, db, sync} = require("../../passthrough") +const {as, db, sync} = require("../../passthrough") const {id, permissions} = require("../../../addbot") /** @type {import("../auth")} */ const auth = sync.require("../auth") @@ -33,6 +33,24 @@ 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" @@ -61,21 +79,15 @@ 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 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 oauthResult = await getOauth2Token(event)(id, redirect_uri, reg.ooye.discord_client_secret, parsedQuery.data.code) + const parsedToken = schema.token.parse(oauthResult) - 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}) - } + 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}) // Set auto-create for the guild // @ts-ignore diff --git a/src/web/routes/oauth.test.js b/src/web/routes/oauth.test.js new file mode 100644 index 0000000..2f3a791 --- /dev/null +++ b/src/web/routes/oauth.test.js @@ -0,0 +1,121 @@ +// @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/test/data.js b/test/data.js index 387ad6a..e80b436 100644 --- a/test/data.js +++ b/test/data.js @@ -180,6 +180,39 @@ 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: [], @@ -5433,6 +5466,50 @@ 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 76c822f..36ec0b6 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -3,10 +3,12 @@ BEGIN TRANSACTION; INSERT INTO guild_active (guild_id, autocreate) VALUES ('112760669178241024', 1), ('66192955777486848', 1), -('665289423482519565', 0); +('665289423482519565', 0), +('1345641201902288987', 1); INSERT INTO guild_space (guild_id, space_id, privacy_level) VALUES -('112760669178241024', '!jjmvBegULiLucuWEHU:cadence.moe', 0); +('112760669178241024', '!jjmvBegULiLucuWEHU:cadence.moe', 0), +('1345641201902288987', '!CvQMeeqXIkgedUpkzv: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'), @@ -21,7 +23,8 @@ INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom ('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, '665289423482519565'), -('1438284564815548418', '!MHxNpwtgVqWOrmyoTn:cadence.moe', 'sin-cave', NULL, NULL, NULL, '665289423482519565'); +('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; @@ -78,7 +81,8 @@ WITH a (message_id, channel_id) AS (VALUES ('1339000288144658482', '176333891320283136'), ('1381212840957972480', '112760669178241024'), ('1401760355339862066', '112760669178241024'), -('1439351590262800565', '1438284564815548418')) +('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; INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES @@ -124,7 +128,9 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part ('$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); +('$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); INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'), @@ -155,7 +161,8 @@ 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'); +('288858540888686602', 'upstinky', 0, 'mxc://cadence.moe/mwZaCtRGAQQyOItagDeCocEO'), +('1437322787994992650', 'cx_marvelous', 0, 'mxc://cadence.moe/TPZdosVUjTIopsLijkygIbti'); INSERT INTO member_cache (room_id, mxid, displayname, avatar_url, power_level) VALUES ('!jjmvBegULiLucuWEHU:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL, 50), @@ -200,4 +207,10 @@ 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 6470aae..be7febf 100644 --- a/test/test.js +++ b/test/test.js @@ -29,6 +29,7 @@ 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" const sync = new HeatSync({watchFS: false}) @@ -140,6 +141,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../src/web/routes/info.test") require("../src/web/routes/link.test") require("../src/web/routes/log-in-with-matrix.test") + require("../src/web/routes/oauth.test") require("../src/web/routes/password.test") require("../src/discord/utils.test") require("../src/matrix/kstate.test") @@ -147,6 +149,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not 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") diff --git a/test/web.js b/test/web.js index 09af95b..463c6b1 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, api?: Partial, snow?: {[k in keyof SnowTransfer]?: Partial}, createRoom?: Partial, createSpace?: Partial, headers?: any}} [options] + * @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, headers?: any}} [options] */ async test(method, inputUrl, options = {}) { const url = new URL(inputUrl, "http://a") @@ -87,6 +87,8 @@ class Router { snow: options.snow, createRoom: options.createRoom, createSpace: options.createSpace, + getOauth2Token: options.getOauth2Token, + getClient: options.getClient, sessions: { h3: { id: "h3",