diff --git a/package-lock.json b/package-lock.json
index aa7822f..803fe53 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,7 @@
"license": "AGPL-3.0-or-later",
"dependencies": {
"@chriscdn/promise-semaphore": "^3.0.1",
- "@cloudrac3r/discord-markdown": "^2.6.6",
+ "@cloudrac3r/discord-markdown": "^2.6.5",
"@cloudrac3r/giframe": "^0.4.3",
"@cloudrac3r/html-template-tag": "^5.0.1",
"@cloudrac3r/in-your-element": "^1.1.1",
@@ -35,7 +35,7 @@
"lru-cache": "^11.0.2",
"prettier-bytes": "^1.0.4",
"sharp": "^0.33.4",
- "snowtransfer": "^0.15.0",
+ "snowtransfer": "^0.14.2",
"stream-mime-type": "^1.0.2",
"try-to-catch": "^3.0.1",
"uqr": "^0.1.2",
@@ -119,9 +119,9 @@
}
},
"node_modules/@chriscdn/promise-semaphore": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/@chriscdn/promise-semaphore/-/promise-semaphore-3.1.1.tgz",
- "integrity": "sha512-ALLLLYlPfd/QZLptcVi6HQRK1zaCDWZoqYYw+axLmCatFs4gVTSZ5nqlyxwFe4qwR/K84HvOMa9hxda881FqMA==",
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@chriscdn/promise-semaphore/-/promise-semaphore-3.0.1.tgz",
+ "integrity": "sha512-fVlCnoYE4hDzpcYRPtmN7dmcpmd2zxyPWjyfjIKI9Y+gsI7rwZSkjtuwMi8HFtlkSmNh8L7Zr37hdqeL13sYrw==",
"license": "MIT"
},
"node_modules/@cloudcmd/stub": {
@@ -225,9 +225,9 @@
}
},
"node_modules/@cloudrac3r/discord-markdown": {
- "version": "2.6.6",
- "resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.6.6.tgz",
- "integrity": "sha512-4FNO7WmACPvcTrQjeLQLr9WRuP7JDUVUGFrRJvmAjiMs2UlUAsShfSRuU2SCqz3QqmX8vyJ06wy2hkjTTyRtbw==",
+ "version": "2.6.5",
+ "resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.6.5.tgz",
+ "integrity": "sha512-B4uQNsyva5JNW0CVYkcunMQwWfrok1Hd5FYww/cWcvb98zp/pJdJfE3hoRl9EbnxNK2l62IJQ9j8HmssMFHJ9Q==",
"license": "MIT",
"dependencies": {
"simple-markdown": "^0.7.3"
@@ -949,9 +949,9 @@
"license": "MIT"
},
"node_modules/@stackoverflow/stacks": {
- "version": "2.8.4",
- "resolved": "https://registry.npmjs.org/@stackoverflow/stacks/-/stacks-2.8.4.tgz",
- "integrity": "sha512-FfA7Bw7a0AQrMw3/bG6G4BUrZ698F7Cdk6HkR9T7jdaufORkiX5d16wI4j4b5Sqm1FwkaZAF+ZSKLL1w0tAsew==",
+ "version": "2.8.3",
+ "resolved": "https://registry.npmjs.org/@stackoverflow/stacks/-/stacks-2.8.3.tgz",
+ "integrity": "sha512-ZGBeuXJC7moK/f+lgl2dCAW85etD/RO0DNubocdH2qzpJMuuGXX0GMeEAfrTOe+B00I8E1OqTnS1cpkqGdHBdQ==",
"license": "MIT",
"dependencies": {
"@hotwired/stimulus": "^3.2.2",
@@ -1107,9 +1107,9 @@
"dev": true
},
"node_modules/@types/node": {
- "version": "22.18.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.0.tgz",
- "integrity": "sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==",
+ "version": "22.17.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.1.tgz",
+ "integrity": "sha512-y3tBaz+rjspDTylNjAX37jEC3TETEFGNJL6uQDxwF9/8GLLIjW1rvVHlynyuUKMnMr1Roq8jOv3vkopBjC4/VA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1452,13 +1452,13 @@
}
},
"node_modules/cloudstorm": {
- "version": "0.14.1",
- "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.14.1.tgz",
- "integrity": "sha512-x95WCKg818E1rE1Ru45NPD3RoIq0pg3WxwvF0GE7Eq07pAeLcjSRqM1lUmbmfjdOqZrWdSRYA1NETVZ8QhVrIA==",
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.14.0.tgz",
+ "integrity": "sha512-EgjMGxb2Z+L6Acti6DzL/bEbR495AIqPThyW4DaG6Jpvd0ZuM5eC13EiyxV8wlqAME612QO2LjqbhkdXn/327Q==",
"license": "MIT",
"dependencies": {
- "discord-api-types": "^0.38.21",
- "snowtransfer": "^0.15.0"
+ "discord-api-types": "^0.38.12",
+ "snowtransfer": "^0.14.2"
},
"engines": {
"node": ">=22.0.0"
@@ -1616,9 +1616,9 @@
}
},
"node_modules/discord-api-types": {
- "version": "0.38.22",
- "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.22.tgz",
- "integrity": "sha512-2gnYrgXN3yTlv2cKBISI/A8btZwsSZLwKpIQXeI1cS8a7W7wP3sFVQOm3mPuuinTD8jJCKGPGNH399zE7Un1kA==",
+ "version": "0.38.19",
+ "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.19.tgz",
+ "integrity": "sha512-NUNMTgjYrgxt7wrTNEqnEez4hIAYbfyBpsjxT5gW7+82GjQCPDZvN+em6t+4/P5kGWnnwDa4ci070BV7eI6GbA==",
"license": "MIT",
"workspaces": [
"scripts/actions/documentation"
@@ -2719,12 +2719,12 @@
}
},
"node_modules/snowtransfer": {
- "version": "0.15.0",
- "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.15.0.tgz",
- "integrity": "sha512-kEDGKtFiH5nSkHsDZonEUuDx99lUasJoZ7AGrgvE8HzVG59vjvqc//C+pjWj4DuJqTj4Q+Z1L/M/MYNim8F2VA==",
+ "version": "0.14.2",
+ "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.14.2.tgz",
+ "integrity": "sha512-Fi8OdRmaIgeCj58oVej+tQAoY2I+Xp/6PAYV8X93jE/2E6Anc87SbTbDV6WZXCnuzTQz3gty8JOGz02qI7Qs9A==",
"license": "MIT",
"dependencies": {
- "discord-api-types": "^0.38.21"
+ "discord-api-types": "^0.38.8"
},
"engines": {
"node": ">=16.15.0"
@@ -3447,9 +3447,9 @@
}
},
"node_modules/zod": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.5.tgz",
- "integrity": "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==",
+ "version": "4.0.17",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.17.tgz",
+ "integrity": "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
diff --git a/package.json b/package.json
index 2fb21f2..a7d3eaa 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
},
"dependencies": {
"@chriscdn/promise-semaphore": "^3.0.1",
- "@cloudrac3r/discord-markdown": "^2.6.6",
+ "@cloudrac3r/discord-markdown": "^2.6.5",
"@cloudrac3r/giframe": "^0.4.3",
"@cloudrac3r/html-template-tag": "^5.0.1",
"@cloudrac3r/in-your-element": "^1.1.1",
@@ -44,7 +44,7 @@
"lru-cache": "^11.0.2",
"prettier-bytes": "^1.0.4",
"sharp": "^0.33.4",
- "snowtransfer": "^0.15.0",
+ "snowtransfer": "^0.14.2",
"stream-mime-type": "^1.0.2",
"try-to-catch": "^3.0.1",
"uqr": "^0.1.2",
diff --git a/src/d2m/actions/register-pk-user.js b/src/d2m/actions/register-pk-user.js
index 27e949c..b5e44e5 100644
--- a/src/d2m/actions/register-pk-user.js
+++ b/src/d2m/actions/register-pk-user.js
@@ -146,7 +146,7 @@ async function syncUser(messageID, author, roomID, shouldActuallySync) {
try {
// API lookup
var pkMessage = await fetchMessage(messageID)
- db.prepare("REPLACE INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES (?, ?, ?)").run(pkMessage.member.uuid, pkMessage.sender, author.username)
+ db.prepare("INSERT OR IGNORE INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES (?, ?, ?)").run(pkMessage.member.uuid, pkMessage.sender, author.username)
} catch (e) {
// Fall back to offline cache
const senderMxid = from("sim_proxy").join("sim", "user_id").join("sim_member", "mxid").where({displayname: author.username, room_id: roomID}).pluck("mxid").get()
diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js
index 1d6288a..a8e5a6b 100644
--- a/src/d2m/converters/message-to-event.js
+++ b/src/d2m/converters/message-to-event.js
@@ -33,10 +33,9 @@ function getDiscordParseCallbacks(message, guild, useHTML) {
user: node => {
const mxid = select("sim", "mxid", {user_id: node.id}).pluck().get()
const interaction = message.interaction_metadata || message.interaction
- const username = message.mentions?.find(ment => ment.id === node.id)?.username
- || message.referenced_message?.mentions?.find(ment => ment.id === node.id)?.username
+ const username = message.mentions.find(ment => ment.id === node.id)?.username
+ || message.referenced_message?.mentions.find(ment => ment.id === node.id)?.username
|| (interaction?.user.id === node.id ? interaction.user.username : null)
- || (message.author.id === node.id ? message.author.username : null)
|| node.id
if (mxid && useHTML) {
return `@${username}`
diff --git a/src/matrix/api.js b/src/matrix/api.js
index 41af63f..f9a85c0 100644
--- a/src/matrix/api.js
+++ b/src/matrix/api.js
@@ -22,7 +22,11 @@ function path(p, mxid, otherParams = {}) {
const u = new URL(p, "http://localhost")
if (mxid) u.searchParams.set("user_id", mxid)
for (const entry of Object.entries(otherParams)) {
- if (entry[1] != undefined) {
+ if (Array.isArray(entry[1])) {
+ for (const element of entry[1]) {
+ u.searchParams.append(entry[0], element)
+ }
+ } else if (entry[1] != undefined) {
u.searchParams.set(entry[0], entry[1])
}
}
@@ -62,11 +66,14 @@ async function createRoom(content) {
}
/**
+ * @param {string} roomIDOrAlias
+ * @param {string?} [mxid]
+ * @param {string[]?} [via]
* @returns {Promise} room ID
*/
-async function joinRoom(roomIDOrAlias, mxid) {
+async function joinRoom(roomIDOrAlias, mxid, via) {
/** @type {Ty.R.RoomJoined} */
- const root = await mreq.mreq("POST", path(`/client/v3/join/${roomIDOrAlias}`, mxid), {})
+ const root = await mreq.mreq("POST", path(`/client/v3/join/${roomIDOrAlias}`, mxid, {via}), {})
return root.room_id
}
diff --git a/src/matrix/api.test.js b/src/matrix/api.test.js
index 82565eb..da92385 100644
--- a/src/matrix/api.test.js
+++ b/src/matrix/api.test.js
@@ -24,3 +24,7 @@ test("api path: real world mxid", t => {
test("api path: extras number works", t => {
t.equal(path(`/client/v3/rooms/!example/timestamp_to_event`, null, {ts: 1687324651120}), "/client/v3/rooms/!example/timestamp_to_event?ts=1687324651120")
})
+
+test("api path: multiple via params", t => {
+ t.equal(path(`/client/v3/rooms/!example/join`, null, {via: ["cadence.moe", "matrix.org"], ts: 1687324651120}), "/client/v3/rooms/!example/join?via=cadence.moe&via=matrix.org&ts=1687324651120")
+})
diff --git a/src/types.d.ts b/src/types.d.ts
index 37da633..aaeed60 100644
--- a/src/types.d.ts
+++ b/src/types.d.ts
@@ -148,6 +148,14 @@ export namespace Event {
prev_content?: any
}
+ export type Outer_StrippedChildStateEvent = {
+ type: string
+ state_key: string
+ sender: string
+ origin_server_ts: number
+ content: any
+ }
+
export type M_Room_Message = {
msgtype: "m.text" | "m.emote"
body: string
@@ -344,7 +352,7 @@ export namespace R {
export type Hierarchy = {
avatar_url?: string
canonical_alias?: string
- children_state: {}
+ children_state: Event.Outer_StrippedChildStateEvent[]
guest_can_join: boolean
join_rule?: string
name?: string
diff --git a/src/web/routes/link.js b/src/web/routes/link.js
index c5f404e..d16ecea 100644
--- a/src/web/routes/link.js
+++ b/src/web/routes/link.js
@@ -12,6 +12,20 @@ const auth = sync.require("../auth")
const mreq = sync.require("../../matrix/mreq")
const {reg} = require("../../matrix/read-registration")
+/**
+ * @param {string} UserID
+ * @returns {string} the HS of the user, or "" if the user ID is malformed
+ */
+function getHSOfUser(user) {
+ domainStartIndex = user.indexOf(":");
+ if (domainStartIndex >= 1) {
+ return user.slice(domainStartIndex + 1)
+ }
+
+ return ""
+}
+
+
/**
* @param {H3Event} event
* @returns {import("../../matrix/api")}
@@ -75,10 +89,16 @@ as.router.post("/api/link-space", defineEventHandler(async event => {
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()
+ via = [ getHSOfUser(inviteSender) ]
+
// Check space exists and bridge is joined
try {
- await api.joinRoom(parsedBody.space_id)
+ await api.joinRoom(parsedBody.space_id, null, via)
} catch (e) {
+ if (via.join("") == "") {
+ throw createError({status: 403, message: "Unable To Join", data: `Unable to join the requested Matrix space. Please invite the bridge to the space and try again. (Server said: ${e.errcode} - ${e.message})`})
+ }
throw createError({status: 403, message: e.errcode, data: `${e.errcode} - ${e.message}`})
}
@@ -134,19 +154,33 @@ as.router.post("/api/link", defineEventHandler(async event => {
if (row) throw createError({status: 400, message: "Bad Request", data: `Channel ID ${row.channel_id} or room ID ${parsedBody.matrix} are already bridged and cannot be reused`})
// Check room is part of the guild's space
- let found = false
+ let foundRoom = false
+ /** @type {string[]?} */
+ let foundVia = null
for await (const room of api.generateFullHierarchy(spaceID)) {
- if (room.room_id === parsedBody.matrix && !room.room_type) {
- found = true
- break
+ // When finding a space during iteration, look at space's children state, because we need a `via` to join the room (when we find it later)
+ for (const state of room.children_state) {
+ if (state.type === "m.space.child" && state.state_key === parsedBody.matrix) {
+ foundVia = state.content.via
+ }
}
+
+ // When finding a room during iteration, see if it was the requested room (to confirm that the room is in the space)
+ if (room.room_id === parsedBody.matrix && !room.room_type) {
+ foundRoom = true
+ }
+
+ if (foundRoom && foundVia) break
}
- if (!found) throw createError({status: 400, message: "Bad Request", data: "Matrix room needs to be part of the bridged space"})
+ if (!foundRoom) throw createError({status: 400, message: "Bad Request", data: "Matrix room needs to be part of the bridged space"})
// Check room exists and bridge is joined
try {
- await api.joinRoom(parsedBody.matrix)
+ await api.joinRoom(parsedBody.matrix, null, foundVia)
} catch (e) {
+ if (!foundVia) {
+ throw createError({status: 403, message: "Unable To Join", data: `Unable to join the requested Matrix room. Please invite the bridge to the room and try again. (Server said: ${e.errcode} - ${e.message})`})
+ }
throw createError({status: 403, message: e.errcode, data: `${e.errcode} - ${e.message}`})
}
diff --git a/src/web/routes/link.test.js b/src/web/routes/link.test.js
index 0d8d366..068bc9b 100644
--- a/src/web/routes/link.test.js
+++ b/src/web/routes/link.test.js
@@ -360,7 +360,7 @@ test("web link room: check that room is part of space (not in hierarchy)", async
t.equal(called, 1)
})
-test("web link room: check that bridge can join room", async t => {
+test("web link room: check that bridge can join room (notices lack of via and asks for invite instead)", async t => {
let called = 0
const [error] = await tryToCatch(() => router.test("post", "/api/link", {
sessionData: {
@@ -381,7 +381,55 @@ test("web link room: check that bridge can join room", async t => {
t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
yield {
room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
- children_state: {},
+ children_state: [],
+ guest_can_join: false,
+ num_joined_members: 2
+ }
+ /* c8 ignore next */
+ }
+ }
+ }))
+ t.equal(error.data, "Unable to join the requested Matrix room. Please invite the bridge to the room and try again. (Server said: M_FORBIDDEN - not allowed to join I guess)")
+ t.equal(called, 2)
+})
+
+test("web link room: check that bridge can join room (uses via for join attempt)", async t => {
+ let called = 0
+ const [error] = await tryToCatch(() => router.test("post", "/api/link", {
+ sessionData: {
+ managedGuilds: ["665289423482519565"]
+ },
+ body: {
+ discord: "665310973967597573",
+ matrix: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
+ guild_id: "665289423482519565"
+ },
+ api: {
+ async joinRoom(roomID, _, via) {
+ called++
+ t.deepEqual(via, ["cadence.moe", "hashi.re"])
+ throw new MatrixServerError({errcode: "M_FORBIDDEN", error: "not allowed to join I guess"})
+ },
+ async *generateFullHierarchy(spaceID) {
+ called++
+ t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
+ yield {
+ room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
+ children_state: [],
+ guest_can_join: false,
+ num_joined_members: 2
+ }
+ yield {
+ room_id: "!zTMspHVUBhFLLSdmnS:cadence.moe",
+ children_state: [{
+ type: "m.space.child",
+ state_key: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
+ sender: "@elliu:hashi.re",
+ content: {
+ via: ["cadence.moe", "hashi.re"]
+ },
+ origin_server_ts: 0
+ }],
guest_can_join: false,
num_joined_members: 2
}
@@ -414,7 +462,7 @@ test("web link room: check that bridge has PL 100 in target room (event missing)
t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
yield {
room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
- children_state: {},
+ children_state: [],
guest_can_join: false,
num_joined_members: 2
}
@@ -454,7 +502,7 @@ test("web link room: check that bridge has PL 100 in target room (users default)
t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
yield {
room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
- children_state: {},
+ children_state: [],
guest_can_join: false,
num_joined_members: 2
}
@@ -494,7 +542,7 @@ test("web link room: successfully calls createRoom", async t => {
t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
yield {
room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
- children_state: {},
+ children_state: [],
guest_can_join: false,
num_joined_members: 2
}
diff --git a/test/test.js b/test/test.js
index 3695a84..8d9ad16 100644
--- a/test/test.js
+++ b/test/test.js
@@ -17,6 +17,8 @@ const {reg} = require("../src/matrix/read-registration")
reg.ooye.discord_token = "Njg0MjgwMTkyNTUzODQ0NzQ3.Xl3zlw.baby"
reg.ooye.server_origin = "https://matrix.cadence.moe" // so that tests will pass even when hard-coded
reg.ooye.server_name = "cadence.moe"
+reg.ooye.namespace_prefix = "_ooye_"
+reg.sender_localpart = "_ooye_bot"
reg.id = "baby"
reg.as_token = "don't actually take authenticated actions on the server"
reg.hs_token = "don't actually take authenticated actions on the server"