Compare commits
2 commits
b765f564b3
...
a3ce911218
| Author | SHA1 | Date | |
|---|---|---|---|
| a3ce911218 | |||
| 544c5206c7 |
13 changed files with 376 additions and 90 deletions
|
|
@ -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!
|
||||
|
||||
----
|
||||
<br><br><br><br><br>
|
||||
# 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
|
||||
|
||||
|
|
|
|||
17
scripts/plu-ral-api.js
Normal file
17
scripts/plu-ral-api.js
Normal file
|
|
@ -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.")
|
||||
})()
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
170
src/d2m/actions/register-plu-ral-user.js
Normal file
170
src/d2m/actions/register-plu-ral-user.js
Normal file
|
|
@ -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<Ty.PluRalWebhookMessage>} */
|
||||
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<string>} 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<string>} 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<string>} 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
|
||||
|
|
@ -5,16 +5,15 @@ const DiscordTypes = require("discord-api-types/v10")
|
|||
|
||||
const passthrough = require("../../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) {
|
||||
|
|
|
|||
|
|
@ -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<any>} */
|
||||
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<string, number>} 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<string, BumpingEntry>} 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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
19
src/db/migrations/0039-refactor-speedbump.sql
Normal file
19
src/db/migrations/0039-refactor-speedbump.sql
Normal file
|
|
@ -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;
|
||||
11
src/db/orm-defs.d.ts
vendored
11
src/db/orm-defs.d.ts
vendored
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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} */
|
||||
|
|
|
|||
23
src/types.d.ts
vendored
23
src/types.d.ts
vendored
|
|
@ -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<T> = {
|
||||
type: string
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue