More code coverage

This commit is contained in:
Cadence Ember
2026-01-10 02:28:18 +13:00
parent 513e67189e
commit 505c41a35e
23 changed files with 735 additions and 102 deletions

View File

@@ -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"
}
}

View File

@@ -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, {

View File

@@ -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 {

View File

@@ -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, <img data-mx-emoticon height="32" src="mxc://cadence.moe/TPZdosVUjTIopsLijkygIbti" title=":cx_marvelous:" alt=":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: `<blockquote><a href="https://matrix.to/#/!mHmhQQPwXNananaOLD:cadence.moe/$pgzCQjq_y5sy8RvWOUuoF3obNHjs8iNvt9c-odrOCPY">In reply to</a> <a href="https://matrix.to/#/@cadence:cadence.moe">Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆</a><br>[Media]</blockquote>cross-room reply`,
"m.mentions": {
user_ids: [
"@cadence:cadence.moe"
]
}
}
])
})

View File

@@ -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)

View File

@@ -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")
})

View File

@@ -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")
})

View File

@@ -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<Ty.Event.M_Room_Tombstone>} 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

View File

@@ -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<Ty.Event.M_Reaction>} event */
function onReactionAdd(event) {

View File

@@ -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<Ty.Event.M_Room_Tombstone>} 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<Ty.Event.M_Room_Member>} event
* @param {import("./api")} api
* @param {import("../d2m/actions/create-room")} createRoom
* @returns {Promise<boolean>} 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(() => {

View File

@@ -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 <strong>winners</strong>. To keep bridging, I need you to invite me to the new room: <a href="https://matrix.to/#/!JBxeGYnzQwLnaooNEW:cadence.moe">https://matrix.to/#/!JBxeGYnzQwLnaooNEW:cadence.moe</a>`
})
}
})
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)
})

View File

@@ -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

View File

@@ -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)
})

View File

@@ -73,23 +73,47 @@ function filterTo(xs, fn) {
return filtered
}
/**
* @param {{type: number, parent_id?: string, position?: number}} channel
* @param {Map<string, {type: number, parent_id?: string, position?: number}>} 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

View File

@@ -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"])
})

View File

@@ -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 {

View File

@@ -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 ""
}

View File

@@ -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<DiscordTypes.RESTGetAPICurrentUserGuildsResult>}}}
*/
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

View File

@@ -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")
})

View File

@@ -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,

View File

@@ -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;

View File

@@ -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")

View File

@@ -51,7 +51,7 @@ class Router {
/**
* @param {string} method
* @param {string} inputUrl
* @param {{event?: any, params?: any, body?: any, sessionData?: any, api?: Partial<import("../src/matrix/api")>, snow?: {[k in keyof SnowTransfer]?: Partial<SnowTransfer[k]>}, createRoom?: Partial<import("../src/d2m/actions/create-room")>, createSpace?: Partial<import("../src/d2m/actions/create-space")>, headers?: any}} [options]
* @param {{event?: any, params?: any, body?: any, sessionData?: any, getOauth2Token?: any, getClient?: (string) => {user: {getGuilds: () => Promise<DiscordTypes.RESTGetAPICurrentUserGuildsResult>}}, api?: Partial<import("../src/matrix/api")>, snow?: {[k in keyof SnowTransfer]?: Partial<SnowTransfer[k]>}, createRoom?: Partial<import("../src/d2m/actions/create-room")>, createSpace?: Partial<import("../src/d2m/actions/create-space")>, 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",