diff --git a/src/db/migrations/0041-room-mention-role.sql b/src/db/migrations/0041-room-mention-role.sql new file mode 100644 index 0000000..d8a6280 --- /dev/null +++ b/src/db/migrations/0041-room-mention-role.sql @@ -0,0 +1,5 @@ +BEGIN TRANSACTION; + +ALTER TABLE channel_room ADD COLUMN room_mention_role TEXT; + +COMMIT; diff --git a/src/db/orm-defs.d.ts b/src/db/orm-defs.d.ts index ff63d32..2376627 100644 --- a/src/db/orm-defs.d.ts +++ b/src/db/orm-defs.d.ts @@ -21,6 +21,7 @@ export type Models = { speedbump_checked: number | null guild_id: string | null custom_topic: number + room_mention_role: string | null } channel_speedbump: { diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 5fe1109..77e984c 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -460,14 +460,15 @@ async function handleRoomOrMessageLinks(input, di) { async function checkWrittenMentions(content, senderMxid, roomID, guild, di) { let writtenMentionMatch = content.match(/(?:^|[^"[<>/A-Za-z0-9])@([A-Za-z][A-Za-z0-9._\[\]\(\)-]+):?/d) // /d flag for indices requires node.js 16+ if (writtenMentionMatch) { - if (writtenMentionMatch[1] === "room") { // convert @room to @everyone + if (writtenMentionMatch[1] === "room") { // convert @room to discord const {powers: {[senderMxid]: userPower}, powerLevels} = await mxUtils.getEffectivePower(roomID, [senderMxid], di.api) if (userPower >= (powerLevels.notifications?.room ?? 50)) { + const roleID = select("channel_room", "room_mention_role", {room_id: roomID}).pluck().get() return { // @ts-ignore - typescript doesn't know about indices yet - content: content.slice(0, writtenMentionMatch.indices[1][0]-1) + `@everyone` + content.slice(writtenMentionMatch.indices[1][1]), + content: content.slice(0, writtenMentionMatch.indices[1][0]-1) + (roleID ? `<@&${roleID}>` : "@everyone") + content.slice(writtenMentionMatch.indices[1][1]), ensureJoined: [], - allowedMentionsParse: ["everyone"], + allowedMentionsParse: roleID ? [] : ["everyone"], allowedMentionsUsers: [] } } @@ -860,6 +861,8 @@ async function eventToMessage(event, guild, channel, di) { } }) + input = input.replace(/]*href="https:\/\/matrix\.to\/#\/(?:!|%21|#|%23)[^"\/]*"[^>]*>@room<\/a>/g, "@room") + // Handling mentions of rooms and room-messages input = await handleRoomOrMessageLinks(input, di) diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index b262a37..b8d88f9 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -4,7 +4,7 @@ const DiscordTypes = require("discord-api-types/v10") const {eventToMessage} = require("./event-to-message") const data = require("../../../test/data") const {MatrixServerError} = require("../../matrix/mreq") -const {select, discord} = require("../../passthrough") +const {select, discord, db} = require("../../passthrough") /** * @param {string} roomID diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index c464954..1b28b46 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -188,6 +188,72 @@ block body td(colspan="3") .s-empty-state No channels linked between Discord and Matrix yet... + if space_id && linkedChannelsWithDetails.length + - const customMentionRooms = linkedChannelsWithDetails.filter(row => row.room_mention_role) + - const defaultMentionRooms = linkedChannelsWithDetails.filter(row => !row.room_mention_role) + - const mentionRoles = [...guild.roles].filter(role => role.id !== guild_id).sort((a, b) => b.position - a.position) + h3.mt32.fs-category#room-notifications Room notifications + .s-card.p0 + p.s-description.m12 Choose which Discord role receives Matrix @room notifications + if customMentionRooms.length + .s-table-container(style="container-type: inline-size") + table.s-table.s-table__bx-simple(style="table-layout: fixed") + thead + tr + th(scope="col" style="width: calc(50cqw - 40px)") Channel + th.pr12(scope="col") Notify role + tbody + each row in customMentionRooms + tr + td.va-middle + +discord(row.channel) + td.pr12 + .d-flex.ai-start.g8 + form.fl-grow1.wmn0.d-flex.ai-stretch.g8(method="post" action=rel("/api/room-mention-role")) + input(type="hidden" name="guild_id" value=guild_id) + input(type="hidden" name="channel_id" value=row.channel.id) + .s-select.fl-grow1.wmn0 + select.w100(name="room_mention_role" id="mention-role-" + row.channel.id aria-label="Notify role for #" + row.channel.name onchange="this.form.requestSubmit()" style="height: 32px") + if !mentionRoles.some(role => role.id === row.room_mention_role) + option(value=row.room_mention_role selected disabled) Deleted role (choose a replacement) + each role in mentionRoles + option(value=role.id selected=row.room_mention_role === role.id)= "@" + role.name + button.s-btn.s-btn__outlined.py0.box-border(type="submit" data-room-mention-save style="height: 32px; min-height: 0") Save + form.d-flex.ai-stretch(method="post" action=rel("/api/room-mention-role")) + input(type="hidden" name="guild_id" value=guild_id) + input(type="hidden" name="channel_id" value=row.channel.id) + input(type="hidden" name="room_mention_role" value="") + button.s-btn.s-btn__danger.s-btn__outlined.py0(type="submit" aria-label="Remove custom role for #" + row.channel.name style="height: 32px") Remove + + script. + document.querySelectorAll('[data-room-mention-save]').forEach(button => button.remove()) + + if defaultMentionRooms.length && mentionRoles.length + details.mt16.mx12.mb12 + summary.s-label.cursor-pointer Add custom role + form.mt16(method="post" action=rel("/api/room-mention-role")) + input(type="hidden" name="guild_id" value=guild_id) + .d-flex.fw-wrap.ai-end.g16 + .fl-grow1.wmn0(style="flex-basis: 0; min-width: 180px") + label.s-label(for="mention-role-channel") Channel + .s-select + select.w100.box-border#mention-role-channel(name="channel_id" required style="height: 32px") + option(value="" selected disabled) Choose a channel + each row in defaultMentionRooms + - const parent = discord.channels.get(row.channel.parent_id) + option(value=row.channel.id)= "#" + row.channel.name + (parent ? " - " + parent.name : "") + .fl-grow1.wmn0(style="flex-basis: 0; min-width: 180px") + label.s-label(for="mention-role-new") Notify role + .s-select + select.w100.box-border#mention-role-new(name="room_mention_role" required style="height: 32px") + option(value="" selected disabled) Choose a role + each role in mentionRoles + option(value=role.id)= "@" + role.name + .d-flex.ai-stretch(style="height: 32px") + button.s-btn.s-btn__icon.s-btn__filled.d-inline-flex.ai-center.g4.py0.box-border(type="submit" style="min-height: 0") + != icons.Icons.IconPlusSm + span Add + h3.fs-category.mt32 Auto-create .s-card.d-grid.px0 form.d-flex.ai-center.g16 diff --git a/src/web/routes/guild-settings.js b/src/web/routes/guild-settings.js index 9bfff3d..9f626d7 100644 --- a/src/web/routes/guild-settings.js +++ b/src/web/routes/guild-settings.js @@ -140,3 +140,22 @@ as.router.post("/api/default-roles", defineEventHandler(async event => { return sendRedirect(event, `/guild?guild_id=${guildID}`, 302) } })) + +// An empty role restores @everyone +as.router.post("/api/room-mention-role", defineEventHandler(async event => { + const body = await readValidatedBody(event, z.object({ + guild_id: z.string(), + channel_id: z.string(), + room_mention_role: z.string().regex(/^(?:[0-9]+)?$/) + }).parse) + const managed = await auth.getManagedGuilds(event) + if (!managed.has(body.guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"}) + const room = select("channel_room", "room_id", {channel_id: body.channel_id, guild_id: body.guild_id}).get() + if (!room) throw createError({status: 404, message: "Linked channel not found"}) + const guild = discord.guilds.get(body.guild_id) + if (body.room_mention_role && (!guild?.roles.some(role => role.id === body.room_mention_role) || body.room_mention_role === body.guild_id)) { + throw createError({status: 400, message: "Choose a role from this server"}) + } + db.prepare("UPDATE channel_room SET room_mention_role = ? WHERE channel_id = ? AND guild_id = ?").run(body.room_mention_role || null, body.channel_id, body.guild_id) + return sendRedirect(event, `/guild?guild_id=${body.guild_id}#room-notifications`, 302) +})) diff --git a/src/web/routes/guild-settings.test.js b/src/web/routes/guild-settings.test.js index 00acb89..d1a978b 100644 --- a/src/web/routes/guild-settings.test.js +++ b/src/web/routes/guild-settings.test.js @@ -2,7 +2,7 @@ const {tryToCatch} = require("try-to-catch") const {router, test} = require("../../../test/web") -const {select} = require("../../passthrough") +const {select, discord} = require("../../passthrough") const {MatrixServerError} = require("../../matrix/mreq") test("web autocreate: checks permissions", async t => { diff --git a/src/web/routes/guild.js b/src/web/routes/guild.js index c9561e5..ef2065e 100644 --- a/src/web/routes/guild.js +++ b/src/web/routes/guild.js @@ -99,7 +99,7 @@ function getChannelRoomsLinks(guild, rooms, roles) { let channelIDs = discord.guildChannelMap.get(guild.id) assert(channelIDs) - let linkedChannels = select("channel_room", ["channel_id", "room_id", "name", "nick"], {channel_id: channelIDs}).all() + let linkedChannels = select("channel_room", ["channel_id", "room_id", "name", "nick", "room_mention_role"], {channel_id: channelIDs}).all() let linkedChannelsWithDetails = linkedChannels.map(c => ({ // @ts-ignore /** @type {DiscordTypes.APIGuildChannel} */ channel: discord.channels.get(c.channel_id),