diff --git a/docs/developer-orientation.md b/docs/developer-orientation.md
index 0f5e0c0..bff64c2 100644
--- a/docs/developer-orientation.md
+++ b/docs/developer-orientation.md
@@ -113,7 +113,6 @@ Total transitive production dependencies: 144
* (0) ansi-colors: Helps with interactive prompting for the initial setup, and it's already pulled in by enquirer.
* (1) chunk-text: It does what I want.
* (0) cloudstorm: Discord gateway library with bring-your-own-caching that I trust.
-* (0) denque: Much faster than using arrays as a queue.
* (0) discord-api-types: Bitfields needed at runtime and types needed for development.
* (0) domino: DOM implementation that's already pulled in by turndown.
* (2) enquirer: Interactive prompting for the initial setup rather than forcing users to edit YAML non-interactively.
diff --git a/package-lock.json b/package-lock.json
index 04dbbe4..eb07b4d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "out-of-your-element",
- "version": "3.6.0",
+ "version": "3.5.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "out-of-your-element",
- "version": "3.6.0",
+ "version": "3.5.1",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@chriscdn/promise-semaphore": "^3.0.1",
@@ -18,14 +18,13 @@
"@cloudrac3r/pngjs": "^7.0.3",
"@cloudrac3r/pug": "^4.0.4",
"@cloudrac3r/stream-type": "^1.0.0",
- "@cloudrac3r/turndown": "^7.1.5",
+ "@cloudrac3r/turndown": "^7.1.4",
"@stackoverflow/stacks": "^2.5.4",
"@stackoverflow/stacks-icons": "^6.0.2",
"ansi-colors": "^4.1.3",
"better-sqlite3": "^12.2.0",
"chunk-text": "^2.0.1",
- "cloudstorm": "^0.19.0",
- "denque": "^2.1.0",
+ "cloudstorm": "^0.17.1",
"discord-api-types": "^0.38.38",
"domino": "^2.1.6",
"enquirer": "^2.4.1",
@@ -38,7 +37,7 @@
"mime-types": "^2.1.35",
"prettier-bytes": "^1.0.4",
"sharp": "^0.34.5",
- "snowtransfer": "^0.19.0",
+ "snowtransfer": "^0.17.5",
"try-to-catch": "^4.0.5",
"uqr": "^0.1.2",
"xxhash-wasm": "^1.0.2",
@@ -268,9 +267,9 @@
}
},
"node_modules/@cloudrac3r/turndown": {
- "version": "7.1.5",
- "resolved": "https://registry.npmjs.org/@cloudrac3r/turndown/-/turndown-7.1.5.tgz",
- "integrity": "sha512-pENoG62B4UM2ecsg57cuSRDa1OV/YFYTFGg1aoQsKUdHBBglhE9I7EwEMYpS96QoM/auyglfaxjgTeqhcSGCIA==",
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/@cloudrac3r/turndown/-/turndown-7.1.4.tgz",
+ "integrity": "sha512-bQAwcvcSqBTdEHPMt+IAZWIoDh+2eRuy9TgD0FUdxVurbvj3CUHTxLfzlmsO0UTi+GHpgYqDSsVdV7kYTNq5Qg==",
"license": "MIT",
"dependencies": {
"domino": "^2.1.6"
@@ -1317,13 +1316,13 @@
}
},
"node_modules/cloudstorm": {
- "version": "0.19.0",
- "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.19.0.tgz",
- "integrity": "sha512-mjuP5o2nS/CgSljajh08KjN6jYMfNBBttSPHc1YUN74f7OkmhUhv8QE8JFDXomxCfdnLNJcqBQtjRLrs8nkfVQ==",
+ "version": "0.17.1",
+ "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.17.1.tgz",
+ "integrity": "sha512-LYUwzHagRYRd93XocOqi+HCHdzPYI9cW7Yf7pYqinxgG+Qka1OiqBKWTCcLiEuiqXaOV30kr8c6aZ/c1QcDP4Q==",
"license": "MIT",
"dependencies": {
- "discord-api-types": "^0.38.49",
- "snowtransfer": "^0.19.0"
+ "discord-api-types": "^0.38.47",
+ "snowtransfer": "^0.17.5"
},
"engines": {
"node": ">=22.0.0"
@@ -1458,15 +1457,6 @@
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
"license": "MIT"
},
- "node_modules/denque": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
- "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=0.10"
- }
- },
"node_modules/destr": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
@@ -1483,9 +1473,9 @@
}
},
"node_modules/discord-api-types": {
- "version": "0.38.49",
- "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.49.tgz",
- "integrity": "sha512-XnqcWmnFZFAE8ZM8SHAw9DIV8D3Or00rMQ8iQLotrEA2PmXhl+ykaf6L6q4l474hrSUH1JaYcv+iOMRWp2p6Tg==",
+ "version": "0.38.47",
+ "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.47.tgz",
+ "integrity": "sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA==",
"license": "MIT",
"workspaces": [
"scripts/actions/documentation"
@@ -2589,12 +2579,12 @@
}
},
"node_modules/snowtransfer": {
- "version": "0.19.0",
- "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.19.0.tgz",
- "integrity": "sha512-Vebj4FCdpUsEKAzqC1p50ol5o09CLnrffvBCpTv0FNFefvXPXwogxR8kr4zJApIRt7oWQVZZVkxAq4XnGlICjQ==",
+ "version": "0.17.7",
+ "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.17.7.tgz",
+ "integrity": "sha512-scbOjYezo1Ycfk21atCEkeXIISTT7R7JTHCdiZ/7m7k4XbSb6o5q8Mu2fev5IqFpNyqIVjA0d/MZQ+eP/gtwfg==",
"license": "MIT",
"dependencies": {
- "discord-api-types": "^0.38.49"
+ "discord-api-types": "^0.38.47"
},
"engines": {
"node": ">=22.0.0"
diff --git a/package.json b/package.json
index c086517..9dfd2a8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "out-of-your-element",
- "version": "3.6.0",
+ "version": "3.5.1",
"description": "A bridge between Matrix and Discord",
"main": "index.js",
"repository": {
@@ -27,14 +27,13 @@
"@cloudrac3r/pngjs": "^7.0.3",
"@cloudrac3r/pug": "^4.0.4",
"@cloudrac3r/stream-type": "^1.0.0",
- "@cloudrac3r/turndown": "^7.1.5",
+ "@cloudrac3r/turndown": "^7.1.4",
"@stackoverflow/stacks": "^2.5.4",
"@stackoverflow/stacks-icons": "^6.0.2",
"ansi-colors": "^4.1.3",
"better-sqlite3": "^12.2.0",
"chunk-text": "^2.0.1",
- "cloudstorm": "^0.19.0",
- "denque": "^2.1.0",
+ "cloudstorm": "^0.17.1",
"discord-api-types": "^0.38.38",
"domino": "^2.1.6",
"enquirer": "^2.4.1",
@@ -47,7 +46,7 @@
"mime-types": "^2.1.35",
"prettier-bytes": "^1.0.4",
"sharp": "^0.34.5",
- "snowtransfer": "^0.19.0",
+ "snowtransfer": "^0.17.5",
"try-to-catch": "^4.0.5",
"uqr": "^0.1.2",
"xxhash-wasm": "^1.0.2",
diff --git a/scripts/setup.js b/scripts/setup.js
index 6ce4c9f..69b62a2 100644
--- a/scripts/setup.js
+++ b/scripts/setup.js
@@ -359,7 +359,7 @@ function defineEchoHandler() {
console.log("โ
Emojis are ready...")
// set profile data on discord...
- await discord.snow.user.editSelf({avatar: "data:image/png;base64," + avatarBuffer.toString("base64")})
+ await discord.snow.user.updateSelf({avatar: "data:image/png;base64," + avatarBuffer.toString("base64")})
console.log("โ
Discord profile updated...")
// set profile data on homeserver...
diff --git a/src/d2m/actions/create-space.js b/src/d2m/actions/create-space.js
index 403fdb2..7a751e2 100644
--- a/src/d2m/actions/create-space.js
+++ b/src/d2m/actions/create-space.js
@@ -8,8 +8,6 @@ const {reg} = require("../../matrix/read-registration")
const passthrough = require("../../passthrough")
const {discord, sync, db, select} = passthrough
-/** @type {import("../../matrix/mreq")} */
-const mreq = sync.require("../../matrix/mreq")
/** @type {import("../../matrix/api")} */
const api = sync.require("../../matrix/api")
/** @type {import("../../matrix/file")} */
@@ -239,12 +237,8 @@ async function syncSpaceExpressions(data, checkBeforeSync) {
try {
existing = await api.getStateEvent(spaceID, "im.ponies.room_emotes", eventKey)
} catch (e) {
- if (e instanceof mreq.MatrixServerError && e.httpStatus < 400) {
- // State event not found. This space doesn't have any existing emojis. We create a dummy empty event for comparison's sake.
- existing = fn([], guild)
- } else {
- throw e
- }
+ // State event not found. This space doesn't have any existing emojis. We create a dummy empty event for comparison's sake.
+ existing = fn([], guild)
}
if (isDeepStrictEqual(existing, content)) return
}
diff --git a/src/d2m/actions/send-message.js b/src/d2m/actions/send-message.js
index 5b3b4f3..8550d43 100644
--- a/src/d2m/actions/send-message.js
+++ b/src/d2m/actions/send-message.js
@@ -60,7 +60,8 @@ async function sendMessage(message, channel, guild, row) {
const detailedResultsMessage = await pollEnd.endPoll(message)
if (detailedResultsMessage) {
const threadParent = select("channel_room", "thread_parent", {channel_id: message.channel_id}).pluck().get()
- const {channelID, threadID} = dUtils.swapThreadID(message.channel_id, threadParent)
+ const channelID = threadParent ? threadParent : message.channel_id
+ const threadID = threadParent ? message.channel_id : undefined
sentResultsMessage = await channelWebhook.sendMessageWithWebhook(channelID, detailedResultsMessage, threadID)
}
}
diff --git a/src/d2m/actions/speedbump.js b/src/d2m/actions/speedbump.js
index 42e3a35..218f046 100644
--- a/src/d2m/actions/speedbump.js
+++ b/src/d2m/actions/speedbump.js
@@ -1,5 +1,6 @@
// @ts-check
+const DiscordTypes = require("discord-api-types/v10")
const passthrough = require("../../passthrough")
const {discord, select, db} = passthrough
@@ -69,18 +70,12 @@ async function doSpeedbump(messageID) {
* Check whether to slow down a message, and do it. After it passes the speedbump, return whether it's okay or if it's been deleted.
* @param {string} channelID
* @param {string} messageID
- * @param {string} [userID] if provided, only slow down the message when the user has used PK before
* @returns whether it was deleted, and data about the channel's (not thread's) speedbump
*/
-async function maybeDoSpeedbump(channelID, messageID, userID) {
- let row = select("channel_room", ["room_id", "thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: channelID}).get()
- if (row?.thread_parent) row = select("channel_room", ["room_id", "thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: row.thread_parent}).get() // webhooks belong to the channel, not the thread
- if (!row?.speedbump_webhook_id) return {affected: false, row: null} // channel not affected, no speedbump
- if (userID) {
- if (row.speedbump_webhook_id === userID) return {affected: false, row} // shortcut
- const userHasProxy = select("sim_proxy", "user_id", {proxy_owner_id: userID}).pluck().get()
- if (!userHasProxy) return {affected: false, row} // user has not used PK before, no speedbump
- }
+async function maybeDoSpeedbump(channelID, messageID) {
+ let row = select("channel_room", ["thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: channelID}).get()
+ if (row?.thread_parent) row = select("channel_room", ["thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: row.thread_parent}).get() // webhooks belong to the channel, not the thread
+ if (!row?.speedbump_webhook_id) return {affected: false, row: null} // not affected, no speedbump
const affected = await doSpeedbump(messageID)
return {affected, row} // maybe affected, and there is a speedbump
}
diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js
index aeda572..7229d3d 100644
--- a/src/d2m/converters/message-to-event.js
+++ b/src/d2m/converters/message-to-event.js
@@ -265,9 +265,8 @@ function getFormattedInteraction(interaction, isThinkingInteraction) {
* @param {any} newEvents merge into events
* @param {any} events will be modified
* @param {boolean} forceSameMsgtype whether m.text may only be combined with m.text, etc
- * @param {boolean} [forceMerge] if true, must merge event, will error if it had to append
*/
-function mergeTextEvents(newEvents, events, forceSameMsgtype, forceMerge = false) {
+function mergeTextEvents(newEvents, events, forceSameMsgtype) {
let prev = events.at(-1)
for (const ne of newEvents) {
const isAllText = prev?.body && prev?.formatted_body && ["m.text", "m.notice"].includes(ne.msgtype) && ["m.text", "m.notice"].includes(prev?.msgtype)
@@ -279,8 +278,6 @@ function mergeTextEvents(newEvents, events, forceSameMsgtype, forceMerge = false
rep.addLine(ne.body, ne.formatted_body)
prev.body = rep.body
prev.formatted_body = rep.formattedBody
- } else if (forceMerge) {
- throw new Error("Unable to merge events")
} else {
events.push(ne)
}
@@ -557,7 +554,7 @@ async function messageToEvent(message, guild, options = {}, di) {
// Handling emojis that we don't know about. The emoji has to be present in the DB for it to be picked up in the emoji markdown converter.
// So we scan the message ahead of time for all its emojis and ensure they are in the DB.
- const emojiMatches = [...content.matchAll(/<(a?):([^:>]+):([0-9]+)>/g)]
+ const emojiMatches = [...content.matchAll(/<(a?):([^:>]{1,64}):([0-9]+)>/g)]
await Promise.all(emojiMatches.map(match => {
const id = match[3]
const name = match[2]
@@ -970,8 +967,7 @@ async function messageToEvent(message, guild, options = {}, di) {
// May only be a section accessory or in an action row (up to 5)
if (component.style === DiscordTypes.ButtonStyle.Link) {
assert(component.label) // required for Discord to validate link buttons
- const link = await transformContentMessageLinks(component.url)
- stack.msb.add(`[${component.label} ${link}] `, tag`${component.label} `)
+ stack.msb.add(`[${component.label} ${component.url}] `, tag`${component.label} `)
}
}
@@ -984,19 +980,7 @@ async function messageToEvent(message, guild, options = {}, di) {
const {body, formatted_body} = stack.msb.get()
if (body.trim().length) {
- // Create new message if Components V2 (cannot have regular content)
- if ((message.flags ?? 0) & DiscordTypes.MessageFlags.IsComponentsV2) {
- await addTextEvent(body, formatted_body, "m.text")
- }
- // Add to existing message if legacy components https://docs.discord.com/developers/components/reference#legacy-message-component-behavior
- else {
- mergeTextEvents([{
- msgtype: "m.text",
- body,
- format: "org.matrix.custom.html",
- formatted_body
- }], events, false, true)
- }
+ await addTextEvent(body, formatted_body, "m.text")
}
}
diff --git a/src/d2m/converters/message-to-event.test.components.js b/src/d2m/converters/message-to-event.test.components.js
index 1ef83c3..137b63b 100644
--- a/src/d2m/converters/message-to-event.test.components.js
+++ b/src/d2m/converters/message-to-event.test.components.js
@@ -1,7 +1,6 @@
const {test} = require("supertape")
const {messageToEvent} = require("./message-to-event")
const data = require("../../../test/data")
-const {mockGetEffectivePower} = require("../../matrix/utils.test")
test("message2event components: pk question mark output", async t => {
const events = await messageToEvent(data.message_with_components.pk_question_mark_response, data.guild.general, {})
@@ -78,24 +77,3 @@ test("message2event components: pk question mark output", async t => {
msgtype: "m.text",
}])
})
-
-test("message2event components: pk ping message legacy components", async t => {
- const events = await messageToEvent(data.message_with_components.pk_ping_components_v1, data.guild.general, {}, {
- api: {
- async getJoinedMembers() {
- return {joined: {}}
- },
- getEffectivePower: mockGetEffectivePower()
- }
- })
- t.deepEqual(events, [{
- $type: "m.room.message",
- msgtype: "m.text",
- body: "โญ cadence used `/๐ Ping author`"
- + "\nPsst, **Red** (@cadence.worm:), you have been pinged by @cadence.worm:."
- + "\n[Jump https://matrix.to/#/!TqlyQmifxGUggEmdBN:cadence.moe/$l9FMmsEbh9K0NUReeEpWOMZYGRlUOE8yLcm6P-TYHSM?via=cadence.moe] ",
- format: "org.matrix.custom.html",
- formatted_body: "
โญ cadence used /๐ Ping author
Psst, Red (@cadence.worm), you have been pinged by @cadence.worm.
Jump ",
- "m.mentions": {}
- }])
-})
diff --git a/src/d2m/discord-client.js b/src/d2m/discord-client.js
index b9b45e2..c2a0549 100644
--- a/src/d2m/discord-client.js
+++ b/src/d2m/discord-client.js
@@ -14,8 +14,6 @@ const {sync} = passthrough
/** @type {import("./discord-packets")} */
const discordPackets = sync.require("./discord-packets")
-const CONNECTION_DEBUG = false
-
class DiscordClient {
/**
* @param {string} discordToken
@@ -26,7 +24,7 @@ class DiscordClient {
const intents = [
"DIRECT_MESSAGES", "DIRECT_MESSAGE_REACTIONS", "DIRECT_MESSAGE_TYPING",
"GUILDS", "GUILD_EMOJIS_AND_STICKERS", "GUILD_MESSAGES", "GUILD_MESSAGE_REACTIONS", "GUILD_MESSAGE_TYPING", "GUILD_WEBHOOKS", "GUILD_MESSAGE_POLLS",
- "MESSAGE_CONTENT", "GUILD_MEMBERS"
+ "MESSAGE_CONTENT"
]
if (reg.ooye.receive_presences !== false) intents.push("GUILD_PRESENCES")
this.discordToken = discordToken
@@ -61,17 +59,15 @@ class DiscordClient {
})
}
- if (CONNECTION_DEBUG) {
- const addEventLogger = (eventName, logName) => {
- this.cloud.on(eventName, (...args) => {
- const d = new Date().toISOString().slice(0, 19)
- console.error(`[${d} Client ${logName}]`, ...args)
- })
- }
- addEventLogger("error", "Error")
- addEventLogger("disconnected", "Disconnected")
- addEventLogger("ready", "Ready")
+ const addEventLogger = (eventName, logName) => {
+ this.cloud.on(eventName, (...args) => {
+ const d = new Date().toISOString().slice(0, 19)
+ console.error(`[${d} Client ${logName}]`, ...args)
+ })
}
+ addEventLogger("error", "Error")
+ addEventLogger("disconnected", "Disconnected")
+ addEventLogger("ready", "Ready")
}
}
diff --git a/src/d2m/discord-packets.js b/src/d2m/discord-packets.js
index b17ad4e..afea9ea 100644
--- a/src/d2m/discord-packets.js
+++ b/src/d2m/discord-packets.js
@@ -1,222 +1,173 @@
// @ts-check
-const assert = require("assert")
-const {scheduler} = require("timers/promises")
+// Discord library internals type beat
+
+const DiscordTypes = require("discord-api-types/v10")
const passthrough = require("../passthrough")
-const {sync} = passthrough
+const {sync, db} = passthrough
-/** @type {import("../matrix/homeserver-status")} */
-const homeserverStatus = sync.require("../matrix/homeserver-status")
+const utils = {
+ /**
+ * @param {import("./discord-client")} client
+ * @param {import("cloudstorm").IGatewayMessage} message
+ * @param {string} listen "full", "half", "no" - whether to set up the event listeners for OOYE to operate
+ */
+ async onPacket(client, message, listen) {
+ // requiring this later so that the client is already constructed by the time event-dispatcher is loaded
+ /** @type {typeof import("./event-dispatcher")} */
+ const eventDispatcher = sync.require("./event-dispatcher")
+ /** @type {import("../discord/register-interactions")} */
+ const interactions = sync.require("../discord/register-interactions")
-let checkedHomeserver = false
+ // Client internals, keep track of the state we need
+ if (message.t === "READY") {
+ if (client.ready) return
+ client.ready = true
+ client.user = message.d.user
+ client.application = message.d.application
+ console.log(`Discord logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`)
+ interactions.registerInteractions()
-/**
- * @param {import("./discord-client")} client
- * @param {import("cloudstorm").IGatewayMessage} message
- * @param {string} listen "full", "half", "no" - whether to set up the event listeners for OOYE to operate
- */
-async function onPacket(client, message, listen) {
- // requiring this later so that the client is already constructed by the time event-dispatcher is loaded
- /** @type {typeof import("./event-dispatcher")} */
- const eventDispatcher = sync.require("./event-dispatcher")
- /** @type {import("../discord/register-interactions")} */
- const interactions = sync.require("../discord/register-interactions")
+ } else if (message.t === "GUILD_CREATE") {
+ message.d.members = message.d.members.filter(m => m.user.id === client.user.id) // only keep the bot's own member - it's needed to determine private channels on web
+ client.guilds.set(message.d.id, message.d)
+ const arr = []
+ client.guildChannelMap.set(message.d.id, arr)
+ for (const channel of message.d.channels || []) {
+ // @ts-ignore
+ channel.guild_id = message.d.id
+ arr.push(channel.id)
+ client.channels.set(channel.id, channel)
+ }
+ for (const thread of message.d.threads || []) {
+ // @ts-ignore
+ thread.guild_id = message.d.id
+ arr.push(thread.id)
+ client.channels.set(thread.id, thread)
+ }
- // Client internals, keep track of the state we need
- if (message.t === "READY") {
- if (client.ready) return
- client.ready = true
- client.user = message.d.user
- client.application = message.d.application
- console.log(`Discord logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`)
- interactions.registerInteractions()
-
- } else if (message.t === "GUILD_CREATE") {
- message.d.members = message.d.members.filter(m => m.user.id === client.user.id) // only keep the bot's own member - it's needed to determine private channels on web
- client.guilds.set(message.d.id, message.d)
- const arr = []
- client.guildChannelMap.set(message.d.id, arr)
- for (const channel of message.d.channels || []) {
- // @ts-ignore
- channel.guild_id = message.d.id
- arr.push(channel.id)
- client.channels.set(channel.id, channel)
- }
- for (const thread of message.d.threads || []) {
- // @ts-ignore
- thread.guild_id = message.d.id
- arr.push(thread.id)
- client.channels.set(thread.id, thread)
- }
-
- if (listen === "full") {
- try {
- /*
- Info about guilds is populated one guild at a time.
- For m->d bridging to work, the guild needs to be populated, so we need to have GUILD_CREATE for the guild.
- If we ping the homeserver, it will send us any pending events, so we need to wait for all GUILD_CREATES before we ping.
- We must attempt a ping because we don't want to try sending missed d->m messages to an offline homeserver.
- This delay can be removed if ONE of the following is done:
- 1. m->d can queue incoming events until their guild exists in memory
- 2. d->m missed messages can have their errors handled and added to queue, rather than pinging first
- */
- let isMainCharacter = false
- if (!checkedHomeserver) {
- checkedHomeserver = true
- isMainCharacter = true
- console.log("Warming up guilds~")
+ if (listen === "full") {
+ try {
+ await eventDispatcher.checkMissedExpressions(message.d)
+ await eventDispatcher.checkMissedMessages(client, message.d)
+ await eventDispatcher.checkMissedPins(client, message.d)
+ await eventDispatcher.checkMissedLeaves(client, message.d)
+ } catch (e) {
+ console.error("Failed to sync missed events. To retry, please fix this error and restart OOYE:")
+ console.error(e)
}
- await scheduler.wait(5000)
- if (isMainCharacter) {
- checkedHomeserver = true
- process.stdout.write("Connecting to homeserver... ")
- await homeserverStatus.homeserverStatus.waitForOnline(true)
- console.log("ok.\nReplaying past events. Welcome to Out Of Your Element.")
+ }
+
+ } else if (message.t === "GUILD_UPDATE") {
+ const guild = client.guilds.get(message.d.id)
+ if (guild) {
+ for (const prop of Object.keys(message.d)) {
+ if (!["channels", "threads"].includes(prop)) {
+ guild[prop] = message.d[prop]
+ }
+ }
+ }
+
+ } else if (message.t === "GUILD_EMOJIS_UPDATE") {
+ const guild = client.guilds.get(message.d.guild_id)
+ if (guild) {
+ guild.emojis = message.d.emojis
+ }
+
+ } else if (message.t === "GUILD_STICKERS_UPDATE") {
+ const guild = client.guilds.get(message.d.guild_id)
+ if (guild) {
+ guild.stickers = message.d.stickers
+ }
+
+ } else if (message.t === "GUILD_ROLE_CREATE" || message.t === "GUILD_ROLE_UPDATE" || message.t === "GUILD_ROLE_DELETE") {
+ const guild = client.guilds.get(message.d.guild_id)
+ /** Delete this in case of UPDATE or DELETE */
+ const targetID = "role_id" in message.d ? message.d.role_id : message.d.role.id
+ /** Add this in case of CREATE or UPDATE */
+ const newRoles = []
+ if ("role" in message.d) newRoles.push(message.d.role)
+ if (guild) {
+ const targetIndex = guild.roles.findIndex(r => r.id === targetID)
+ if (targetIndex !== -1) {
+ // Role already exists. Delete it and maybe replace it.
+ guild.roles.splice(targetIndex, 1, ...newRoles)
} else {
- await homeserverStatus.homeserverStatus.waitForOnline(false)
- }
-
- await eventDispatcher.checkMissedExpressions(message.d)
- await eventDispatcher.checkMissedMessages(client, message.d)
- await eventDispatcher.checkMissedPins(client, message.d)
- await eventDispatcher.checkMissedLeaves(client, message.d)
- } catch (e) {
- console.error("Failed to sync missed events. To retry, please fix this error and restart OOYE:")
- console.error(e)
- }
- }
-
- } else if (message.t === "GUILD_UPDATE") {
- const guild = client.guilds.get(message.d.id)
- if (guild) {
- for (const prop of Object.keys(message.d)) {
- if (!["channels", "threads"].includes(prop)) {
- guild[prop] = message.d[prop]
+ // Role doesn't already exist.
+ guild.roles.push(...newRoles)
}
}
- }
- } else if (message.t === "GUILD_EMOJIS_UPDATE") {
- const guild = client.guilds.get(message.d.guild_id)
- if (guild) {
- guild.emojis = message.d.emojis
- }
-
- } else if (message.t === "GUILD_STICKERS_UPDATE") {
- const guild = client.guilds.get(message.d.guild_id)
- if (guild) {
- guild.stickers = message.d.stickers
- }
-
- } else if (message.t === "GUILD_ROLE_CREATE" || message.t === "GUILD_ROLE_UPDATE" || message.t === "GUILD_ROLE_DELETE") {
- const guild = client.guilds.get(message.d.guild_id)
- /** Delete this in case of UPDATE or DELETE */
- const targetID = "role_id" in message.d ? message.d.role_id : message.d.role.id
- /** Add this in case of CREATE or UPDATE */
- const newRoles = []
- if ("role" in message.d) newRoles.push(message.d.role)
- if (guild) {
- const targetIndex = guild.roles.findIndex(r => r.id === targetID)
- if (targetIndex !== -1) {
- // Role already exists. Delete it and maybe replace it.
- guild.roles.splice(targetIndex, 1, ...newRoles)
- } else {
- // Role doesn't already exist.
- guild.roles.push(...newRoles)
+ } else if (message.t === "GUILD_MEMBER_UPDATE") {
+ const guild = client.guilds.get(message.d.guild_id)
+ const member = guild?.members.find(m => m.user.id === message.d.user.id)
+ if (member) { // only update existing members (i.e. the bot's own member) - don't want to inflate the cache with new irrelevant ones
+ Object.assign(member, message.d)
}
- }
- } else if (message.t === "GUILD_MEMBER_UPDATE") {
- const guild = client.guilds.get(message.d.guild_id)
- const member = guild?.members.find(m => m.user.id === message.d.user.id)
- if (member) { // only update existing members (i.e. the bot's own member) - don't want to inflate the cache with new irrelevant ones
- Object.assign(member, message.d)
- }
+ } else if (message.t === "THREAD_CREATE") {
+ client.channels.set(message.d.id, message.d)
+ if (message.d["guild_id"]) {
+ const channels = client.guildChannelMap.get(message.d["guild_id"])
+ if (channels && !channels.includes(message.d.id)) channels.push(message.d.id)
+ }
- } else if (message.t === "THREAD_CREATE") {
- client.channels.set(message.d.id, message.d)
- if (message.d["guild_id"]) {
- const channels = client.guildChannelMap.get(message.d["guild_id"])
- if (channels && !channels.includes(message.d.id)) channels.push(message.d.id)
- }
-
- } else if (message.t === "CHANNEL_UPDATE" || message.t === "THREAD_UPDATE") {
- client.channels.set(message.d.id, message.d)
+ } else if (message.t === "CHANNEL_UPDATE" || message.t === "THREAD_UPDATE") {
+ client.channels.set(message.d.id, message.d)
- } else if (message.t === "CHANNEL_PINS_UPDATE") {
- const channel = client.channels.get(message.d.channel_id)
- if (channel) {
- channel["last_pin_timestamp"] = message.d.last_pin_timestamp
- }
+ } else if (message.t === "CHANNEL_PINS_UPDATE") {
+ const channel = client.channels.get(message.d.channel_id)
+ if (channel) {
+ channel["last_pin_timestamp"] = message.d.last_pin_timestamp
+ }
- } else if (message.t === "GUILD_DELETE") {
- client.guilds.delete(message.d.id)
- const channels = client.guildChannelMap.get(message.d.id)
- if (channels) {
- for (const id of channels) client.channels.delete(id)
- }
- client.guildChannelMap.delete(message.d.id)
-
-
- } else if (message.t === "CHANNEL_CREATE") {
- client.channels.set(message.d.id, message.d)
- if (message.d["guild_id"]) { // obj[prop] notation can be used to access a property without typescript complaining that it doesn't exist on all values something can have
- const channels = client.guildChannelMap.get(message.d["guild_id"])
- if (channels && !channels.includes(message.d.id)) channels.push(message.d.id)
- }
-
- } else if (message.t === "CHANNEL_DELETE") {
- client.channels.delete(message.d.id)
- if (message.d["guild_id"]) {
- const channels = client.guildChannelMap.get(message.d["guild_id"])
+ } else if (message.t === "GUILD_DELETE") {
+ client.guilds.delete(message.d.id)
+ const channels = client.guildChannelMap.get(message.d.id)
if (channels) {
- const previous = channels.indexOf(message.d.id)
- if (previous !== -1) channels.splice(previous, 1)
+ for (const id of channels) client.channels.delete(id)
+ }
+ client.guildChannelMap.delete(message.d.id)
+
+
+ } else if (message.t === "CHANNEL_CREATE") {
+ client.channels.set(message.d.id, message.d)
+ if (message.d["guild_id"]) { // obj[prop] notation can be used to access a property without typescript complaining that it doesn't exist on all values something can have
+ const channels = client.guildChannelMap.get(message.d["guild_id"])
+ if (channels && !channels.includes(message.d.id)) channels.push(message.d.id)
+ }
+
+ } else if (message.t === "CHANNEL_DELETE") {
+ client.channels.delete(message.d.id)
+ if (message.d["guild_id"]) {
+ const channels = client.guildChannelMap.get(message.d["guild_id"])
+ if (channels) {
+ const previous = channels.indexOf(message.d.id)
+ if (previous !== -1) channels.splice(previous, 1)
+ }
+ }
+ }
+
+ // Event dispatcher for OOYE bridge operations
+ if (listen === "full" && message.t) {
+ try {
+ if (message.t === "MESSAGE_REACTION_REMOVE" || message.t === "MESSAGE_REACTION_REMOVE_EMOJI" || message.t === "MESSAGE_REACTION_REMOVE_ALL") {
+ await eventDispatcher.onSomeReactionsRemoved(client, message.d)
+
+ } else if (message.t === "INTERACTION_CREATE") {
+ await interactions.dispatchInteraction(message.d)
+
+ } else if (message.t in eventDispatcher) {
+ await eventDispatcher[message.t](client, message.d)
+ }
+ } catch (e) {
+ // Let OOYE try to handle errors too
+ await eventDispatcher.onError(client, e, message)
}
}
}
-
- // Event dispatcher for OOYE bridge operations
- if (listen === "full" && message.t) {
- const alwaysRealTimeEvents = ["PRESENCE_UPDATE"]
- if (alwaysRealTimeEvents.includes(message.t) || homeserverStatus.homeserverStatus.isRealTime()) {
- dispatchPacketToBridge(client, message)
- } else {
- homeserverStatus.homeserverStatus.queuePacket(message)
- }
- }
}
-/**
- * @param {import("./discord-client")} client
- * @param {import("cloudstorm").IGatewayMessage} message
- */
-async function dispatchPacketToBridge(client, message) {
- // requiring this later so that the client is already constructed by the time event-dispatcher is loaded
- /** @type {typeof import("./event-dispatcher")} */
- const eventDispatcher = sync.require("./event-dispatcher")
- /** @type {import("../discord/register-interactions")} */
- const interactions = sync.require("../discord/register-interactions")
-
- assert(message.t) // checked above
-
- try {
- if (message.t === "MESSAGE_REACTION_REMOVE" || message.t === "MESSAGE_REACTION_REMOVE_EMOJI" || message.t === "MESSAGE_REACTION_REMOVE_ALL") {
- await eventDispatcher.onSomeReactionsRemoved(client, message.d)
-
- } else if (message.t === "INTERACTION_CREATE") {
- await interactions.dispatchInteraction(message.d)
-
- } else if (message.t in eventDispatcher) {
- await eventDispatcher[message.t](client, message.d)
- }
- } catch (e) {
- // Let OOYE try to handle errors too
- await eventDispatcher.onError(client, e, message)
- }
-}
-
-module.exports.onPacket = onPacket
-module.exports.dispatchPacketToBridge = dispatchPacketToBridge
+module.exports = utils
diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js
index 31ef7ea..8101a03 100644
--- a/src/d2m/event-dispatcher.js
+++ b/src/d2m/event-dispatcher.js
@@ -60,7 +60,7 @@ module.exports = {
matrixEventDispatcher.printError(gatewayMessage.t, "Discord", e, gatewayMessage)
- const channelID = gatewayMessage.d?.["channel_id"]
+ const channelID = gatewayMessage.d["channel_id"]
if (!channelID) return
const roomID = select("channel_room", "room_id", {channel_id: channelID}).pluck().get()
if (!roomID) return
@@ -313,7 +313,7 @@ module.exports = {
if (!createRoom.existsOrAutocreatable(channel, guild.id)) return // Check that the sending-to room exists or is autocreatable
- const {affected, row} = await speedbump.maybeDoSpeedbump(message.channel_id, message.id, message.author.id)
+ const {affected, row} = await speedbump.maybeDoSpeedbump(message.channel_id, message.id)
if (affected) return
// @ts-ignore
@@ -335,11 +335,13 @@ module.exports = {
if (dUtils.isEphemeralMessage(data)) return // Ephemeral messages are for the eyes of the receiver only!
// Edits need to go through the speedbump as well. If the message is delayed but the edit isn't, we don't have anything to edit from.
- const {affected, row} = await speedbump.maybeDoSpeedbump(data.channel_id, data.id, data.author.id)
+ const {affected, row} = await speedbump.maybeDoSpeedbump(data.channel_id, data.id)
if (affected) return
- // Check that the sending-to room exists, and deal with Eventual Consistency(TM)
- if (!await retrigger.waitForMessage(data.id)) return
+ if (!row) {
+ // Check that the sending-to room exists, and deal with Eventual Consistency(TM)
+ if (!await retrigger.waitForMessage(data.id)) return
+ }
/** @type {DiscordTypes.GatewayMessageCreateDispatchData} */
// @ts-ignore
diff --git a/src/discord/register-interactions.js b/src/discord/register-interactions.js
index dc97287..e3d58c4 100644
--- a/src/discord/register-interactions.js
+++ b/src/discord/register-interactions.js
@@ -16,7 +16,7 @@ const ping = sync.require("./interactions/ping.js")
// User must have EVERY permission in default_member_permissions to be able to use the command
function registerInteractions() {
- discord.snow.interaction.editApplicationCommands(id, [{
+ discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{
name: "Matrix info",
contexts: [DiscordTypes.InteractionContextType.Guild],
type: DiscordTypes.ApplicationCommandType.Message,
@@ -91,32 +91,40 @@ function registerInteractions() {
async function dispatchInteraction(interaction) {
const interactionId = interaction.data?.["custom_id"] || interaction.data?.["name"]
try {
- if (interactionId === "Matrix info") {
- await matrixInfo.interact(interaction)
- } else if (interactionId === "invite") {
- await invite.interact(interaction)
- } else if (interactionId === "invite_channel") {
- await invite.interactButton(interaction)
- } else if (interactionId === "Permissions") {
- await permissions.interact(interaction)
- } else if (interactionId === "permissions_edit") {
- await permissions.interactEdit(interaction)
- } else if (interactionId === "Responses") {
- /** @type {DiscordTypes.APIMessageApplicationCommandGuildInteraction} */ // @ts-ignore
- const messageInteraction = interaction
- if (select("poll", "message_id", {message_id: messageInteraction.data.target_id}).get()) {
- await pollResponses.interact(messageInteraction)
+ if (interaction.type === DiscordTypes.InteractionType.MessageComponent || interaction.type === DiscordTypes.InteractionType.ModalSubmit) {
+ // All we get is custom_id, don't know which context the button was clicked in.
+ // So we namespace these ourselves in the custom_id. Currently the only existing namespace is POLL_.
+ if (interaction.data.custom_id.startsWith("POLL_")) {
+ await poll.interact(interaction)
} else {
- await reactions.interact(messageInteraction)
+ throw new Error(`Unknown message component ${interaction.data.custom_id}`)
}
- } else if (interactionId === "ping") {
- await ping.interact(interaction)
- } else if (interactionId === "privacy") {
- await privacy.interact(interaction)
- } else if (interactionId.startsWith("POLL_")) {
- await poll.interact(interaction)
} else {
- throw new Error(`Unknown interaction ${interactionId}`)
+ if (interactionId === "Matrix info") {
+ await matrixInfo.interact(interaction)
+ } else if (interactionId === "invite") {
+ await invite.interact(interaction)
+ } else if (interactionId === "invite_channel") {
+ await invite.interactButton(interaction)
+ } else if (interactionId === "Permissions") {
+ await permissions.interact(interaction)
+ } else if (interactionId === "permissions_edit") {
+ await permissions.interactEdit(interaction)
+ } else if (interactionId === "Responses") {
+ /** @type {DiscordTypes.APIMessageApplicationCommandGuildInteraction} */ // @ts-ignore
+ const messageInteraction = interaction
+ if (select("poll", "message_id", {message_id: messageInteraction.data.target_id}).get()) {
+ await pollResponses.interact(messageInteraction)
+ } else {
+ await reactions.interact(messageInteraction)
+ }
+ } else if (interactionId === "ping") {
+ await ping.interact(interaction)
+ } else if (interactionId === "privacy") {
+ await privacy.interact(interaction)
+ } else {
+ throw new Error(`Unknown interaction ${interactionId}`)
+ }
}
} catch (e) {
let stackLines = null
diff --git a/src/discord/utils.js b/src/discord/utils.js
index 6fb0b43..0d400f1 100644
--- a/src/discord/utils.js
+++ b/src/discord/utils.js
@@ -182,18 +182,6 @@ function filterTo(xs, fn) {
return filtered
}
-/**
- * The parameters correspond to the columns of the channel_room table.
- * @param {string} rowChannelID thread ID, OR channel ID if there is no thread
- * @param {string | null | undefined} rowThreadParent channel ID if there is a thread
- */
-function swapThreadID(rowChannelID, rowThreadParent) {
- return {
- channelID: rowThreadParent ? rowThreadParent : rowChannelID,
- threadID: rowThreadParent ? rowChannelID : undefined
- }
-}
-
const supportedPlaintextPreviewExtensions = new Set([
"4d",
"abnf",
@@ -594,5 +582,4 @@ module.exports.timestampToSnowflakeInexact = timestampToSnowflakeInexact
module.exports.getPublicUrlForCdn = getPublicUrlForCdn
module.exports.howOldUnbridgedMessage = howOldUnbridgedMessage
module.exports.filterTo = filterTo
-module.exports.swapThreadID = swapThreadID
module.exports.supportedPlaintextPreviewExtensions = supportedPlaintextPreviewExtensions
diff --git a/src/m2d/actions/emoji-sheet.js b/src/m2d/actions/emoji-sheet.js
index 077240c..ed5ab88 100644
--- a/src/m2d/actions/emoji-sheet.js
+++ b/src/m2d/actions/emoji-sheet.js
@@ -23,7 +23,7 @@ async function getAndConvertEmoji(mxc) {
const res = await api.getMedia(mxc, {signal: abortController.signal})
if (res.status !== 200) {
const root = await res.json()
- throw new mreq.MatrixServerError(root, res.status, {mxc})
+ throw new mreq.MatrixServerError(root, {mxc})
}
const readable = stream.Readable.fromWeb(res.body)
return emojiSheetConverter.convertImageStream(readable, () => {
diff --git a/src/m2d/actions/sticker.js b/src/m2d/actions/sticker.js
index f5a1e16..8eeb5d2 100644
--- a/src/m2d/actions/sticker.js
+++ b/src/m2d/actions/sticker.js
@@ -22,7 +22,7 @@ async function getAndResizeSticker(mxc) {
const res = await api.getMedia(mxc)
if (res.status !== 200) {
const root = await res.json()
- throw new mreq.MatrixServerError(root, res.status, {mxc})
+ throw new mreq.MatrixServerError(root, {mxc})
}
const streamIn = Readable.fromWeb(res.body)
diff --git a/src/m2d/actions/update-pins.js b/src/m2d/actions/update-pins.js
index 1b3f1bd..1ff2bb9 100644
--- a/src/m2d/actions/update-pins.js
+++ b/src/m2d/actions/update-pins.js
@@ -16,9 +16,9 @@ async function updatePins(pins, prev) {
.select("reference_channel_id", "message_id").where({event_id}).and("ORDER BY part ASC").get()
if (!row) continue
if (added) {
- discord.snow.channel.createChannelPinnedMessage(row.reference_channel_id, row.message_id, "Message pinned on Matrix")
+ discord.snow.channel.addChannelPinnedMessage(row.reference_channel_id, row.message_id, "Message pinned on Matrix")
} else {
- discord.snow.channel.deleteChannelPinnedMessage(row.reference_channel_id, row.message_id, "Message unpinned on Matrix")
+ discord.snow.channel.removeChannelPinnedMessage(row.reference_channel_id, row.message_id, "Message unpinned on Matrix")
}
}
}
diff --git a/src/m2d/actions/vote.js b/src/m2d/actions/vote.js
index 84f8cc7..926b957 100644
--- a/src/m2d/actions/vote.js
+++ b/src/m2d/actions/vote.js
@@ -1,12 +1,18 @@
// @ts-check
const Ty = require("../../types")
+const DiscordTypes = require("discord-api-types/v10")
+const {Readable} = require("stream")
const assert = require("assert").strict
+const crypto = require("crypto")
const passthrough = require("../../passthrough")
-const {sync, db, select} = passthrough
+const {sync, discord, db, select} = passthrough
-/** @type {import("../../discord/utils")} */
-const dUtils = sync.require("../../discord/utils")
+const {reg} = require("../../matrix/read-registration")
+/** @type {import("../../matrix/api")} */
+const api = sync.require("../../matrix/api")
+/** @type {import("../../matrix/utils")} */
+const utils = sync.require("../../matrix/utils")
/** @type {import("../converters/poll-components")} */
const pollComponents = sync.require("../converters/poll-components")
/** @type {import("./channel-webhook")} */
@@ -27,11 +33,10 @@ async function updateVote(event) {
// If poll was started on Matrix, the Discord version is using components, so we can update that to the current status
if (messageRow.source === 0) {
- const row = select("channel_room", ["channel_id", "thread_parent"], {room_id: event.room_id}).get()
- assert(row)
- const {channelID, threadID} = dUtils.swapThreadID(row.channel_id, row.thread_parent)
- await webhook.editMessageWithWebhook(channelID, messageID, pollComponents.getPollComponentsFromDatabase(messageID), threadID)
+ const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
+ assert(channelID)
+ await webhook.editMessageWithWebhook(channelID, messageID, pollComponents.getPollComponentsFromDatabase(messageID))
}
}
-module.exports.updateVote = updateVote
+module.exports.updateVote = updateVote
\ No newline at end of file
diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js
index fbaf769..0a18a14 100644
--- a/src/m2d/converters/event-to-message.js
+++ b/src/m2d/converters/event-to-message.js
@@ -10,7 +10,6 @@ const domino = require("domino")
const assert = require("assert").strict
const entities = require("entities")
const pb = require("prettier-bytes")
-const mimeTypes = require("mime-types")
const {tag} = require("@cloudrac3r/html-template-tag")
const passthrough = require("../../passthrough")
@@ -98,6 +97,15 @@ turndownService.addRule("underline", {
}
})
+turndownService.addRule("blockquote", {
+ filter: "blockquote",
+ replacement: function (content) {
+ content = content.replace(/^\n+|\n+$/g, "")
+ content = content.replace(/^/gm, "> ")
+ return content
+ }
+})
+
turndownService.addRule("spoiler", {
filter: function (node, options) {
return node.tagName === "SPAN" && node.hasAttribute("data-mx-spoiler")
@@ -614,20 +622,13 @@ async function eventToMessage(event, guild, channel, di) {
captionContent.addLine(`(Spoiler: ${fileSpoilerReason})`)
}
- // If filename is not specified, create one from mimetype
- let filename = event.content.filename || event.content.body
- if (!filename) {
- const extension = mimeTypes.extension(event.content.info?.mimetype)
- if (!extension) throw new Error("Filename and mimetype missing. Please report this bug to your client.")
- filename = `file.${extension}`
- }
-
// File link as alternative to uploading
if (!("file" in event.content) && event.content.info?.size > getFileSizeForGuild(guild)) {
// Upload (unencrypted) file as link, because it's too large for Discord
// Do this by constructing a sample Matrix message with the link and then use the text processor to convert that + the original caption.
const url = mxUtils.getPublicUrlForMxc(event.content.url)
assert(url)
+ const filename = event.content.filename || event.content.body
const emoji = attachmentEmojis.has(event.content.msgtype) ? attachmentEmojis.get(event.content.msgtype) + " " : ""
if (fileIsSpoiler) {
captionContent.addLine(`${emoji}Uploaded SPOILER file: <${url}> (${pb(event.content.info.size)})`, tag`${emoji}Uploaded SPOILER file: ${filename} (${pb(event.content.info.size)})`) // the space is necessary to work around a bug in Discord's URL previewer. the preview still gets blurred in the client.
@@ -636,6 +637,7 @@ async function eventToMessage(event, guild, channel, di) {
}
} else {
// Upload file as file
+ let filename = event.content.filename || event.content.body
if (fileIsSpoiler) filename = "SPOILER_" + filename
if ("file" in event.content) {
// Encrypted
@@ -853,7 +855,7 @@ async function eventToMessage(event, guild, channel, di) {
input = input.replace(/("https:\/\/matrix.to.*?<\/a>):?/g, "$1")
// Element adds a bunch of
before but doesn't render them. I can't figure out how this even works in the browser, so let's just delete those.
- input = input.replace(/(?:\n|
\s*)*<\/blockquote>(?:\n|
\s*)*/g, "")
+ input = input.replace(/(?:\n|
\s*)*<\/blockquote>/g, "")
// The matrix spec hasn't decided whether \n counts as a newline or not, but I'm going to count it, because if it's in the data it's there for a reason.
// But I should not count it if it's between block elements.
diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js
index 158c2ef..650e442 100644
--- a/src/m2d/converters/event-to-message.test.js
+++ b/src/m2d/converters/event-to-message.test.js
@@ -855,40 +855,6 @@ test("event2message: html lines are bridged correctly", async t => {
)
})
-test("event2message: breaks between consecutive blockquotes are preserved", async t => {
- t.deepEqual(
- await eventToMessage({
- type: "m.room.message",
- sender: "@cadence:cadence.moe",
- content: {
- msgtype: "m.text",
- body: "this was inspired by someone telling me\n\n>betrothed is an insane vocab pull\n\n> i think thats part of why im so scared of cadence\n\n> if an insult comes my way, not only do i not know when, but i'll have to humiliate myself and look it up",
- format: "org.matrix.custom.html",
- formatted_body: "this was inspired by someone telling me
\n\nbetrothed is an insane vocab pull
\n
\n\ni think thats part of why im so scared of cadence
\n
\n\nif an insult comes my way, not only do i not know when, but i'll have to humiliate myself and look it up
\n
"
- },
- origin_server_ts: 1783571303430,
- event_id: "$cdltlkLTUp-ma2uYmLKy7o3RkDgpDbtdYfngST_igWE",
- room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- allowed_mentions: {
- parse: ["users", "roles"]
- },
- username: "cadence [they]",
- avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90",
- content: "this was inspired by someone telling me"
- + "\n\n> betrothed is an insane vocab pull"
- + "\n\n> i think thats part of why im so scared of cadence"
- + "\n\n> if an insult comes my way, not only do i not know when, but i'll have to humiliate myself and look it up"
- }]
- }
- )
-})
-
/*test("event2message: whitespace is retained", async t => {
t.deepEqual(
await eventToMessage({
@@ -1328,7 +1294,7 @@ test("event2message: characters are encoded properly in code blocks", async t =>
)
})
-test("event2message: quotes have an appropriate amount of whitespace (br following)", async t => {
+test("event2message: quotes have an appropriate amount of whitespace", async t => {
t.deepEqual(
await eventToMessage({
content: {
@@ -1362,40 +1328,6 @@ test("event2message: quotes have an appropriate amount of whitespace (br followi
)
})
-test("event2message: quotes have an appropriate amount of whitespace (p following)", async t => {
- t.deepEqual(
- await eventToMessage({
- content: {
- msgtype: "m.text",
- body: "wrong body",
- format: "org.matrix.custom.html",
- formatted_body: "\nshort story
\n
\nby whose standards? LOL
\nI basically went all the way back to childhood (which analysts always like you to do)
\n
\nlaughed out loud
"
- },
- event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
- origin_server_ts: 1688301929913,
- room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
- sender: "@cadence:cadence.moe",
- type: "m.room.message",
- unsigned: {
- age: 405299
- }
- }),
- {
- ensureJoined: [],
- messagesToDelete: [],
- messagesToEdit: [],
- messagesToSend: [{
- username: "cadence [they]",
- content: "> short story\nby whose standards? LOL\n\n> I basically went all the way back to childhood (which analysts always like you to do)\nlaughed out loud",
- avatar_url: "https://bridge.example.org/download/letter-avatar?letter=C&hue=90",
- allowed_mentions: {
- parse: ["users", "roles"]
- }
- }]
- }
- )
-})
-
test("event2message: lists have appropriate line breaks", async t => {
t.deepEqual(
await eventToMessage({
diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js
index 129fca5..3580d1b 100644
--- a/src/m2d/event-dispatcher.js
+++ b/src/m2d/event-dispatcher.js
@@ -24,8 +24,6 @@ const vote = sync.require("./actions/vote")
const matrixCommandHandler = sync.require("../matrix/matrix-command-handler")
/** @type {import("../matrix/utils")} */
const utils = sync.require("../matrix/utils")
-/** @type {import("../matrix/mreq")}) */
-const mreq = sync.require("../matrix/mreq")
/** @type {import("../matrix/api")}) */
const api = sync.require("../matrix/api")
/** @type {import("../d2m/actions/create-room")} */
@@ -34,8 +32,6 @@ const createRoom = sync.require("../d2m/actions/create-room")
const roomUpgrade = require("../matrix/room-upgrade")
/** @type {import("../d2m/actions/retrigger")} */
const retrigger = sync.require("../d2m/actions/retrigger")
-/** @type {import("../matrix/homeserver-status")} */
-const homeserverStatus = sync.require("../matrix/homeserver-status")
const {reg} = require("../matrix/read-registration")
let lastReportedEvent = 0
@@ -170,12 +166,7 @@ async function sendError(roomID, source, type, e, payload) {
key: "๐"
}
})
- } catch (e) {
- if (e instanceof mreq.MatrixServerError && [502, 503].includes(e.httpStatus) && e.errcode === "CX_SERVER_ERROR") {
- // Matrix homeserver is down (reverse proxy indicated failure)
- homeserverStatus.homeserverStatus.setErrorWithPacket(payload)
- }
- }
+ } catch (e) {}
}
function guard(type, fn) {
diff --git a/src/matrix/api.js b/src/matrix/api.js
index 47fc565..9b7f280 100644
--- a/src/matrix/api.js
+++ b/src/matrix/api.js
@@ -452,8 +452,7 @@ async function ping() {
headers: {
Authorization: `Bearer ${reg.as_token}`
},
- body: "{}",
- signal: AbortSignal.timeout(15e3)
+ body: "{}"
})
const root = await res.json()
return {
@@ -475,7 +474,7 @@ async function ping() {
async function getMedia(mxc, init = {}) {
init = {...init}
- const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/([a-zA-Z0-9_-]+)$/)
+ const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
assert(mediaParts)
let route = "download"
diff --git a/src/matrix/homeserver-status.js b/src/matrix/homeserver-status.js
deleted file mode 100644
index 3ceac84..0000000
--- a/src/matrix/homeserver-status.js
+++ /dev/null
@@ -1,134 +0,0 @@
-// @ts-check
-
-const assert = require("assert")
-const Denque = require("denque")
-const StateMachine = require("snowtransfer").StateMachine
-
-const passthrough = require("../passthrough")
-const {sync} = passthrough
-/** @type {import("../d2m/discord-packets")} */
-const discordPackets = sync.require("../d2m/discord-packets")
-/** @type {import("../matrix/api")} */
-const api = sync.require("../matrix/api")
-
-const DEBUG_HOMESERVER_STATUS = false
-
-function debugHomeserverStatus(message) {
- if (DEBUG_HOMESERVER_STATUS) {
- console.log(message)
- }
-}
-
-const homeserverStatus = new class HomeserverStatus {
- constructor() {
- /** @private */
- this.queue = new Denque()
-
- /** @private */
- this.pingInterval = undefined
-
- /** @private */
- this.sm = new StateMachine("online")
- .defineState("online")
-
- .defineState("checking", {
- onEnter: [async () => {
- const pingResult = await api.ping().catch(e => ({ok: false}))
- if (pingResult.ok) {
- this.sm.doTransition("check ok")
- } else {
- this.sm.doTransition("check fail")
- }
- }],
- onLeave: [],
- transitions: new Map()
- })
-
- .defineState("offline", {
- onEnter: [() => {
- this.pingInterval = setInterval(async () => {
- const pingResult = await api.ping().catch(e => ({ok: false, status: "net", root: e.message}))
- if (pingResult.ok) {
- this.sm.doTransition("ping ok")
- }
- }, 15e3)
- }],
- onLeave: [() => {
- clearInterval(this.pingInterval)
- }],
- transitions: new Map()
- })
-
- .defineState("recovering", {
- onEnter: [async () => { // Drain queue.
- while (!this.queue.isEmpty()) {
- const packet = this.queue.peekFront() // same position as .shift()
- debugHomeserverStatus(`homeserver status: ${new Date().toISOString()} dq packet ${packet.t} ${packet.d?.content}`)
- await discordPackets.dispatchPacketToBridge(passthrough.discord, packet)
- if (this.sm.currentStateName !== "recovering") return // got kicked out due to another error
- this.queue.shift()
- }
- this.sm.doTransition("recovered")
- }],
- onLeave: [],
- transitions: new Map()
- })
-
- .defineUniversalTransition("error", "offline")
- .defineTransition("offline", "ping ok", "recovering")
- .defineTransition("recovering", "recovered", "online")
- .defineTransition("online", "check", "checking")
- .defineTransition("checking", "check ok", "recovering")
- .defineTransition("checking", "check fail", "offline")
-
- this.sm.on("enter", st => debugHomeserverStatus(`homeserver status: ${st}`))
- this.sm.setMaxListeners(101)
- this.sm.freeze()
- }
-
- isRealTime() {
- return this.sm.currentStateName === "online"
- }
-
- /** @param {boolean} forceCheck */
- waitForOnline(forceCheck) {
- const onlinePromise = new Promise(resolve => {
- // Already online? Start check or just done
- if (this.sm.currentStateName === "online") {
- if (forceCheck) {
- this.sm.doTransition("check")
- } else {
- return resolve(null)
- }
- }
-
- // Checking or not online. Wait for online.
- const onlineListener = stateName => {
- if (stateName === "online") {
- this.sm.removeListener("enter", onlineListener)
- resolve(null)
- }
- }
- this.sm.on("enter", onlineListener)
- })
- return onlinePromise
- }
-
- /**
- * When offline or recovering, call this for incoming packets to queue them to be sent in order later.
- */
- queuePacket(packet) {
- assert(["offline", "recovering"].includes(this.sm.currentStateName))
- this.queue.push(packet)
- }
-
- setErrorWithPacket(packet) {
- const wasRecovering = this.sm.currentStateName === "recovering"
- this.sm.doTransition("error")
- if (!wasRecovering) { // if was recovering then packet is already in the right place in queue
- this.queuePacket(packet)
- }
- }
-}
-
-module.exports.homeserverStatus = homeserverStatus
diff --git a/src/matrix/mreq.js b/src/matrix/mreq.js
index bf238f2..bb59506 100644
--- a/src/matrix/mreq.js
+++ b/src/matrix/mreq.js
@@ -9,12 +9,9 @@ const {reg} = require("./read-registration.js")
const baseUrl = `${reg.ooye.server_origin}/_matrix`
class MatrixServerError extends Error {
- /** @param {number} httpStatus} */
- constructor(data, httpStatus, opts) {
+ constructor(data, opts) {
super(data.error || data.errcode)
this.data = data
- /** @type {number} */
- this.httpStatus = httpStatus
/** @type {string} */
this.errcode = data.errcode
this.opts = opts
@@ -47,11 +44,11 @@ async function _convertBody(body) {
async function makeMatrixServerError(res, opts = {}) {
delete opts.headers?.["Authorization"]
if (res.headers.get("content-type") === "application/json") {
- return new MatrixServerError(await res.json(), res.status, opts)
+ return new MatrixServerError(await res.json(), opts)
} else if (res.headers.get("content-type")?.startsWith("text/")) {
- return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, message: await res.text()}, res.status, opts)
+ return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, message: await res.text()}, opts)
} else {
- return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, content_type: res.headers.get("content-type")}, res.status, opts)
+ return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, content_type: res.headers.get("content-type")}, opts)
}
}
@@ -81,12 +78,12 @@ async function mreq(method, url, bodyIn, extra = {}) {
var root = JSON.parse(text)
} catch (e) {
delete opts.headers?.["Authorization"]
- throw new MatrixServerError(text, res.status, {baseUrl, url, ...opts})
+ throw new MatrixServerError(text, {baseUrl, url, ...opts})
}
if (!res.ok || root.errcode) {
delete opts.headers?.["Authorization"]
- throw new MatrixServerError(root, res.status, {baseUrl, url, ...opts})
+ throw new MatrixServerError(root, {baseUrl, url, ...opts})
}
return root
}
diff --git a/src/matrix/utils.js b/src/matrix/utils.js
index fed71c9..eee635b 100644
--- a/src/matrix/utils.js
+++ b/src/matrix/utils.js
@@ -250,7 +250,7 @@ function getPublicUrlForMxc(mxc) {
*/
function makeMxcPublic(mxc) {
assert(hasher, "xxhash is not ready yet")
- const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/([a-zA-Z0-9_-]+)$/)
+ const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
if (!mediaParts) return undefined
const serverAndMediaID = `${mediaParts[1]}/${mediaParts[2]}`
diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug
index 7411a1e..9791ae3 100644
--- a/src/web/pug/guild.pug
+++ b/src/web/pug/guild.pug
@@ -122,7 +122,7 @@ block body
#role-add.s-popover(popover style="display: revert").ws2.px0.py4.bs-lg.overflow-visible
.s-popover--arrow.s-popover--arrow__tc
+add-roles-menu(guild, guild_id)
- p.fc-light.mb0.mt8 Matrix users will start with these roles. If your main channels are gated by a role, use this to let Matrix users skip the gate.
+ p.fc-medium.mb0.mt8 Matrix users will start with these roles. If your main channels are gated by a role, use this to let Matrix users skip the gate.
h3.mt32.fs-category Features
.s-card.d-grid.px0.g16
@@ -191,14 +191,14 @@ block body
label.s-btn.s-btn__muted.ta-left.truncate(for=channel.id)
+discord(channel, true, "Announcement")
else
- .s-empty-state.p8 No Discord channels available.
+ .s-empty-state.p8 All Discord channels are linked.
.fl-grow1.s-btn-group.fd-column.w30
each room in unlinkedRooms
input.s-btn--radio(type="radio" name="matrix" required id=room.room_id value=room.room_id)
label.s-btn.s-btn__muted.ta-left.truncate(for=room.room_id)
+matrix(room, true)
else
- .s-empty-state.p8 No Matrix rooms available.
+ .s-empty-state.p8 All Matrix rooms are linked.
input(type="hidden" name="guild_id" value=guild_id)
div
button.s-btn.s-btn__icon.s-btn__filled#link-button
diff --git a/start.js b/start.js
index 51942c6..39e8ea0 100755
--- a/start.js
+++ b/start.js
@@ -31,9 +31,8 @@ sync.require("./src/m2d/event-dispatcher")
;(async () => {
await migrate.migrate(db)
- process.stdout.write("Connecting to Discord... ")
await discord.cloud.connect()
- console.log("ok.")
+ console.log("Discord gateway started")
sync.require("./src/web/server")
await power.applyPower()
diff --git a/test/data.js b/test/data.js
index eab9a63..f3092bc 100644
--- a/test/data.js
+++ b/test/data.js
@@ -5473,189 +5473,6 @@ module.exports = {
content: '-# Original Message ID: 1466556003645657118 ยท '
}
]
- },
- pk_ping_components_v1: {
- type: 23,
- content: "Psst, **Red** (<@772659086046658620>), you have been pinged by <@772659086046658620>.",
- mentions: [
- {
- id: "772659086046658620",
- username: "cadence.worm",
- avatar: "466df0c98b1af1e1388f595b4c1ad1b9",
- discriminator: "0",
- public_flags: 0,
- flags: 0,
- banner: null,
- accent_color: null,
- global_name: "cadence",
- avatar_decoration_data: null,
- collectibles: null,
- display_name_styles: null,
- banner_color: null,
- clan: {
- identity_guild_id: "532245108070809601",
- identity_enabled: true,
- tag: "doll",
- badge: "dba08126b4e810a0e096cc7cd5bc37f0"
- },
- primary_guild: {
- identity_guild_id: "532245108070809601",
- identity_enabled: true,
- tag: "doll",
- badge: "dba08126b4e810a0e096cc7cd5bc37f0"
- }
- }
- ],
- mention_roles: [],
- attachments: [],
- embeds: [],
- timestamp: "2026-03-25T07:07:02.626000+00:00",
- edited_timestamp: null,
- flags: 0,
- components: [
- {
- type: 1,
- id: 1,
- components: [
- {
- type: 2,
- id: 2,
- style: 5,
- label: "Jump",
- url: "https://discord.com/channels/1160893336324931584/1160894080998461480/1440549403667468320"
- }
- ]
- }
- ],
- id: "1486260105908457653",
- channel_id: "1160894080998461480",
- author: {
- id: "466378653216014359",
- username: "PluralKit",
- avatar: "b78ef67a081737a830b60aa47d9ebcd9",
- discriminator: "4020",
- public_flags: 65536,
- flags: 65536,
- bot: true,
- banner: null,
- accent_color: null,
- global_name: null,
- avatar_decoration_data: null,
- collectibles: null,
- display_name_styles: null,
- banner_color: null,
- clan: null,
- primary_guild: null
- },
- pinned: false,
- mention_everyone: false,
- tts: false,
- application_id: "466378653216014359",
- interaction: {
- id: "1486260103928614932",
- type: 2,
- name: "๐ Ping author",
- user: {
- id: "772659086046658620",
- username: "cadence.worm",
- avatar: "466df0c98b1af1e1388f595b4c1ad1b9",
- discriminator: "0",
- public_flags: 0,
- flags: 0,
- banner: null,
- accent_color: null,
- global_name: "cadence",
- avatar_decoration_data: null,
- collectibles: null,
- display_name_styles: null,
- banner_color: null,
- clan: {
- identity_guild_id: "532245108070809601",
- identity_enabled: true,
- tag: "doll",
- badge: "dba08126b4e810a0e096cc7cd5bc37f0"
- },
- primary_guild: {
- identity_guild_id: "532245108070809601",
- identity_enabled: true,
- tag: "doll",
- badge: "dba08126b4e810a0e096cc7cd5bc37f0"
- }
- }
- },
- webhook_id: "466378653216014359",
- message_reference: {
- type: 0,
- channel_id: "1160894080998461480",
- message_id: "1440549403667468320",
- guild_id: "1160893336324931584"
- },
- interaction_metadata: {
- id: "1486260103928614932",
- type: 2,
- user: {
- id: "772659086046658620",
- username: "cadence.worm",
- avatar: "466df0c98b1af1e1388f595b4c1ad1b9",
- discriminator: "0",
- public_flags: 0,
- flags: 0,
- banner: null,
- accent_color: null,
- global_name: "cadence",
- avatar_decoration_data: null,
- collectibles: null,
- display_name_styles: null,
- banner_color: null,
- clan: {
- identity_guild_id: "532245108070809601",
- identity_enabled: true,
- tag: "doll",
- badge: "dba08126b4e810a0e096cc7cd5bc37f0"
- },
- primary_guild: {
- identity_guild_id: "532245108070809601",
- identity_enabled: true,
- tag: "doll",
- badge: "dba08126b4e810a0e096cc7cd5bc37f0"
- }
- },
- authorizing_integration_owners: { "0": "1160893336324931584" },
- name: "๐ Ping author",
- command_type: 3,
- target_message_id: "1440549403667468320"
- },
- referenced_message: {
- type: 0,
- content: "test",
- mentions: [],
- mention_roles: [],
- attachments: [],
- embeds: [],
- timestamp: "2025-11-19T03:49:01.948000+00:00",
- edited_timestamp: null,
- flags: 0,
- components: [],
- id: "1440549403667468320",
- channel_id: "1160894080998461480",
- author: {
- id: "1195662438662680720",
- username: "special name",
- avatar: "a82347890f2739e5880cd82b8c1a708e",
- discriminator: "0000",
- public_flags: 0,
- flags: 0,
- bot: true,
- global_name: null,
- clan: null,
- primary_guild: null
- },
- pinned: false,
- mention_everyone: false,
- tts: false,
- application_id: "466378653216014359",
- webhook_id: "1195662438662680720"
- }
}
},
message_update: {
diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql
index 1662320..8dd71cd 100644
--- a/test/ooye-test-data.sql
+++ b/test/ooye-test-data.sql
@@ -95,8 +95,7 @@ WITH a (message_id, channel_id) AS (VALUES
('1381212840957972480', '112760669178241024'),
('1401760355339862066', '112760669178241024'),
('1439351590262800565', '1438284564815548418'),
-('1404133238414376971', '112760669178241024'),
-('1440549403667468320', '1160894080998461480'))
+('1404133238414376971', '112760669178241024'))
SELECT message_id, max(historical_room_index) as historical_room_index FROM a INNER JOIN historical_channel_room ON historical_channel_room.reference_channel_id = a.channel_id GROUP BY message_id;
INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES
@@ -144,8 +143,7 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part
('$7P2O_VTQNHvavX5zNJ35DV-dbJB1Ag80tGQP_JzGdhk', 'm.room.message', 'm.text', '1401760355339862066', 0, 0, 0),
('$ielAnR6geu0P1Tl5UXfrbxlIf-SV9jrNprxrGXP3v7M', 'm.room.message', 'm.image', '1439351590262800565', 0, 0, 0),
('$uUKLcTQvik5tgtTGDKuzn0Ci4zcCvSoUcYn2X7mXm9I', 'm.room.message', 'm.text', '1404133238414376971', 0, 1, 1),
-('$LhmoWWvYyn5_AHkfb6FaXmLI6ZOC1kloql5P40YDmIk', 'm.room.message', 'm.notice', '1404133238414376971', 1, 0, 1),
-('$l9FMmsEbh9K0NUReeEpWOMZYGRlUOE8yLcm6P-TYHSM', 'm.room.message', 'm.text', '1440549403667468320', 0, 0, 1);
+('$LhmoWWvYyn5_AHkfb6FaXmLI6ZOC1kloql5P40YDmIk', 'm.room.message', 'm.notice', '1404133238414376971', 1, 0, 1);
INSERT INTO file (discord_url, mxc_url) VALUES
('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'),