From ae57fa28016722744c696d63dbecad1893510313 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 1 Feb 2025 22:03:41 +1300 Subject: [PATCH 1/4] Only announce if they can reasonably type here --- src/web/routes/link.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/web/routes/link.js b/src/web/routes/link.js index efc474f..9db39d7 100644 --- a/src/web/routes/link.js +++ b/src/web/routes/link.js @@ -3,6 +3,7 @@ const {z} = require("zod") const {defineEventHandler, useSession, createError, readValidatedBody, setResponseHeader} = require("h3") const Ty = require("../../types") +const DiscordTypes = require("discord-api-types/v10") const {discord, db, as, sync, select, from} = require("../../passthrough") /** @type {import("../../d2m/actions/create-space")} */ @@ -64,10 +65,12 @@ as.router.post("/api/link", defineEventHandler(async event => { await createRoom.syncRoom(parsedBody.discord) // Send a notification in the room - await api.sendEvent(parsedBody.matrix, "m.room.message", { - msgtype: "m.notice", - body: "👋 This room is now bridged with Discord. Say hi!" - }) + if (channel.type === DiscordTypes.ChannelType.GuildText) { + await api.sendEvent(parsedBody.matrix, "m.room.message", { + msgtype: "m.notice", + body: "👋 This room is now bridged with Discord. Say hi!" + }) + } setResponseHeader(event, "HX-Refresh", "true") return null // 204 From 17ea92a8c2b229f27643edee4c1ddc32e3da5d47 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 1 Feb 2025 22:11:32 +1300 Subject: [PATCH 2/4] Fix unlinking left rooms --- src/d2m/actions/create-room.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index 066d3fa..f72f6ef 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -11,6 +11,8 @@ const {discord, sync, db, select, from} = passthrough const file = sync.require("../../matrix/file") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") +/** @type {import("../../matrix/mreq")} */ +const mreq = sync.require("../../matrix/mreq") /** @type {import("../../matrix/kstate")} */ const ks = sync.require("../../matrix/kstate") /** @type {import("../../discord/utils")} */ @@ -412,9 +414,20 @@ async function unbridgeDeletedChannel(channel, guildID) { const row = from("guild_space").join("guild_active", "guild_id").select("space_id", "autocreate").get() assert.ok(row) + let botInRoom = true + // remove declaration that the room is bridged - await api.sendState(roomID, "uk.half-shot.bridge", `moe.cadence.ooye://discord/${guildID}/${channel.id}`, {}) - if ("topic" in channel) { + try { + await api.sendState(roomID, "uk.half-shot.bridge", `moe.cadence.ooye://discord/${guildID}/${channel.id}`, {}) + } catch (e) { + if (String(e).includes("not in room")) { + botInRoom = false + } else { + throw e + } + } + + if (botInRoom && "topic" in channel) { // previously the Matrix topic would say the channel ID. we should remove that await api.sendState(roomID, "m.room.topic", "", {topic: channel.topic || ""}) } @@ -430,6 +443,8 @@ async function unbridgeDeletedChannel(channel, guildID) { db.prepare("DELETE FROM member_cache WHERE room_id = ?").run(roomID) db.prepare("DELETE FROM channel_room WHERE room_id = ? AND channel_id = ?").run(roomID, channel.id) // cascades to most other tables, like messages + if (!botInRoom) return + // demote admins in room /** @type {Ty.Event.M_Power_Levels} */ const powerLevelContent = await api.getStateEvent(roomID, "m.room.power_levels", "") From eec8b0f15bf6f78ecae228ed8ba38333c3e27f74 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 1 Feb 2025 22:27:27 +1300 Subject: [PATCH 3/4] Add loading indicator to invite screens --- src/web/pug/guild.pug | 6 ++---- src/web/pug/invite.pug | 6 +++--- src/web/pug/ok.pug | 2 +- src/web/routes/guild.js | 5 +++-- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index 1d564dc..050e2c1 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -56,7 +56,7 @@ block body .fl-grow1 h2.fs-headline1 Invite a Matrix user - form.d-grid.g-af-column.gy4.gx8.jc-start(method="post" action="/api/invite" style="grid-template-rows: repeat(2, auto)") + form.d-grid.g-af-column.gy4.gx8.jc-start(method="post" action="/api/invite" hx-post="/api/invite" hx-indicator="#invite-button") label.s-label(for="mxid") Matrix ID input.fl-grow1.s-input.wmx3#mxid(name="mxid" required placeholder="@user:example.org") label.s-label(for="permissions") Permissions @@ -67,12 +67,10 @@ block body option(value="admin") Admin input(type="hidden" name="guild_id" value=guild_id) .grid--row-start2 - button.s-btn.s-btn__filled Invite + button.s-btn.s-btn__filled#invite-button Invite div != svg - h2.mt48.fs-headline1 Moderation - h2.mt48.fs-headline1 Matrix setup h3.mt32.fs-category Linked channels diff --git a/src/web/pug/invite.pug b/src/web/pug/invite.pug index 7f7ff2e..8cb977b 100644 --- a/src/web/pug/invite.pug +++ b/src/web/pug/invite.pug @@ -13,11 +13,11 @@ block body .s-page-title.mb24 h1.s-page-title--header= guild.name - .d-flex.g16 + .d-flex.g16#form-container .fl-grow1 h2.fs-headline1 Invite a Matrix user - form.d-flex.gy16.fd-column(method="post" action="/api/invite" style="grid-template-rows: repeat(2, auto)") + form.d-flex.gy16.fd-column(method="post" action="/api/invite" hx-post="/api/invite" hx-indicator="#invite-button" hx-select="#ok" hx-target="#form-container") .d-flex.gy4.fd-column label.s-label(for="mxid") Matrix ID input.fl-grow1.s-input.wmx3#mxid(name="mxid" required placeholder="@user:example.org") @@ -30,4 +30,4 @@ block body option(value="admin") Admin input(type="hidden" name="nonce" value=nonce) div - button.s-btn.s-btn__filled.htmx-indicator Invite + button.s-btn.s-btn__filled#invite-button Invite diff --git a/src/web/pug/ok.pug b/src/web/pug/ok.pug index 9aed737..dee4ed8 100644 --- a/src/web/pug/ok.pug +++ b/src/web/pug/ok.pug @@ -1,6 +1,6 @@ extends includes/template.pug block body - .ta-center.wmx5.p48.mx-auto + .ta-center.wmx5.p48.mx-auto#ok != icons.Spots.SpotApproveXL p.mt24.fs-body2= msg diff --git a/src/web/routes/guild.js b/src/web/routes/guild.js index 8a1cbc2..3d0bf6f 100644 --- a/src/web/routes/guild.js +++ b/src/web/routes/guild.js @@ -2,7 +2,7 @@ const assert = require("assert/strict") const {z} = require("zod") -const {H3Event, defineEventHandler, sendRedirect, useSession, createError, getValidatedQuery, readValidatedBody} = require("h3") +const {H3Event, defineEventHandler, sendRedirect, useSession, createError, getValidatedQuery, readValidatedBody, setResponseHeader} = require("h3") const {randomUUID} = require("crypto") const {LRUCache} = require("lru-cache") const Ty = require("../../types") @@ -191,9 +191,10 @@ as.router.post("/api/invite", defineEventHandler(async event => { ( parsedBody.permissions === "admin" ? 100 : parsedBody.permissions === "moderator" ? 50 : 0) - await api.setUserPowerCascade(spaceID, parsedBody.mxid, powerLevel) + if (powerLevel) await api.setUserPowerCascade(spaceID, parsedBody.mxid, powerLevel) if (parsedBody.guild_id) { + setResponseHeader(event, "HX-Refresh", true) return sendRedirect(event, `/guild?guild_id=${guild_id}`, 302) } else { return sendRedirect(event, "/ok?msg=User has been invited.", 302) From ad510794487cb6c27235f747141d2c21513d597c Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 1 Feb 2025 23:12:50 +1300 Subject: [PATCH 4/4] Don't overwrite room custom topics --- package-lock.json | 15 +++++++------- package.json | 2 +- src/d2m/actions/create-room.js | 5 ++++- src/d2m/actions/create-room.test.js | 20 +++++++++++++++++++ .../0018-add-custom-topic-to-channel-room.sql | 5 +++++ src/db/orm-defs.d.ts | 2 ++ src/m2d/event-dispatcher.js | 11 ++++++++++ src/types.d.ts | 4 ++++ src/web/routes/guild.test.js | 12 ++--------- 9 files changed, 57 insertions(+), 19 deletions(-) create mode 100644 src/db/migrations/0018-add-custom-topic-to-channel-room.sql diff --git a/package-lock.json b/package-lock.json index 1852245..7fd76a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@cloudrac3r/giframe": "^0.4.3", "@cloudrac3r/html-template-tag": "^5.0.1", "@cloudrac3r/in-your-element": "^1.0.0", - "@cloudrac3r/mixin-deep": "^3.0.0", + "@cloudrac3r/mixin-deep": "^3.0.1", "@cloudrac3r/pngjs": "^7.0.3", "@cloudrac3r/pug": "^4.0.4", "@cloudrac3r/turndown": "^7.1.4", @@ -281,9 +281,10 @@ } }, "node_modules/@cloudrac3r/mixin-deep": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@cloudrac3r/mixin-deep/-/mixin-deep-3.0.0.tgz", - "integrity": "sha512-yQz1wHSZbHfbKaGSjrV3wIG0e9MnElKlmekMKJPRdTn2jhF2Mt8wfMPX8U7v6rTyzR/7BTrX8CCUcrJMLgoQqw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@cloudrac3r/mixin-deep/-/mixin-deep-3.0.1.tgz", + "integrity": "sha512-awxfIraHjJ/URNlZ0ROc78Tdjtfk/fM/Gnj1embfrSN08h/HpRtLmPc3xlG3T2vFAy1AkONaebd52u7o6kDaYw==", + "license": "MIT", "engines": { "node": ">=6" } @@ -3217,9 +3218,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", - "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==", + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", + "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==", "license": "MIT", "engines": { "node": ">=18.17" diff --git a/package.json b/package.json index 1705c02..1d2e411 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "@cloudrac3r/giframe": "^0.4.3", "@cloudrac3r/html-template-tag": "^5.0.1", "@cloudrac3r/in-your-element": "^1.0.0", - "@cloudrac3r/mixin-deep": "^3.0.0", + "@cloudrac3r/mixin-deep": "^3.0.1", "@cloudrac3r/pngjs": "^7.0.3", "@cloudrac3r/pug": "^4.0.4", "@cloudrac3r/turndown": "^7.1.4", diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index f72f6ef..cf785ac 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -89,9 +89,10 @@ async function channelToKState(channel, guild, di) { assert(typeof parentSpaceID === "string") } - const channelRow = select("channel_room", ["nick", "custom_avatar"], {channel_id: channel.id}).get() + const channelRow = select("channel_room", ["nick", "custom_avatar", "custom_topic"], {channel_id: channel.id}).get() const customName = channelRow?.nick const customAvatar = channelRow?.custom_avatar + const hasCustomTopic = channelRow?.custom_topic const [convertedName, convertedTopic] = convertNameAndTopic(channel, guild, customName) const avatarEventContent = {} @@ -167,6 +168,8 @@ async function channelToKState(channel, guild, di) { } } + if (hasCustomTopic) delete channelKState["m.room.topic/"] + return {spaceID: parentSpaceID, privacyLevel, channelKState} } diff --git a/src/d2m/actions/create-room.test.js b/src/d2m/actions/create-room.test.js index a1766dd..0376d65 100644 --- a/src/d2m/actions/create-room.test.js +++ b/src/d2m/actions/create-room.test.js @@ -94,6 +94,26 @@ test("channel2room: room where limited people can mention everyone", async t => t.equal(called, 1) }) +test("channel2room: matrix room that already has a custom topic set", async t => { + let called = 0 + async function getStateEvent(roomID, type, key) { // getting power levels from space to apply to room + called++ + t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return {} + } + db.prepare("UPDATE channel_room SET custom_topic = 1 WHERE channel_id = ?").run(testData.channel.general.id) + const expected = mixin({}, testData.room.general, {"m.room.power_levels/": {notifications: {room: 20}}}) + // @ts-ignore + delete expected["m.room.topic/"] + t.deepEqual( + kstateStripConditionals(await channelToKState(testData.channel.general, testData.guild.general, {api: {getStateEvent}}).then(x => x.channelKState)), + expected + ) + t.equal(called, 1) +}) + test("convertNameAndTopic: custom name and topic", t => { t.deepEqual( _convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:", type: 0}, {id: "456"}, "hauntings"), diff --git a/src/db/migrations/0018-add-custom-topic-to-channel-room.sql b/src/db/migrations/0018-add-custom-topic-to-channel-room.sql new file mode 100644 index 0000000..c33d21c --- /dev/null +++ b/src/db/migrations/0018-add-custom-topic-to-channel-room.sql @@ -0,0 +1,5 @@ +BEGIN TRANSACTION; + +ALTER TABLE channel_room ADD COLUMN custom_topic INTEGER DEFAULT 0; + +COMMIT; diff --git a/src/db/orm-defs.d.ts b/src/db/orm-defs.d.ts index c235e99..a272625 100644 --- a/src/db/orm-defs.d.ts +++ b/src/db/orm-defs.d.ts @@ -10,6 +10,8 @@ export type Models = { speedbump_id: string | null speedbump_webhook_id: string | null speedbump_checked: number | null + guild_id: string | null + custom_topic: number } event_message: { diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index fc2a558..ac44a19 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -164,6 +164,17 @@ async event => { db.prepare("UPDATE channel_room SET nick = ? WHERE room_id = ?").run(name, event.room_id) })) +sync.addTemporaryListener(as, "type:m.room.topic", guard("m.room.topic", +/** + * @param {Ty.Event.StateOuter} event + */ +async event => { + if (event.state_key !== "") return + if (utils.eventSenderIsFromDiscord(event.sender)) return + const customTopic = +!!event.content.topic + db.prepare("UPDATE channel_room SET custom_topic = ? WHERE room_id = ?").run(customTopic, event.room_id) +})) + sync.addTemporaryListener(as, "type:m.room.pinned_events", guard("m.room.pinned_events", /** * @param {Ty.Event.StateOuter} event diff --git a/src/types.d.ts b/src/types.d.ts index 84aad44..cc33a4a 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -243,6 +243,10 @@ export namespace Event { name?: string } + export type M_Room_Topic = { + topic?: string + } + export type M_Room_PinnedEvents = { pinned: string[] } diff --git a/src/web/routes/guild.test.js b/src/web/routes/guild.test.js index 3952d14..a95cbea 100644 --- a/src/web/routes/guild.test.js +++ b/src/web/routes/guild.test.js @@ -177,16 +177,12 @@ test("api invite: can invite to a moderated guild", async t => { async inviteToRoom(roomID, mxidToInvite, mxid) { t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") called++ - }, - async setUserPowerCascade(roomID, mxid, power) { - t.equal(power, 0) - called++ } } }) ) t.notOk(error) - t.equal(called, 3) + t.equal(called, 2) }) test("api invite: does not reinvite joined users", async t => { @@ -205,14 +201,10 @@ test("api invite: does not reinvite joined users", async t => { async getStateEvent(roomID, type, key) { called++ return {membership: "join"} - }, - async setUserPowerCascade(roomID, mxid, power) { - t.equal(power, 0) - called++ } } }) ) t.notOk(error) - t.equal(called, 2) + t.equal(called, 1) })