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
17 changed files with 78 additions and 193 deletions
+20 -6
View File
@@ -57,6 +57,19 @@
"node": ">=22" "node": ">=22"
} }
}, },
"../discord-markdown": {
"name": "@cloudrac3r/discord-markdown",
"version": "2.9.0",
"extraneous": true,
"license": "MIT",
"dependencies": {
"simple-markdown": "^0.7.3"
},
"devDependencies": {
"@cloudrac3r/tap-dot": "^2.0.2",
"supertape": "^13.6.2"
}
},
"node_modules/@babel/helper-string-parser": { "node_modules/@babel/helper-string-parser": {
"version": "7.27.1", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
@@ -133,9 +146,9 @@
} }
}, },
"node_modules/@cloudrac3r/discord-markdown": { "node_modules/@cloudrac3r/discord-markdown": {
"version": "2.9.1", "version": "2.9.0",
"resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.9.1.tgz", "resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.9.0.tgz",
"integrity": "sha512-sYaDfRBaS7uMVQx4wJwT68F6OzJZX84DF2XvHkQr8vdwTCvnpeaT26obqvzNawVwVoQ4xjIMZxafgMrAEK4Oxw==", "integrity": "sha512-0S22mNlT9rPhYnxP+qzKWayVe+hFWcEqxVCviF/pyzWAb9nLaBZkIVk0jHTImoA1MY0mqx1uMKjIcxVHkEZe3A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"simple-markdown": "^0.7.3" "simple-markdown": "^0.7.3"
@@ -270,9 +283,9 @@
} }
}, },
"node_modules/@cloudrac3r/tell-me-when": { "node_modules/@cloudrac3r/tell-me-when": {
"version": "2.0.1", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@cloudrac3r/tell-me-when/-/tell-me-when-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@cloudrac3r/tell-me-when/-/tell-me-when-2.0.0.tgz",
"integrity": "sha512-nm1Ao/vpHDn2YMivnc9EVr/QrcT5IG6G7o1Ffh31YafIo9Cc3bSN1jPLL9eOuKKLCYT1/DZP7Nh8PXAN371bAQ==", "integrity": "sha512-U+TEOTe6f/vSIeD/QP0VPrkLrhnFtQs2euxBOAaqC8rzQKu8nNoKSFuhipT6DyuCZH48B58+qrPMmH2w2IgMhA==",
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"tell-me-when": "dist/src/cli.js" "tell-me-when": "dist/src/cli.js"
@@ -1162,6 +1175,7 @@
"version": "13.0.3", "version": "13.0.3",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.3.tgz", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.3.tgz",
"integrity": "sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==", "integrity": "sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==",
"hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"node-addon-api": "^8.0.0" "node-addon-api": "^8.0.0"
-3
View File
@@ -71,8 +71,5 @@
"addbot": "node addbot.js", "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": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js | tap-dot",
"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" "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"
},
"allowScripts": {
"better-sqlite3": true
} }
} }
+2 -2
View File
@@ -915,8 +915,8 @@ async function messageToEvent(message, guild, options = {}, di) {
// Add time // Add time
if (event.scheduled_end_time) { if (event.scheduled_end_time) {
const formatter = new Intl.DateTimeFormat("en-NZ", {weekday: "long", month: "long", day: "numeric", hour: "numeric", minute: "2-digit", timeZone: "UTC"}) // 9 June at 3:00 pm NZT const formatter = new Intl.DateTimeFormat("en-NZ", {month: "long", day: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "shortGeneric", timeZone: reg.ooye.time_zone}) // 9 June at 3:00 pm NZT
var displayTime = (formatter.formatRange(new Date(event.scheduled_start_time), new Date(event.scheduled_end_time)) + " (UTC)").replaceAll(/\p{Zs}/gv, " ") var displayTime = formatter.formatRange(new Date(event.scheduled_start_time), new Date(event.scheduled_end_time))
} else { } else {
var displayTime = discordTime.convertDiscordTime(unixSeconds, "F") var displayTime = discordTime.convertDiscordTime(unixSeconds, "F")
} }
+6 -6
View File
@@ -1589,12 +1589,12 @@ test("message2event: irl invite event renders embed", async t => {
{ {
$type: "m.room.message", $type: "m.room.message",
msgtype: "m.notice", msgtype: "m.notice",
body: `| Scheduled Event - Sunday, 8 June, 10:00 am – 12:00 pm (UTC)` body: `| Scheduled Event - 8 June at 10:00 pm NZT – 9 June at 12:00 am NZT`
+ `\n| ## forest exploration` + `\n| ## forest exploration`
+ `\n| ` + `\n| `
+ `\n| 📍 the dark forest`, + `\n| 📍 the dark forest`,
format: "org.matrix.custom.html", format: "org.matrix.custom.html",
formatted_body: `<blockquote><p>Scheduled Event - <a href=\"https://bridge.example.org/time/1749376800/F\">Sunday, 8 June, 10:00 am – 12:00 pm (UTC)</a></p>` formatted_body: `<blockquote><p>Scheduled Event - 8 June at 10:00 pm NZT – 9 June at 12:00 am NZT</p>`
+ `<strong>forest exploration</strong>` + `<strong>forest exploration</strong>`
+ `<p>📍 the dark forest</p></blockquote>`, + `<p>📍 the dark forest</p></blockquote>`,
"m.mentions": {} "m.mentions": {}
@@ -1622,13 +1622,13 @@ test("message2event: vc invite event renders embed", async t => {
{ {
$type: "m.room.message", $type: "m.room.message",
msgtype: "m.notice", msgtype: "m.notice",
body: `| Scheduled Event - Monday, 9 June 2025 at 3:00 am (UTC)` body: `| Scheduled Event - 9 June at 3:00 pm NZT`
+ `\n| ## Cooking (Netrunners)` + `\n| ## Cooking (Netrunners)`
+ `\n| Short circuited brain interfaces actually just means your brain is medium rare, yum.` + `\n| Short circuited brain interfaces actually just means your brain is medium rare, yum.`
+ `\n| ` + `\n| `
+ `\n| 🔊 Cooking`, + `\n| 🔊 Cooking`,
format: "org.matrix.custom.html", format: "org.matrix.custom.html",
formatted_body: `<blockquote><p>Scheduled Event - <a href=\"https://bridge.example.org/time/1749438000/F\">Monday, 9 June 2025 at 3:00 am (UTC)</a></p>` formatted_body: `<blockquote><p>Scheduled Event - 9 June at 3:00 pm NZT</p>`
+ `<strong>Cooking (Netrunners)</strong><br>Short circuited brain interfaces actually just means your brain is medium rare, yum.` + `<strong>Cooking (Netrunners)</strong><br>Short circuited brain interfaces actually just means your brain is medium rare, yum.`
+ `<p>🔊 Cooking</p></blockquote>`, + `<p>🔊 Cooking</p></blockquote>`,
"m.mentions": {} "m.mentions": {}
@@ -1664,13 +1664,13 @@ test("message2event: vc invite event renders embed with room link", async t => {
{ {
$type: "m.room.message", $type: "m.room.message",
msgtype: "m.notice", msgtype: "m.notice",
body: `| Scheduled Event - Monday, 9 June 2025 at 3:00 am (UTC)` body: `| Scheduled Event - 9 June at 3:00 pm NZT`
+ `\n| ## Cooking (Netrunners)` + `\n| ## Cooking (Netrunners)`
+ `\n| Short circuited brain interfaces actually just means your brain is medium rare, yum.` + `\n| Short circuited brain interfaces actually just means your brain is medium rare, yum.`
+ `\n| ` + `\n| `
+ `\n| 🔊 Hey. - https://matrix.to/#/!FuDZhlOAtqswlyxzeR:cadence.moe?via=cadence.moe`, + `\n| 🔊 Hey. - https://matrix.to/#/!FuDZhlOAtqswlyxzeR:cadence.moe?via=cadence.moe`,
format: "org.matrix.custom.html", format: "org.matrix.custom.html",
formatted_body: `<blockquote><p>Scheduled Event - <a href=\"https://bridge.example.org/time/1749438000/F\">Monday, 9 June 2025 at 3:00 am (UTC)</a></p>` formatted_body: `<blockquote><p>Scheduled Event - 9 June at 3:00 pm NZT</p>`
+ `<strong>Cooking (Netrunners)</strong><br>Short circuited brain interfaces actually just means your brain is medium rare, yum.` + `<strong>Cooking (Netrunners)</strong><br>Short circuited brain interfaces actually just means your brain is medium rare, yum.`
+ `<p>🔊 Hey. - <a href="https://matrix.to/#/!FuDZhlOAtqswlyxzeR:cadence.moe?via=cadence.moe">Hey.</a></p></blockquote>`, + `<p>🔊 Hey. - <a href="https://matrix.to/#/!FuDZhlOAtqswlyxzeR:cadence.moe?via=cadence.moe">Hey.</a></p></blockquote>`,
"m.mentions": {} "m.mentions": {}
@@ -1,5 +0,0 @@
BEGIN TRANSACTION;
ALTER TABLE channel_room ADD COLUMN room_mention_role TEXT;
COMMIT;
@@ -0,0 +1,5 @@
BEGIN TRANSACTION;
ALTER TABLE channel_room ADD COLUMN ping_role_id TEXT;
COMMIT;
+1 -1
View File
@@ -21,7 +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
room_mention_role: string | null ping_role_id: string | null
} }
channel_speedbump: { channel_speedbump: {
+1 -1
View File
@@ -44,7 +44,7 @@ function getFormatOptions(style) {
function convertDiscordTime(timestamp, style) { function convertDiscordTime(timestamp, style) {
const formatter = new Intl.DateTimeFormat("en-NZ", getFormatOptions(style)) const formatter = new Intl.DateTimeFormat("en-NZ", getFormatOptions(style))
const d = new Date(timestamp * 1000) const d = new Date(timestamp * 1000)
return (formatter.format(d) + " (UTC)").replaceAll(/\p{Zs}/gv, " ") return formatter.format(d) + " (UTC)"
} }
module.exports.getFormatOptions = getFormatOptions module.exports.getFormatOptions = getFormatOptions
+7 -9
View File
@@ -452,18 +452,18 @@ async function handleRoomOrMessageLinks(input, di) {
/** /**
* @param {string} content * @param {string} content
* @param {{content: any, room_id: string, sender: string}} event * @param {string} senderMxid
* @param {string} roomID
* @param {DiscordTypes.APIGuild} guild * @param {DiscordTypes.APIGuild} guild
* @param {{api: import("../../matrix/api"), snow: import("snowtransfer").SnowTransfer}} di * @param {{api: import("../../matrix/api"), snow: import("snowtransfer").SnowTransfer}} di
*/ */
async function checkWrittenMentions(content, event, 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 discord if (writtenMentionMatch[1] === "room") { // convert @room to discord
if (("m.mentions" in event.content) && !event.content["m.mentions"].room) return // check it's allowed by m.mentions if present const {powers: {[senderMxid]: userPower}, powerLevels} = await mxUtils.getEffectivePower(roomID, [senderMxid], di.api)
const {powers: {[event.sender]: userPower}, powerLevels} = await mxUtils.getEffectivePower(event.room_id, [event.sender], di.api)
if (userPower >= (powerLevels.notifications?.room ?? 50)) { if (userPower >= (powerLevels.notifications?.room ?? 50)) {
const roleID = select("channel_room", "room_mention_role", {room_id: event.room_id}).pluck().get() 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) + (roleID ? `<@&${roleID}>` : "@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]),
@@ -861,8 +861,6 @@ async function eventToMessage(event, guild, channel, di) {
} }
}) })
input = input.replace(/<a\s[^>]*href="https:\/\/matrix\.to\/#\/(?:!|%21|#|%23)[^"\/]*"[^>]*>@room<\/a>/g, "@room")
// Handling mentions of rooms and room-messages // Handling mentions of rooms and room-messages
input = await handleRoomOrMessageLinks(input, di) input = await handleRoomOrMessageLinks(input, di)
@@ -914,7 +912,7 @@ async function eventToMessage(event, guild, channel, di) {
// Check written mentions // Check written mentions
if (node.nodeType === 3 && node.nodeValue.includes("@") && !nodeIsChildOf(node, ["A", "CODE", "PRE"])) { if (node.nodeType === 3 && node.nodeValue.includes("@") && !nodeIsChildOf(node, ["A", "CODE", "PRE"])) {
const result = await checkWrittenMentions(node.nodeValue, event, guild, di) const result = await checkWrittenMentions(node.nodeValue, event.sender, event.room_id, guild, di)
if (result) { if (result) {
node.nodeValue = result.content node.nodeValue = result.content
ensureJoined.push(...result.ensureJoined) ensureJoined.push(...result.ensureJoined)
@@ -1032,7 +1030,7 @@ async function eventToMessage(event, guild, channel, di) {
} }
} }
const result = await checkWrittenMentions(content, event, guild, di) const result = await checkWrittenMentions(content, event.sender, event.room_id, guild, di)
if (result) { if (result) {
content = result.content content = result.content
ensureJoined.push(...result.ensureJoined) ensureJoined.push(...result.ensureJoined)
+1 -121
View File
@@ -5521,9 +5521,6 @@ test("event2message: @room converts to @everyone and is allowed when the room do
type: "m.room.message", type: "m.room.message",
sender: "@cadence:cadence.moe", sender: "@cadence:cadence.moe",
content: { content: {
"m.mentions": {
room: true
},
msgtype: "m.text", msgtype: "m.text",
body: "@room dinner's ready", body: "@room dinner's ready",
}, },
@@ -5566,124 +5563,7 @@ test("event2message: @room converts to @everyone and is allowed when the room do
content: "@everyone dinner's ready", content: "@everyone dinner's ready",
avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90", avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90",
allowed_mentions: { allowed_mentions: {
users: [], parse: ["users", "roles", "everyone"]
parse: ["roles", "everyone"]
}
}],
ensureJoined: []
}
)
})
test("event2message: @room is converted to a role in configured rooms", async t => {
let called = 0
t.deepEqual(
await eventToMessage({
type: "m.room.message",
sender: "@cadence:cadence.moe",
content: {
msgtype: "m.text",
body: "@room dinner's ready",
},
room_id: "!fGgIymcYWOqjbSRUdV:cadence.moe",
event_id: "$SiXetU9h9Dg-M9Frcw_C6ahnoXZ3QPZe3MVJR5tcB9A"
}, data.guild.general, data.channel.general, {
api: {
async getStateEvent(roomID, type, key) {
called++
t.equal(roomID, "!fGgIymcYWOqjbSRUdV:cadence.moe")
t.equal(type, "m.room.power_levels")
t.equal(key, "")
return {
users: {},
notifications: {
room: 0
}
}
},
async getStateEventOuter(roomID, type, key) {
t.equal(roomID, "!fGgIymcYWOqjbSRUdV:cadence.moe")
t.equal(type, "m.room.create")
t.equal(key, "")
return {
type: "m.room.create",
state_key: "",
sender: "@_ooye_bot:cadence.moe",
content: {
room_version: "11"
}
}
}
}
}),
{
messagesToDelete: [],
messagesToEdit: [],
messagesToSend: [{
username: "cadence [they]",
content: "<@&123456> dinner's ready",
avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU?preset=avatar",
allowed_mentions: {
parse: ["users", "roles"]
}
}],
ensureJoined: []
}
)
})
test("event2message: @room is not allowed with mismatched m.mentions", async t => {
let called = 0
t.deepEqual(
await eventToMessage({
type: "m.room.message",
sender: "@cadence:cadence.moe",
content: {
"m.mentions": {},
msgtype: "m.text",
body: "@room dinner's ready",
},
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
event_id: "$SiXetU9h9Dg-M9Frcw_C6ahnoXZ3QPZe3MVJR5tcB9A"
}, data.guild.general, data.channel.general, {
api: {
async getStateEvent(roomID, type, key) {
called++
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
t.equal(type, "m.room.power_levels")
t.equal(key, "")
return {
users: {},
notifications: {
room: 0
}
}
},
async getStateEventOuter(roomID, type, key) {
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
t.equal(type, "m.room.create")
t.equal(key, "")
return {
type: "m.room.create",
state_key: "",
sender: "@_ooye_bot:cadence.moe",
content: {
room_version: "11"
}
}
}
}
}),
{
messagesToDelete: [],
messagesToEdit: [],
messagesToSend: [{
username: "cadence [they]",
content: "@room dinner's ready",
avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90",
allowed_mentions: {
users: [],
parse: ["roles"]
} }
}], }],
ensureJoined: [] ensureJoined: []
+23 -23
View File
@@ -189,13 +189,13 @@ block body
.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 if space_id && linkedChannelsWithDetails.length
- const customMentionRooms = linkedChannelsWithDetails.filter(row => row.room_mention_role) - const customPingRooms = linkedChannelsWithDetails.filter(row => row.ping_role_id)
- const defaultMentionRooms = linkedChannelsWithDetails.filter(row => !row.room_mention_role) - const defaultPingRooms = linkedChannelsWithDetails.filter(row => !row.ping_role_id)
- const mentionRoles = [...guild.roles].filter(role => role.id !== guild_id).sort((a, b) => b.position - a.position) - 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 h3.mt32.fs-category#room-notifications Room notifications
.s-card.p0 .s-card.p0
p.s-description.m12 Choose which Discord role receives Matrix @room notifications p.s-description.m12 Choose which Discord role receives Matrix @room notifications
if customMentionRooms.length if customPingRooms.length
.s-table-container(style="container-type: inline-size") .s-table-container(style="container-type: inline-size")
table.s-table.s-table__bx-simple(style="table-layout: fixed") table.s-table.s-table__bx-simple(style="table-layout: fixed")
thead thead
@@ -203,51 +203,51 @@ block body
th(scope="col" style="width: calc(50cqw - 40px)") Channel th(scope="col" style="width: calc(50cqw - 40px)") Channel
th.pr12(scope="col") Notify role th.pr12(scope="col") Notify role
tbody tbody
each row in customMentionRooms each row in customPingRooms
tr tr
td.va-middle td.va-middle
+discord(row.channel) +discord(row.channel)
td.pr12 td.pr12
.d-flex.ai-start.g8 .d-flex.ai-start.g8
form.fl-grow1.wmn0.d-flex.ai-stretch.g8(method="post" action=rel("/api/room-mention-role")) 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="guild_id" value=guild_id)
input(type="hidden" name="channel_id" value=row.channel.id) input(type="hidden" name="channel_id" value=row.channel.id)
.s-select.fl-grow1.wmn0 .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") 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 !mentionRoles.some(role => role.id === row.room_mention_role) if !pingRoles.some(role => role.id === row.ping_role_id)
option(value=row.room_mention_role selected disabled) Deleted role (choose a replacement) option(value=row.ping_role_id selected disabled) Deleted role (choose a replacement)
each role in mentionRoles each role in pingRoles
option(value=role.id selected=row.room_mention_role === role.id)= "@" + role.name 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-mention-save style="height: 32px; min-height: 0") Save 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-mention-role")) 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="guild_id" value=guild_id)
input(type="hidden" name="channel_id" value=row.channel.id) input(type="hidden" name="channel_id" value=row.channel.id)
input(type="hidden" name="room_mention_role" value="") 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 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. script.
document.querySelectorAll('[data-room-mention-save]').forEach(button => button.remove()) document.querySelectorAll('[data-room-ping-save]').forEach(button => button.remove())
if defaultMentionRooms.length && mentionRoles.length if defaultPingRooms.length && pingRoles.length
details.mt16.mx12.mb12 details.mt16.mx12.mb12
summary.s-label.cursor-pointer Add custom role summary.s-label.cursor-pointer Add custom role
form.mt16(method="post" action=rel("/api/room-mention-role")) form.mt16(method="post" action=rel("/api/room-ping-role"))
input(type="hidden" name="guild_id" value=guild_id) input(type="hidden" name="guild_id" value=guild_id)
.d-flex.fw-wrap.ai-end.g16 .d-flex.fw-wrap.ai-end.g16
.fl-grow1.wmn0(style="flex-basis: 0; min-width: 180px") .fl-grow1.wmn0(style="flex-basis: 0; min-width: 180px")
label.s-label(for="mention-role-channel") Channel label.s-label(for="ping-role-channel") Channel
.s-select .s-select
select.w100.box-border#mention-role-channel(name="channel_id" required style="height: 32px") select.w100.box-border#ping-role-channel(name="channel_id" required style="height: 32px")
option(value="" selected disabled) Choose a channel option(value="" selected disabled) Choose a channel
each row in defaultMentionRooms each row in defaultPingRooms
- const parent = discord.channels.get(row.channel.parent_id) - const parent = discord.channels.get(row.channel.parent_id)
option(value=row.channel.id)= "#" + row.channel.name + (parent ? " - " + parent.name : "") option(value=row.channel.id)= "#" + row.channel.name + (parent ? " - " + parent.name : "")
.fl-grow1.wmn0(style="flex-basis: 0; min-width: 180px") .fl-grow1.wmn0(style="flex-basis: 0; min-width: 180px")
label.s-label(for="mention-role-new") Notify role label.s-label(for="ping-role-new") Notify role
.s-select .s-select
select.w100.box-border#mention-role-new(name="room_mention_role" required style="height: 32px") select.w100.box-border#ping-role-new(name="ping_role_id" required style="height: 32px")
option(value="" selected disabled) Choose a role option(value="" selected disabled) Choose a role
each role in mentionRoles each role in pingRoles
option(value=role.id)= "@" + role.name option(value=role.id)= "@" + role.name
.d-flex.ai-stretch(style="height: 32px") .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") button.s-btn.s-btn__icon.s-btn__filled.d-inline-flex.ai-center.g4.py0.box-border(type="submit" style="min-height: 0")
+3 -5
View File
@@ -67,6 +67,7 @@ html(lang="en")
if style !== "R" if style !== "R"
p #[span#remaining.remaining]#[span#direction] #[span#tick.tick] p #[span#remaining.remaining]#[span#direction] #[span#tick.tick]
script. script.
document.body.classList.add("yes-script")
const WIDTH = !{WIDTH} const WIDTH = !{WIDTH}
const timestamp = !{timestamp} const timestamp = !{timestamp}
const style = "!{style}" const style = "!{style}"
@@ -103,7 +104,6 @@ html(lang="en")
st.style.fontSize = Math.floor(parseInt(st.style.fontSize) * WIDTH / l) + "px"; st.style.fontSize = Math.floor(parseInt(st.style.fontSize) * WIDTH / l) + "px";
} }
} }
document.body.classList.add("yes-script")
let frame = 0 let frame = 0
function updateTimeRemaining() { function updateTimeRemaining() {
@@ -116,17 +116,15 @@ html(lang="en")
} }
let difference = Math.abs(timeForRemaining.getTime() - now.getTime()) let difference = Math.abs(timeForRemaining.getTime() - now.getTime())
// Temporal.Duration isn't Baseline https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration
let foundSignificantField = false let foundSignificantField = false
const remainingTextParts = UNITS.map(([unit, duration], index) => { let remainingText = UNITS.map(([unit, duration], index) => {
const multiplier = Math.floor(difference / duration) const multiplier = Math.floor(difference / duration)
difference -= multiplier * duration difference -= multiplier * duration
if (multiplier > 0 || foundSignificantField) { if (multiplier > 0 || foundSignificantField) {
foundSignificantField = true foundSignificantField = true
return multiplier + "\u00a0" + unit + (multiplier !== 1 ? "s" : "") return multiplier + "\u00a0" + unit + (multiplier !== 1 ? "s" : "")
} }
}).filter(s => s) }).filter(s => s).join(", ")
const remainingText = remainingTextParts.length === 2 ? remainingTextParts.join(" and ") : remainingTextParts.join(", ")
if (remainingText) { if (remainingText) {
remaining.textContent = remainingText remaining.textContent = remainingText
direction.textContent = direction.textContent =
+4 -4
View File
@@ -142,20 +142,20 @@ as.router.post("/api/default-roles", defineEventHandler(async event => {
})) }))
// An empty role restores @everyone // An empty role restores @everyone
as.router.post("/api/room-mention-role", defineEventHandler(async event => { as.router.post("/api/room-ping-role", defineEventHandler(async event => {
const body = await readValidatedBody(event, z.object({ const body = await readValidatedBody(event, z.object({
guild_id: z.string(), guild_id: z.string(),
channel_id: z.string(), channel_id: z.string(),
room_mention_role: z.string().regex(/^(?:[0-9]+)?$/) ping_role_id: z.string().regex(/^(?:[0-9]+)?$/)
}).parse) }).parse)
const managed = await auth.getManagedGuilds(event) 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"}) 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() 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"}) if (!room) throw createError({status: 404, message: "Linked channel not found"})
const guild = discord.guilds.get(body.guild_id) 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)) { 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"}) 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) 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) return sendRedirect(event, `/guild?guild_id=${body.guild_id}#room-notifications`, 302)
})) }))
+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", "room_mention_role"], {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),
+3 -1
View File
@@ -1,8 +1,10 @@
#!/usr/bin/env node #!/usr/bin/env node
// @ts-check // @ts-check
// Ensure consistent output from the Intl and Temporal API across Node.js versions // Temporal API stabilised in Node.js 26. Previous versions can use `--harmony-temporal`, but that's jank, so have a polyfill.
if (!("Temporal" in globalThis)) {
require("temporal-polyfill-lite/global") require("temporal-polyfill-lite/global")
}
const fs = require("fs") const fs = require("fs")
const sqlite = require("better-sqlite3") const sqlite = require("better-sqlite3")
-1
View File
@@ -25,7 +25,6 @@ INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom
('1161864271370666075', '!mHmhQQPwXNananMUqq:cadence.moe', 'updates', NULL, NULL, NULL, '112760669178241024'), ('1161864271370666075', '!mHmhQQPwXNananMUqq:cadence.moe', 'updates', NULL, NULL, NULL, '112760669178241024'),
('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'); ('598707048112193536', '!JBxeGYnzQwLnaooOLD:cadence.moe', 'winners', NULL, NULL, NULL, '1345641201902288987');
UPDATE channel_room SET room_mention_role = '123456' WHERE room_id = '!fGgIymcYWOqjbSRUdV:cadence.moe';
INSERT INTO historical_channel_room (reference_channel_id, room_id, upgraded_timestamp) SELECT channel_id, room_id, 0 FROM channel_room; INSERT INTO historical_channel_room (reference_channel_id, room_id, upgraded_timestamp) SELECT channel_id, room_id, 0 FROM channel_room;
-3
View File
@@ -1,8 +1,5 @@
// @ts-check // @ts-check
// Ensure consistent output from the Intl and Temporal API across Node.js versions
require("temporal-polyfill-lite/global")
const fs = require("fs") const fs = require("fs")
const {join} = require("path") const {join} = require("path")
const sqlite = require("better-sqlite3") const sqlite = require("better-sqlite3")