Compare commits

...
7 Commits
Author SHA1 Message Date
N0VA 63e3a4c062 Merge remote-tracking branch 'gitdab/main'
# Conflicts:
#	package-lock.json
#	package.json
#	src/d2m/converters/message-to-event.test.js
#	test/data.js
2026-10-09 09:43:13 +02:00
Cadence Ember 9994db94e6 Don't copy URL previews for GitHub 2026-10-06 15:27:17 +02:00
Cadence Ember d9e93a8554 Support Discord game mentions 2026-10-06 15:27:17 +02:00
Cadence Ember 74af45b407 Fix Klipy gifs when url previews disabled 2026-10-06 15:27:17 +02:00
N0VA 1530151598 Use icons, like the linked channels list (and fix the no-JS save button) 2026-10-04 10:39:11 +02:00
N0VA 6ac6858375 Align things in the UI 2026-10-04 10:05:58 +02:00
N0VA 0b888cfbf2 Configurable @room ping discord roles 2026-10-03 17:53:21 +02:00
8 changed files with 98 additions and 6 deletions
@@ -0,0 +1,5 @@
BEGIN TRANSACTION;
ALTER TABLE channel_room ADD COLUMN ping_role_id TEXT;
COMMIT;
+1
View File
@@ -21,6 +21,7 @@ export type Models = {
speedbump_checked: number | null speedbump_checked: number | null
guild_id: string | null guild_id: string | null
custom_topic: number custom_topic: number
ping_role_id: string | null
} }
channel_speedbump: { channel_speedbump: {
+4 -3
View File
@@ -460,14 +460,15 @@ async function handleRoomOrMessageLinks(input, di) {
async function checkWrittenMentions(content, senderMxid, roomID, guild, 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+ 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) {
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) const {powers: {[senderMxid]: userPower}, powerLevels} = await mxUtils.getEffectivePower(roomID, [senderMxid], di.api)
if (userPower >= (powerLevels.notifications?.room ?? 50)) { if (userPower >= (powerLevels.notifications?.room ?? 50)) {
const roleID = select("channel_room", "ping_role_id", {room_id: roomID}).pluck().get()
return { return {
// @ts-ignore - typescript doesn't know about indices yet // @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: [], ensureJoined: [],
allowedMentionsParse: ["everyone"], allowedMentionsParse: roleID ? [] : ["everyone"],
allowedMentionsUsers: [] allowedMentionsUsers: []
} }
} }
+1 -1
View File
@@ -4,7 +4,7 @@ const DiscordTypes = require("discord-api-types/v10")
const {eventToMessage} = require("./event-to-message") const {eventToMessage} = require("./event-to-message")
const data = require("../../../test/data") const data = require("../../../test/data")
const {MatrixServerError} = require("../../matrix/mreq") const {MatrixServerError} = require("../../matrix/mreq")
const {select, discord} = require("../../passthrough") const {select, discord, db} = require("../../passthrough")
/** /**
* @param {string} roomID * @param {string} roomID
+66
View File
@@ -188,6 +188,72 @@ block body
td(colspan="3") td(colspan="3")
.s-empty-state No channels linked between Discord and Matrix yet... .s-empty-state No channels linked between Discord and Matrix yet...
if space_id && linkedChannelsWithDetails.length
- const customPingRooms = linkedChannelsWithDetails.filter(row => row.ping_role_id)
- const defaultPingRooms = linkedChannelsWithDetails.filter(row => !row.ping_role_id)
- const pingRoles = [...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 customPingRooms.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 customPingRooms
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-ping-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="ping_role_id" id="ping-role-" + row.channel.id aria-label="Notify role for #" + row.channel.name onchange="this.form.requestSubmit()" style="height: 32px")
if !pingRoles.some(role => role.id === row.ping_role_id)
option(value=row.ping_role_id selected disabled) Deleted role (choose a replacement)
each role in pingRoles
option(value=role.id selected=row.ping_role_id === role.id)= "@" + role.name
button.s-btn.s-btn__outlined.py0.box-border(type="submit" data-room-ping-save style="height: 32px; min-height: 0") Save
form.d-flex.ai-stretch(method="post" action=rel("/api/room-ping-role"))
input(type="hidden" name="guild_id" value=guild_id)
input(type="hidden" name="channel_id" value=row.channel.id)
input(type="hidden" name="ping_role_id" 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-ping-save]').forEach(button => button.remove())
if defaultPingRooms.length && pingRoles.length
details.mt16.mx12.mb12
summary.s-label.cursor-pointer Add custom role
form.mt16(method="post" action=rel("/api/room-ping-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="ping-role-channel") Channel
.s-select
select.w100.box-border#ping-role-channel(name="channel_id" required style="height: 32px")
option(value="" selected disabled) Choose a channel
each row in defaultPingRooms
- 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="ping-role-new") Notify role
.s-select
select.w100.box-border#ping-role-new(name="ping_role_id" required style="height: 32px")
option(value="" selected disabled) Choose a role
each role in pingRoles
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 h3.fs-category.mt32 Auto-create
.s-card.d-grid.px0 .s-card.d-grid.px0
form.d-flex.ai-center.g16 form.d-flex.ai-center.g16
+19
View File
@@ -140,3 +140,22 @@ as.router.post("/api/default-roles", defineEventHandler(async event => {
return sendRedirect(event, `/guild?guild_id=${guildID}`, 302) return sendRedirect(event, `/guild?guild_id=${guildID}`, 302)
} }
})) }))
// An empty role restores @everyone
as.router.post("/api/room-ping-role", defineEventHandler(async event => {
const body = await readValidatedBody(event, z.object({
guild_id: z.string(),
channel_id: z.string(),
ping_role_id: 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.ping_role_id && (!guild?.roles.some(role => role.id === body.ping_role_id) || body.ping_role_id === body.guild_id)) {
throw createError({status: 400, message: "Choose a role from this server"})
}
db.prepare("UPDATE channel_room SET ping_role_id = ? WHERE channel_id = ? AND guild_id = ?").run(body.ping_role_id || null, body.channel_id, body.guild_id)
return sendRedirect(event, `/guild?guild_id=${body.guild_id}#room-notifications`, 302)
}))
+1 -1
View File
@@ -2,7 +2,7 @@
const {tryToCatch} = require("try-to-catch") const {tryToCatch} = require("try-to-catch")
const {router, test} = require("../../../test/web") const {router, test} = require("../../../test/web")
const {select} = require("../../passthrough") const {select, discord} = require("../../passthrough")
const {MatrixServerError} = require("../../matrix/mreq") const {MatrixServerError} = require("../../matrix/mreq")
test("web autocreate: checks permissions", async t => { test("web autocreate: checks permissions", async t => {
+1 -1
View File
@@ -99,7 +99,7 @@ function getChannelRoomsLinks(guild, rooms, roles) {
let channelIDs = discord.guildChannelMap.get(guild.id) let channelIDs = discord.guildChannelMap.get(guild.id)
assert(channelIDs) 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", "ping_role_id"], {channel_id: channelIDs}).all()
let linkedChannelsWithDetails = linkedChannels.map(c => ({ let linkedChannelsWithDetails = linkedChannels.map(c => ({
// @ts-ignore // @ts-ignore
/** @type {DiscordTypes.APIGuildChannel} */ channel: discord.channels.get(c.channel_id), /** @type {DiscordTypes.APIGuildChannel} */ channel: discord.channels.get(c.channel_id),