diff --git a/docs/get-started.md b/docs/get-started.md
index 5b14b2a..17c98de 100644
--- a/docs/get-started.md
+++ b/docs/get-started.md
@@ -56,8 +56,16 @@ After that, to get into the rooms on your Matrix account, use the invite form on
I hope you enjoy Out Of Your Element!
-----
-
+# Extras
+
+## /plu/ral support
+
+To support /plu/ral webhook proxying, OOYE needs to call the /plu/ral API authenticated. I can't provide an API key for everyone to share, so you have to make your own. Here's how:
+
+1. Open the [/plu/ral app on Discord](https://discord.com/discovery/applications/1291501048493768784) and "Add App" -> "Add to My Apps (use everywhere)"
+2. Go to any text channel on Discord and use the new `/api` command from /plu/ral.
+3. Create a new application named Out Of Your Element. Copy the displayed token.
+3. Run `node scripts/plu-ral-api.js` and paste the token.
# Appendix
diff --git a/scripts/plu-ral-api.js b/scripts/plu-ral-api.js
new file mode 100644
index 0000000..d3468a3
--- /dev/null
+++ b/scripts/plu-ral-api.js
@@ -0,0 +1,17 @@
+// @ts-check
+
+const {reg, writeRegistration} = require("../src/matrix/read-registration")
+const {prompt} = require("enquirer")
+
+;(async () => {
+ /** @type {{api_key: string}} */
+ const apiKeyResponse = await prompt({
+ type: "text",
+ name: "api_key",
+ message: "Paste your personal /plu/ral API key"
+ })
+
+ reg.ooye.plu_ral_api_key = apiKeyResponse.api_key
+ writeRegistration(reg)
+ console.log("Saved. This change should be applied instantly.")
+})()
diff --git a/src/d2m/actions/edit-message.js b/src/d2m/actions/edit-message.js
index f86a9c8..31f3627 100644
--- a/src/d2m/actions/edit-message.js
+++ b/src/d2m/actions/edit-message.js
@@ -1,24 +1,24 @@
// @ts-check
-const assert = require("assert").strict
-
const passthrough = require("../../passthrough")
const {sync, db, select, from} = passthrough
+const {reg} = require("../../matrix/read-registration")
/** @type {import("../converters/edit-to-changes")} */
const editToChanges = sync.require("../converters/edit-to-changes")
-/** @type {import("./register-pk-user")} */
-const registerPkUser = sync.require("./register-pk-user")
+/** @type {import("./speedbump")} */
+const speedbump = sync.require("./speedbump")
/** @type {import("../../matrix/api")} */
const api = sync.require("../../matrix/api")
/** @type {import("../../matrix/mreq")} */
const mreq = sync.require("../../matrix/mreq")
+/** @type {import("../../discord/utils")} */
+const dUtils = sync.require("../../discord/utils")
/**
* @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message
* @param {import("discord-api-types/v10").APIGuild} guild
- * @param {{speedbump_id: string, speedbump_webhook_id: string} | null} row data about the webhook which is proxying messages in this channel
*/
-async function editMessage(message, guild, row) {
+async function editMessage(message, guild) {
const historicalRoomOfMessage = from("message_room").join("historical_channel_room", "historical_room_index").where({message_id: message.id}).select("room_id").get()
const currentRoom = from("channel_room").join("historical_channel_room", "room_id").where({channel_id: message.channel_id}).select("room_id", "historical_room_index").get()
if (!currentRoom) return
@@ -27,11 +27,9 @@ async function editMessage(message, guild, row) {
let {roomID, eventsToRedact, eventsToReplace, eventsToSend, senderMxid, promotions} = await editToChanges.editToChanges(message, guild, api)
- if (row && row.speedbump_webhook_id === message.webhook_id) {
- // Handle the PluralKit public instance
- if (row.speedbump_id === "466378653216014359") {
- senderMxid = await registerPkUser.syncUser(message.id, message.author, roomID, true)
- }
+ // Sync proxy user profile (if sent by proxy)
+ if (dUtils.isWebhookMessage(message)) {
+ senderMxid = await speedbump.getWebhookSenderId(message, guild.id, roomID)
}
// 1. Replace all the things.
diff --git a/src/d2m/actions/register-pk-user.js b/src/d2m/actions/register-pk-user.js
index 6ecd077..4946d4f 100644
--- a/src/d2m/actions/register-pk-user.js
+++ b/src/d2m/actions/register-pk-user.js
@@ -1,6 +1,6 @@
// @ts-check
-const assert = require("assert")
+const assert = require("assert").strict
const {reg} = require("../../matrix/read-registration")
const Ty = require("../../types")
@@ -132,10 +132,10 @@ async function syncUser(messageID, author, roomID, shouldActuallySync) {
try {
// API lookup
var pkMessage = await fetchMessage(messageID)
- db.prepare("REPLACE INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES (?, ?, ?)").run(pkMessage.member.uuid, pkMessage.sender, author.username)
+ db.prepare("REPLACE INTO sim_proxy (user_id, proxy_owner_id, displayname, proxy_app) VALUES (?, ?, ?, 0)").run(pkMessage.member.uuid, pkMessage.sender, author.username)
} catch (e) {
// Fall back to offline cache
- const senderMxid = from("sim_proxy").join("sim", "user_id").join("sim_member", "mxid").where({displayname: author.username, room_id: roomID}).pluck("mxid").get()
+ const senderMxid = from("sim_proxy").join("sim", "user_id").join("sim_member", "mxid").where({displayname: author.username, room_id: roomID, proxy_app: 0}).pluck("mxid").get()
if (!senderMxid) throw e
return senderMxid
}
diff --git a/src/d2m/actions/register-plu-ral-user.js b/src/d2m/actions/register-plu-ral-user.js
new file mode 100644
index 0000000..04fa28c
--- /dev/null
+++ b/src/d2m/actions/register-plu-ral-user.js
@@ -0,0 +1,170 @@
+// @ts-check
+
+const assert = require("assert").strict
+const {reg} = require("../../matrix/read-registration")
+const Ty = require("../../types")
+
+const passthrough = require("../../passthrough")
+const {sync, db, select, from} = passthrough
+/** @type {import("../../matrix/api")} */
+const api = sync.require("../../matrix/api")
+/** @type {import("../../matrix/file")} */
+const file = sync.require("../../matrix/file")
+/** @type {import("./register-user")} */
+const registerUser = sync.require("./register-user")
+
+/** @returns {Promise} */
+async function fetchMessage(channelID, messageID) {
+ assert(reg.ooye.plu_ral_api_key)
+ try {
+ var res = await fetch(`https://api.plural.gg/messages/${channelID}/${messageID}?member=true`, {
+ headers: {
+ Authorization: reg.ooye.plu_ral_api_key
+ }
+ })
+ } catch (networkError) {
+ // Network issue, raise a more readable message
+ throw new Error(`Failed to connect to /plu/ral API: ${networkError.toString()}`)
+ }
+ if (!res.ok) throw new Error(`/plu/ral API returned an error: ${await res.text()}`)
+ /** @type {any} */
+ const root = await res.json()
+ if (!root.member) throw new Error(`/plu/ral API didn't return member data: ${JSON.stringify(root)}`)
+ return root
+}
+
+/**
+ * Using the same sim names and fake user IDs for /plu/ral members, since unlike PluralKit they don't have a short and a long ID.
+ * @param {Ty.PluRalWebhookMessage} pluRalMessage
+ */
+function getSimName(pluRalMessage) {
+ return `_pl_${pluRalMessage.member_id}`
+}
+
+/**
+ * A sim is an account that is being simulated by the bridge to copy events from the other side.
+ * @param {Ty.PluRalWebhookMessage} pluRalMessage
+ * @returns mxid
+ */
+async function createSim(pluRalMessage) {
+ // Choose sim name
+ const simName = getSimName(pluRalMessage)
+ const localpart = reg.ooye.namespace_prefix + simName
+ const mxid = `@${localpart}:${reg.ooye.server_name}`
+
+ // Save chosen name in the database forever
+ db.prepare("INSERT INTO sim (user_id, username, sim_name, mxid) VALUES (?, ?, ?, ?)").run(simName, simName, simName, mxid)
+
+ // Register matrix user with that name
+ try {
+ await api.register(localpart)
+ } catch (e) {
+ // If user creation fails, manually undo the database change. Still isn't perfect, but should help.
+ // (I would prefer a transaction, but it's not safe to leave transactions open across event loop ticks.)
+ db.prepare("DELETE FROM sim WHERE user_id = ?").run(simName)
+ throw e
+ }
+ return mxid
+}
+
+/**
+ * Ensure a sim is registered for the user.
+ * If there is already a sim, use that one. If there isn't one yet, register a new sim.
+ * @param {Ty.PluRalWebhookMessage} pluRalMessage
+ * @returns {Promise} mxid
+ */
+async function ensureSim(pluRalMessage) {
+ let mxid = null
+ const existing = select("sim", "mxid", {user_id: getSimName(pluRalMessage)}).pluck().get()
+ if (existing) {
+ mxid = existing
+ } else {
+ mxid = await createSim(pluRalMessage)
+ }
+ return mxid
+}
+
+/**
+ * Ensure a sim is registered for the user and is joined to the room.
+ * @param {Ty.PluRalWebhookMessage} pluRalMessage
+ * @param {string} roomID
+ * @returns {Promise} mxid
+ */
+async function ensureSimJoined(pluRalMessage, roomID) {
+ // Ensure room ID is really an ID, not an alias
+ assert.ok(roomID[0] === "!")
+
+ // Ensure user
+ const mxid = await ensureSim(pluRalMessage)
+
+ // Ensure joined
+ const existing = select("sim_member", "mxid", {room_id: roomID, mxid}).pluck().get()
+ if (!existing) {
+ await api.inviteToRoom(roomID, mxid)
+ await api.joinRoom(roomID, mxid)
+ db.prepare("INSERT OR IGNORE INTO sim_member (room_id, mxid) VALUES (?, ?)").run(roomID, mxid)
+ }
+ return mxid
+}
+
+/**
+ * Generate profile data based on webhook displayname and configured avatar.
+ * @param {Ty.PluRalWebhookMessage} pluRalMessage
+ * @param {Ty.WebhookAuthor} author
+ */
+async function memberToStateContent(pluRalMessage, author) {
+ // We prefer to use the member's avatar URL data since the image upload can be cached across channels,
+ // unlike the userAvatar URL which is unique per channel, due to the webhook ID being in the URL.
+ const avatar = pluRalMessage.member.avatar_url || file.userAvatar(author)
+
+ const content = {
+ displayname: author.username,
+ membership: "join",
+ "moe.cadence.ooye.plu_ral_member": pluRalMessage.member
+ }
+ if (avatar) content.avatar_url = await file.uploadDiscordFileToMxc(avatar)
+
+ return content
+}
+
+/**
+ * Sync profile data for a sim user. This function follows the following process:
+ * 1. Look up data about proxy user from API
+ * 2. If this fails, try to use previously cached data (won't sync)
+ * 3. Create and join the sim to the room if needed
+ * 4. Make an object of what the new room member state content would be, including uploading the profile picture if it hasn't been done before
+ * 5. Compare against the previously known state content, which is helpfully stored in the database
+ * 6. If the state content has changed, send it to Matrix and update it in the database for next time
+ * @param {string} channelID to call API with
+ * @param {string} messageID to call API with
+ * @param {Ty.WebhookAuthor} author for profile data
+ * @param {string} roomID room to join member to
+ * @param {boolean} shouldActuallySync whether to actually sync updated user data or just ensure it's joined
+ * @returns {Promise} mxid of the updated sim
+ */
+async function syncUser(channelID, messageID, author, roomID, shouldActuallySync) {
+ try {
+ // API lookup
+ var pluRalMessage = await fetchMessage(channelID, messageID)
+ const simName = getSimName(pluRalMessage)
+ db.prepare("REPLACE INTO sim_proxy (user_id, proxy_owner_id, displayname, proxy_app) VALUES (?, ?, ?, 1)").run(simName, pluRalMessage.author_id, author.username)
+ } catch (e) {
+ // Fall back to offline cache
+ const senderMxid = from("sim_proxy").join("sim", "user_id").join("sim_member", "mxid").where({displayname: author.username, room_id: roomID, proxy_app: 1}).pluck("mxid").get()
+ if (!senderMxid) throw e
+ return senderMxid
+ }
+
+ // Create and join the sim to the room if needed
+ const mxid = await ensureSimJoined(pluRalMessage, roomID)
+
+ if (shouldActuallySync) {
+ // Build current profile data and sync if the hash has changed
+ const content = await memberToStateContent(pluRalMessage, author)
+ await registerUser._sendSyncUser(roomID, mxid, content, null)
+ }
+
+ return mxid
+}
+
+module.exports.syncUser = syncUser
diff --git a/src/d2m/actions/send-message.js b/src/d2m/actions/send-message.js
index 5b3b4f3..41ef70d 100644
--- a/src/d2m/actions/send-message.js
+++ b/src/d2m/actions/send-message.js
@@ -4,17 +4,16 @@ const assert = require("assert").strict
const DiscordTypes = require("discord-api-types/v10")
const passthrough = require("../../passthrough")
-const { discord, sync, db, select, from} = passthrough
+const {discord, sync, db, select, from} = passthrough
+const {reg} = require("../../matrix/read-registration")
/** @type {import("../converters/message-to-event")} */
const messageToEvent = sync.require("../converters/message-to-event")
/** @type {import("../../matrix/api")} */
const api = sync.require("../../matrix/api")
/** @type {import("./register-user")} */
const registerUser = sync.require("./register-user")
-/** @type {import("./register-pk-user")} */
-const registerPkUser = sync.require("./register-pk-user")
-/** @type {import("./register-webhook-user")} */
-const registerWebhookUser = sync.require("./register-webhook-user")
+/** @type {import("./speedbump")} */
+const speedbump = sync.require("./speedbump")
/** @type {import("../actions/create-room")} */
const createRoom = sync.require("../actions/create-room")
/** @type {import("../actions/poll-end")} */
@@ -28,24 +27,15 @@ const channelWebhook = sync.require("../../m2d/actions/channel-webhook")
* @param {DiscordTypes.GatewayMessageCreateDispatchData} message
* @param {DiscordTypes.APIGuildChannel} channel
* @param {DiscordTypes.APIGuild} guild
- * @param {{speedbump_id: string, speedbump_webhook_id: string} | null} row data about the webhook which is proxying messages in this channel
*/
-async function sendMessage(message, channel, guild, row) {
+async function sendMessage(message, channel, guild) {
const roomID = await createRoom.ensureRoom(message.channel_id)
const historicalRoomIndex = select("historical_channel_room", "historical_room_index", {room_id: roomID}).pluck().get()
assert(historicalRoomIndex)
let senderMxid = null
if (dUtils.isWebhookMessage(message)) {
- const useWebhookProfile = select("guild_space", "webhook_profile", {guild_id: guild.id}).pluck().get() ?? 0
- if (row && row.speedbump_webhook_id === message.webhook_id) {
- // Handle the PluralKit public instance
- if (row.speedbump_id === "466378653216014359") {
- senderMxid = await registerPkUser.syncUser(message.id, message.author, roomID, true)
- }
- } else if (useWebhookProfile) {
- senderMxid = await registerWebhookUser.syncUser(message.author, roomID, true)
- }
+ senderMxid = await speedbump.getWebhookSenderId(message, guild.id, roomID)
} else {
// not a webhook
if (message.author.id === discord.application.id) {
diff --git a/src/d2m/actions/speedbump.js b/src/d2m/actions/speedbump.js
index 42e3a35..88cea07 100644
--- a/src/d2m/actions/speedbump.js
+++ b/src/d2m/actions/speedbump.js
@@ -1,13 +1,23 @@
// @ts-check
+const DiscordTypes = require("discord-api-types/v10")
+
const passthrough = require("../../passthrough")
-const {discord, select, db} = passthrough
+const {discord, from, select, db, sync} = passthrough
+
+const {reg} = require("../../matrix/read-registration")
+/** @type {import("./register-pk-user")} */
+const registerPkUser = sync.require("./register-pk-user")
+/** @type {import("./register-plu-ral-user")} */
+const registerPluRalUser = sync.require("./register-plu-ral-user")
+/** @type {import("./register-webhook-user")} */
+const registerWebhookUser = sync.require("./register-webhook-user")
const DEBUG_SPEEDBUMP = false
-function debugSpeedbump(message) {
+function debugSpeedbump(...args) {
if (DEBUG_SPEEDBUMP) {
- console.log(message)
+ console.log(...args)
}
}
@@ -16,7 +26,8 @@ const SPEEDBUMP_UPDATE_FREQUENCY = 2 * 60 * 60 // 2 hours
/** @type {Set} */
const KNOWN_BOTS = new Set([
- "466378653216014359" // PluralKit
+ "466378653216014359", // PluralKit
+ "1291501048493768784", // /plu/ral
])
/**
@@ -28,61 +39,81 @@ async function updateCache(channelID, lastChecked) {
const now = Math.floor(Date.now() / 1000)
if (lastChecked && now - lastChecked < SPEEDBUMP_UPDATE_FREQUENCY) return
const webhooks = await discord.snow.webhook.getChannelWebhooks(channelID)
- const found = webhooks.find(b => KNOWN_BOTS.has(b.application_id))
- const foundApplication = found?.application_id
- const foundWebhook = found?.id
- db.prepare("UPDATE channel_room SET speedbump_id = ?, speedbump_webhook_id = ?, speedbump_checked = ? WHERE channel_id = ?").run(foundApplication, foundWebhook, now, channelID)
+ const found = webhooks.filter(b => KNOWN_BOTS.has(b.application_id))
+ db.transaction(() => {
+ db.prepare("DELETE FROM channel_speedbump WHERE channel_id = ?").run(channelID)
+ for (const webhook of found) {
+ db.prepare("INSERT INTO channel_speedbump (channel_id, speedbump_webhook_id, speedbump_user_id) VALUES (?, ?, ?)").run(channelID, webhook.id, webhook.application_id)
+ }
+ db.prepare("UPDATE channel_room SET speedbump_checked = ? WHERE channel_id = ?").run(now, channelID)
+ })()
}
-/** @type {Map} messageID -> number of gateway events currently bumping */
+/**
+ * @typedef BumpingEntry
+ * @prop {number} number number of gateway events currently bumping for this message ID
+ * @prop {boolean} hasCreate whether there was a message create within the events currently bumping
+ */
+
+/** @type {Map} messageID -> BumpingEntry */
const bumping = new Map()
/**
* Slow down a message. After it passes the speedbump, return whether it's okay or if it's been deleted.
+ * @param {boolean} isCreate
* @param {string} messageID
* @returns whether it was deleted
*/
-async function doSpeedbump(messageID) {
- let value = (bumping.get(messageID) ?? 0) + 1
- bumping.set(messageID, value)
- debugSpeedbump(`[speedbump] WAIT ${messageID}++ = ${value}`)
+async function doSpeedbump(isCreate, messageID) {
+ const entry = bumping.get(messageID) ?? (() => {
+ const entry = {number: 0, hasCreate: false}
+ bumping.set(messageID, entry)
+ return entry
+ })()
+
+ entry.number++
+ entry.hasCreate ||= isCreate
+
+ debugSpeedbump(`[speedbump] WAIT ${messageID}++ =`, entry)
await new Promise(resolve => setTimeout(resolve, SPEEDBUMP_SPEED))
if (!bumping.has(messageID)) {
debugSpeedbump(`[speedbump] DELETED ${messageID}`)
- return true
+ return {skip: true, hasCreate: null}
}
- value = (bumping.get(messageID) ?? 0) - 1
- if (value <= 0) {
- debugSpeedbump(`[speedbump] OK ${messageID}-- = ${value}`)
+
+ if (--entry.number <= 0) {
+ debugSpeedbump(`[speedbump] OK ${messageID}-- =`, entry)
bumping.delete(messageID)
- return false
+ return {skip: false, hasCreate: entry.hasCreate}
} else {
- debugSpeedbump(`[speedbump] MULTI ${messageID}-- = ${value}`)
- bumping.set(messageID, value)
- return true
+ debugSpeedbump(`[speedbump] MULTI ${messageID}-- =`, entry)
+ return {skip: true, hasCreate: null}
}
}
+function getSpeedbumpRows(channelID) {
+ return from("channel_room").join("channel_speedbump", "channel_id").select("thread_parent", "speedbump_user_id", "speedbump_webhook_id").where({channel_id: channelID}).all()
+}
+
/**
* 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
+ * @param {boolean} isCreate
+ * @param {{id: string, channel_id: string, author: {id: string}, backfill?: boolean}} message uses the ID to identify, and the userID to only slow down the message when the user has used PK before
+ * @returns whether to skip this message, and whether the message should be created as a creation
*/
-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
- }
- const affected = await doSpeedbump(messageID)
- return {affected, row} // maybe affected, and there is a speedbump
+async function maybeDoSpeedbump(isCreate, message) {
+ let rows = getSpeedbumpRows(message.channel_id)
+ if (rows[0]?.thread_parent) rows = getSpeedbumpRows(rows[0].thread_parent) // webhooks belong to the channel, not the thread
+ if (!rows.length) return {skip: false} // channel not affected, no speedbump
+
+ if (message.backfill) return {skip: false} // don't slow messages during backfill
+ if (rows.some(r => r.speedbump_webhook_id === message.author.id)) return {skip: false} // shortcut
+ const userHasProxy = select("sim_proxy", "user_id", {proxy_owner_id: message.author.id}).pluck().get()
+ if (!userHasProxy) return {skip: false} // user has not used PK before, no speedbump
+ const {skip, hasCreate} = await doSpeedbump(isCreate, message.id)
+ return {skip, hasCreate} // maybe affected, and there is a speedbump
}
/**
@@ -92,7 +123,25 @@ function onMessageDelete(messageID) {
bumping.delete(messageID)
}
+/**
+ * @param {DiscordTypes.APIMessage} message
+ * @param {string} guildID
+ * @param {string} roomID
+ */
+async function getWebhookSenderId(message, guildID, roomID) {
+ const speedbumpUserID = select("channel_speedbump", "speedbump_user_id", {channel_id: message.channel_id, speedbump_webhook_id: message.webhook_id}).pluck().get()
+ const useWebhookProfile = select("guild_space", "webhook_profile", {guild_id: guildID}).pluck().get() ?? 0
+ if (speedbumpUserID === "466378653216014359") { // PluralKit public instance
+ return await registerPkUser.syncUser(message.id, message.author, roomID, true)
+ } else if (speedbumpUserID === "1291501048493768784" && reg.ooye.plu_ral_api_key) { // /plu/ral public instance
+ return await registerPluRalUser.syncUser(message.channel_id, message.id, message.author, roomID, true)
+ } else if (useWebhookProfile) {
+ return await registerWebhookUser.syncUser(message.author, roomID, true)
+ }
+ return null
+}
+
module.exports.updateCache = updateCache
-module.exports.doSpeedbump = doSpeedbump
module.exports.maybeDoSpeedbump = maybeDoSpeedbump
module.exports.onMessageDelete = onMessageDelete
+module.exports.getWebhookSenderId = getWebhookSenderId
diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js
index c0c33d9..a49453a 100644
--- a/src/d2m/event-dispatcher.js
+++ b/src/d2m/event-dispatcher.js
@@ -288,7 +288,6 @@ module.exports = {
if (!guildID) return // channel must have been a DM channel or something
const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get()
if (!roomID) return // channel wasn't being bridged in the first place
- // @ts-ignore
await createRoom.unbridgeChannel(channel, guildID)
},
@@ -313,11 +312,10 @@ 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)
- if (affected) return
+ const {skip} = await speedbump.maybeDoSpeedbump(true, message)
+ if (skip) return
- // @ts-ignore
- await sendMessage.sendMessage(message, channel, guild, row)
+ await sendMessage.sendMessage(message, channel, guild)
retrigger.finishedBridging(message.id)
},
@@ -335,22 +333,27 @@ 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)
- if (affected) return
-
- // Check that the sending-to room exists, and deal with Eventual Consistency(TM)
- if (!await retrigger.waitForMessage(data.id)) return
+ const {skip, hasCreate} = await speedbump.maybeDoSpeedbump(false, data)
+ if (skip) return
/** @type {DiscordTypes.GatewayMessageCreateDispatchData} */
- // @ts-ignore
const message = data
const channel = client.channels.get(message.channel_id)
if (!channel || !("guild_id" in channel) || !channel.guild_id) return // Nothing we can do in direct messages.
const guild = client.guilds.get(channel.guild_id)
assert(guild)
- // @ts-ignore
- await retrigger.pauseChanges(message.id, editMessage.editMessage(message, guild, row))
+ if (!hasCreate) {
+ // Standard path for most message updates
+ // Check that the target message already exists, and deal with Eventual Consistency(TM)
+ if (!await retrigger.waitForMessage(data.id)) return
+ await retrigger.pauseChanges(message.id, editMessage.editMessage(message, guild))
+ }
+ else {
+ // Path for edit packets that were speedbumped into the latest copy of a message that needs to be created
+ // Just pretend to be MESSAGE_CREATE
+ await sendMessage.sendMessage(message, channel, guild)
+ }
},
/**
diff --git a/src/db/migrations/0039-refactor-speedbump.sql b/src/db/migrations/0039-refactor-speedbump.sql
new file mode 100644
index 0000000..de739dc
--- /dev/null
+++ b/src/db/migrations/0039-refactor-speedbump.sql
@@ -0,0 +1,19 @@
+BEGIN TRANSACTION;
+
+CREATE TABLE "channel_speedbump" (
+ "channel_id" TEXT NOT NULL,
+ "speedbump_webhook_id" TEXT NOT NULL,
+ "speedbump_user_id" TEXT NOT NULL,
+ PRIMARY KEY("channel_id","speedbump_webhook_id"),
+ FOREIGN KEY("channel_id") REFERENCES "channel_room"("channel_id")
+) WITHOUT ROWID;
+
+INSERT INTO channel_speedbump (channel_id, speedbump_webhook_id, speedbump_user_id)
+ SELECT channel_id, speedbump_webhook_id, speedbump_id FROM channel_room WHERE speedbump_id IS NOT NULL AND speedbump_webhook_id IS NOT NULL;
+
+ALTER TABLE channel_room DROP COLUMN speedbump_id;
+ALTER TABLE channel_room DROP COLUMN speedbump_webhook_id;
+
+ALTER TABLE sim_proxy ADD COLUMN proxy_app INTEGER DEFAULT 0;
+
+COMMIT;
diff --git a/src/db/orm-defs.d.ts b/src/db/orm-defs.d.ts
index d95bfc3..14f320a 100644
--- a/src/db/orm-defs.d.ts
+++ b/src/db/orm-defs.d.ts
@@ -18,13 +18,17 @@ export type Models = {
thread_parent: string | null
custom_avatar: string | null
last_bridged_pin_timestamp: number | null
- speedbump_id: string | null
- speedbump_webhook_id: string | null
speedbump_checked: number | null
guild_id: string | null
custom_topic: number
}
+ channel_speedbump: {
+ channel_id: string
+ speedbump_webhook_id: string
+ speedbump_user_id: string
+ }
+
direct: {
mxid: string
room_id: string
@@ -44,6 +48,7 @@ export type Models = {
event_subtype: string | null
part: number
reaction_part: number
+ /** 0 = Matrix, 1 = Discord */
source: number
}
@@ -137,6 +142,8 @@ export type Models = {
user_id: string
proxy_owner_id: string
displayname: string
+ /** 0 = PluralKit, 1 = /plu/ral */
+ proxy_app: number
}
webhook: {
diff --git a/src/matrix/mreq.js b/src/matrix/mreq.js
index bf238f2..b6bc5b9 100644
--- a/src/matrix/mreq.js
+++ b/src/matrix/mreq.js
@@ -9,8 +9,10 @@ const {reg} = require("./read-registration.js")
const baseUrl = `${reg.ooye.server_origin}/_matrix`
class MatrixServerError extends Error {
- /** @param {number} httpStatus} */
- constructor(data, httpStatus, opts) {
+ /**
+ * @param {number} httpStatus}
+ */
+ constructor(data, httpStatus, opts = {}) {
super(data.error || data.errcode)
this.data = data
/** @type {number} */
diff --git a/src/types.d.ts b/src/types.d.ts
index 856ab1c..f65ce59 100644
--- a/src/types.d.ts
+++ b/src/types.d.ts
@@ -36,6 +36,7 @@ export type AppServiceRegistrationConfig = {
web_password: string
time_zone?: string
receive_presences: boolean
+ plu_ral_api_key?: string
}
old_bridge?: {
as_token: string
@@ -124,6 +125,28 @@ export type PkMessage = {
sender: string
}
+export type PluRalWebhookMessage = {
+ original_id: string | null
+ proxy_id: string
+ author_id: string
+ channel_id: string
+ member_id: string
+ reason: string
+ webhook_id: string
+ member: PluRalMember
+}
+
+export type PluRalMember = {
+ id: string
+ name: string
+ pronouns: string
+ bio: string
+ birthday: string
+ color: number | null
+ avatar_url: string
+ private: boolean
+}
+
export namespace Event {
export type Outer = {
type: string
diff --git a/src/web/routes/link.test.js b/src/web/routes/link.test.js
index 0182093..32f89bf 100644
--- a/src/web/routes/link.test.js
+++ b/src/web/routes/link.test.js
@@ -73,7 +73,7 @@ test("web link space: check that OOYE is joined", async t => {
api: {
async joinRoom(roomID) {
called++
- throw new MatrixServerError({errcode: "M_FORBIDDEN", error: "not allowed to join I guess"})
+ throw new MatrixServerError({errcode: "M_FORBIDDEN", error: "not allowed to join I guess"}, 400)
}
}
}))
@@ -368,7 +368,7 @@ test("web link room: check that bridge can join room (notices lack of via and as
api: {
async joinRoom(roomID) {
called++
- throw new MatrixServerError({errcode: "M_FORBIDDEN", error: "not allowed to join I guess"})
+ throw new MatrixServerError({errcode: "M_FORBIDDEN", error: "not allowed to join I guess"}, 400)
},
async *generateFullHierarchy(spaceID) {
called++
@@ -402,7 +402,7 @@ test("web link room: check that bridge can join room (uses via for join attempt)
async joinRoom(roomID, _, via) {
called++
t.deepEqual(via, ["cadence.moe", "hashi.re"])
- throw new MatrixServerError({errcode: "M_FORBIDDEN", error: "not allowed to join I guess"})
+ throw new MatrixServerError({errcode: "M_FORBIDDEN", error: "not allowed to join I guess"}, 400)
},
async *generateFullHierarchy(spaceID) {
called++
@@ -710,7 +710,7 @@ test("web unlink room: checks that the channel is bridged", async t => {
}))
t.equal(error.data, "Channel ID 665310973967597573 is not currently bridged")
- db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar, last_bridged_pin_timestamp, speedbump_id, speedbump_checked, speedbump_webhook_id, guild_id, custom_topic) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(row.channel_id, row.room_id, row.name, row.nick, row.thread_parent, row.custom_avatar, row.last_bridged_pin_timestamp, row.speedbump_id, row.speedbump_checked, row.speedbump_webhook_id, row.guild_id, row.custom_topic)
+ db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar, last_bridged_pin_timestamp, speedbump_checked, guild_id, custom_topic) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(row.channel_id, row.room_id, row.name, row.nick, row.thread_parent, row.custom_avatar, row.last_bridged_pin_timestamp, row.speedbump_checked, row.guild_id, row.custom_topic)
const new_row = db.prepare("SELECT * FROM channel_room WHERE channel_id = '665310973967597573'").get()
t.deepEqual(row, new_row)
})