forked from cadence/out-of-your-element
Compare commits
7 commits
ellie-fix-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f7609b2040 | |||
| b576869764 | |||
| 47dc0504ff | |||
| fbade33ff0 | |||
| e2ab9fa9bf | |||
| 18b6efdd18 | |||
| 313efb29d8 |
18 changed files with 464 additions and 126 deletions
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "out-of-your-element",
|
"name": "out-of-your-element",
|
||||||
"version": "3.5.1",
|
"version": "3.6.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "out-of-your-element",
|
"name": "out-of-your-element",
|
||||||
"version": "3.5.1",
|
"version": "3.6.0",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chriscdn/promise-semaphore": "^3.0.1",
|
"@chriscdn/promise-semaphore": "^3.0.1",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "out-of-your-element",
|
"name": "out-of-your-element",
|
||||||
"version": "3.5.1",
|
"version": "3.6.0",
|
||||||
"description": "A bridge between Matrix and Discord",
|
"description": "A bridge between Matrix and Discord",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,15 @@
|
||||||
|
|
||||||
const {EventEmitter} = require("events")
|
const {EventEmitter} = require("events")
|
||||||
const passthrough = require("../../passthrough")
|
const passthrough = require("../../passthrough")
|
||||||
const {select} = passthrough
|
const {select, sync, from} = passthrough
|
||||||
|
/** @type {import("../../matrix/utils")} */
|
||||||
|
const utils = sync.require("../../matrix/utils")
|
||||||
|
|
||||||
|
/*
|
||||||
|
Due to Eventual Consistency(TM) an update/delete may arrive before the original message arrives
|
||||||
|
(or before the it has finished being bridged to an event).
|
||||||
|
In this case, wait until the original message has finished bridging, then retrigger the passed function.
|
||||||
|
*/
|
||||||
|
|
||||||
const DEBUG_RETRIGGER = false
|
const DEBUG_RETRIGGER = false
|
||||||
|
|
||||||
|
|
@ -12,81 +20,140 @@ function debugRetrigger(message) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const paused = new Set()
|
const storage = new class {
|
||||||
const emitter = new EventEmitter()
|
/** @private @type {Set<string>} */
|
||||||
|
paused = new Set()
|
||||||
|
/** @private @type {Map<string, ((found: Boolean) => any)[]>} id -> list of resolvers */
|
||||||
|
resolves = new Map()
|
||||||
|
/** @private @type {Map<string, ReturnType<setTimeout>>} id -> timer */
|
||||||
|
timers = new Map()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Due to Eventual Consistency(TM) an update/delete may arrive before the original message arrives
|
* The purpose of storage is to store `resolve` and call it at a later time.
|
||||||
* (or before the it has finished being bridged to an event).
|
* @param {string} id
|
||||||
* In this case, wait until the original message has finished bridging, then retrigger the passed function.
|
* @param {(found: Boolean) => any} resolve
|
||||||
* @template {(...args: any[]) => any} T
|
*/
|
||||||
* @param {string} inputID
|
store(id, resolve) {
|
||||||
* @param {T} fn
|
debugRetrigger(`[retrigger] STORE id = ${id}`)
|
||||||
* @param {Parameters<T>} rest
|
this.resolves.set(id, (this.resolves.get(id) || []).concat(resolve)) // add to list in map value
|
||||||
* @returns {boolean} false if the event was found and the function will be ignored, true if the event was not found and the function will be retriggered
|
if (!this.timers.has(id)) {
|
||||||
*/
|
debugRetrigger(`[retrigger] SET TIMER id = ${id}`)
|
||||||
function eventNotFoundThenRetrigger(inputID, fn, ...rest) {
|
this.timers.set(id, setTimeout(() => this.resolve(id, false), 60 * 1000).unref()) // 1 minute
|
||||||
if (!paused.has(inputID)) {
|
|
||||||
if (inputID.match(/^[0-9]+$/)) {
|
|
||||||
const eventID = select("event_message", "event_id", {message_id: inputID}).pluck().get()
|
|
||||||
if (eventID) {
|
|
||||||
debugRetrigger(`[retrigger] OK mid <-> eid = ${inputID} <-> ${eventID}`)
|
|
||||||
return false // event was found so don't retrigger
|
|
||||||
}
|
|
||||||
} else if (inputID.match(/^\$/)) {
|
|
||||||
const messageID = select("event_message", "message_id", {event_id: inputID}).pluck().get()
|
|
||||||
if (messageID) {
|
|
||||||
debugRetrigger(`[retrigger] OK eid <-> mid = ${inputID} <-> ${messageID}`)
|
|
||||||
return false // message was found so don't retrigger
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debugRetrigger(`[retrigger] WAIT id = ${inputID}`)
|
/** @param {string} id */
|
||||||
emitter.once(inputID, () => {
|
isNotPaused(id) {
|
||||||
debugRetrigger(`[retrigger] TRIGGER id = ${inputID}`)
|
return !storage.paused.has(id)
|
||||||
fn(...rest)
|
}
|
||||||
})
|
|
||||||
// if the event never arrives, don't trigger the callback, just clean up
|
/** @param {string} id */
|
||||||
setTimeout(() => {
|
pause(id) {
|
||||||
if (emitter.listeners(inputID).length) {
|
debugRetrigger(`[retrigger] PAUSE id = ${id}`)
|
||||||
debugRetrigger(`[retrigger] EXPIRE id = ${inputID}`)
|
this.paused.add(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go through `resolves` storage and resolve them all. (Also resets timer/paused.)
|
||||||
|
* @param {string} id
|
||||||
|
* @param {boolean} value
|
||||||
|
*/
|
||||||
|
resolve(id, value) {
|
||||||
|
if (this.paused.has(id)) {
|
||||||
|
debugRetrigger(`[retrigger] RESUME id = ${id}`)
|
||||||
|
this.paused.delete(id)
|
||||||
}
|
}
|
||||||
emitter.removeAllListeners(inputID)
|
|
||||||
}, 60 * 1000) // 1 minute
|
if (this.resolves.has(id)) {
|
||||||
return true // event was not found, then retrigger
|
debugRetrigger(`[retrigger] RESOLVE ${value} id = ${id}`)
|
||||||
|
const fns = this.resolves.get(id) || []
|
||||||
|
this.resolves.delete(id)
|
||||||
|
for (const fn of fns) {
|
||||||
|
fn(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.timers.has(id)) {
|
||||||
|
clearTimeout(this.timers.get(id))
|
||||||
|
this.timers.delete(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} id
|
||||||
|
* @param {(found: Boolean) => any} resolve
|
||||||
|
* @param {boolean} existsInDatabase
|
||||||
|
*/
|
||||||
|
function waitFor(id, resolve, existsInDatabase) {
|
||||||
|
if (existsInDatabase && storage.isNotPaused(id)) { // if event already exists and isn't paused then resolve immediately
|
||||||
|
debugRetrigger(`[retrigger] EXISTS id = ${id}`)
|
||||||
|
return resolve(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// doesn't exist. wait for it to exist. storage will resolve true if it exists or false if it timed out
|
||||||
|
return storage.store(id, resolve)
|
||||||
|
}
|
||||||
|
|
||||||
|
const GET_EVENT_PREPARED = from("event_message").select("event_id").and("WHERE event_id = ?").prepare().raw()
|
||||||
|
/**
|
||||||
|
* @param {string} eventID
|
||||||
|
* @returns {Promise<boolean>} if true then the message did not arrive
|
||||||
|
*/
|
||||||
|
function waitForEvent(eventID) {
|
||||||
|
const {promise, resolve} = Promise.withResolvers()
|
||||||
|
waitFor(eventID, resolve, !!GET_EVENT_PREPARED.get(eventID))
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
const GET_MESSAGE_PREPARED = from("event_message").select("message_id").and("WHERE message_id = ?").prepare().raw()
|
||||||
|
/**
|
||||||
|
* @param {string} messageID
|
||||||
|
* @returns {Promise<boolean>} if true then the message did not arrive
|
||||||
|
*/
|
||||||
|
function waitForMessage(messageID) {
|
||||||
|
const {promise, resolve} = Promise.withResolvers()
|
||||||
|
waitFor(messageID, resolve, !!GET_MESSAGE_PREPARED.get(messageID))
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
const GET_REACTION_EVENT_PREPARED = from("reaction").select("hashed_event_id").and("WHERE hashed_event_id = ?").prepare().raw()
|
||||||
|
/**
|
||||||
|
* @param {string} eventID
|
||||||
|
* @returns {Promise<boolean>} if true then the message did not arrive
|
||||||
|
*/
|
||||||
|
function waitForReactionEvent(eventID) {
|
||||||
|
const {promise, resolve} = Promise.withResolvers()
|
||||||
|
waitFor(eventID, resolve, !!GET_REACTION_EVENT_PREPARED.get(utils.getEventIDHash(eventID)))
|
||||||
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Anything calling retrigger during the callback will be paused and retriggered after the callback resolves.
|
* Anything calling retrigger during the callback will be paused and retriggered after the callback resolves.
|
||||||
* @template T
|
* @template T
|
||||||
* @param {string} messageID
|
* @param {string} id
|
||||||
* @param {Promise<T>} promise
|
* @param {Promise<T>} promise
|
||||||
* @returns {Promise<T>}
|
* @returns {Promise<T>}
|
||||||
*/
|
*/
|
||||||
async function pauseChanges(messageID, promise) {
|
async function pauseChanges(id, promise) {
|
||||||
try {
|
try {
|
||||||
debugRetrigger(`[retrigger] PAUSE id = ${messageID}`)
|
storage.pause(id)
|
||||||
paused.add(messageID)
|
|
||||||
return await promise
|
return await promise
|
||||||
} finally {
|
} finally {
|
||||||
debugRetrigger(`[retrigger] RESUME id = ${messageID}`)
|
finishedBridging(id)
|
||||||
paused.delete(messageID)
|
|
||||||
messageFinishedBridging(messageID)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggers any pending operations that were waiting on the corresponding event ID.
|
* Triggers any pending operations that were waiting on the corresponding event ID.
|
||||||
* @param {string} messageID
|
* @param {string} id
|
||||||
*/
|
*/
|
||||||
function messageFinishedBridging(messageID) {
|
function finishedBridging(id) {
|
||||||
if (emitter.listeners(messageID).length) {
|
storage.resolve(id, true)
|
||||||
debugRetrigger(`[retrigger] EMIT id = ${messageID}`)
|
|
||||||
}
|
|
||||||
emitter.emit(messageID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.eventNotFoundThenRetrigger = eventNotFoundThenRetrigger
|
module.exports.waitForMessage = waitForMessage
|
||||||
module.exports.messageFinishedBridging = messageFinishedBridging
|
module.exports.waitForEvent = waitForEvent
|
||||||
|
module.exports.waitForReactionEvent = waitForReactionEvent
|
||||||
module.exports.pauseChanges = pauseChanges
|
module.exports.pauseChanges = pauseChanges
|
||||||
|
module.exports.finishedBridging = finishedBridging
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
const DiscordTypes = require("discord-api-types/v10")
|
|
||||||
const passthrough = require("../../passthrough")
|
const passthrough = require("../../passthrough")
|
||||||
const {discord, select, db} = passthrough
|
const {discord, select, db} = passthrough
|
||||||
|
|
||||||
|
|
@ -70,12 +69,17 @@ 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.
|
* 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} channelID
|
||||||
* @param {string} messageID
|
* @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
|
* @returns whether it was deleted, and data about the channel's (not thread's) speedbump
|
||||||
*/
|
*/
|
||||||
async function maybeDoSpeedbump(channelID, messageID) {
|
async function maybeDoSpeedbump(channelID, messageID, userID) {
|
||||||
let row = select("channel_room", ["thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: channelID}).get()
|
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", ["thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: row.thread_parent}).get() // webhooks belong to the channel, not the thread
|
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} // not affected, no speedbump
|
if (!row?.speedbump_webhook_id) return {affected: false, row: null} // channel not affected, no speedbump
|
||||||
|
if (userID) {
|
||||||
|
const userHasProxy = select("sim_proxy", "user_id", {proxy_owner_id: userID}).pluck().get()
|
||||||
|
if (!userHasProxy) return {affected: false, row: null} // user has not used PK before, no speedbump
|
||||||
|
}
|
||||||
const affected = await doSpeedbump(messageID)
|
const affected = await doSpeedbump(messageID)
|
||||||
return {affected, row} // maybe affected, and there is a speedbump
|
return {affected, row} // maybe affected, and there is a speedbump
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -265,8 +265,9 @@ function getFormattedInteraction(interaction, isThinkingInteraction) {
|
||||||
* @param {any} newEvents merge into events
|
* @param {any} newEvents merge into events
|
||||||
* @param {any} events will be modified
|
* @param {any} events will be modified
|
||||||
* @param {boolean} forceSameMsgtype whether m.text may only be combined with m.text, etc
|
* @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) {
|
function mergeTextEvents(newEvents, events, forceSameMsgtype, forceMerge = false) {
|
||||||
let prev = events.at(-1)
|
let prev = events.at(-1)
|
||||||
for (const ne of newEvents) {
|
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)
|
const isAllText = prev?.body && prev?.formatted_body && ["m.text", "m.notice"].includes(ne.msgtype) && ["m.text", "m.notice"].includes(prev?.msgtype)
|
||||||
|
|
@ -278,6 +279,8 @@ function mergeTextEvents(newEvents, events, forceSameMsgtype) {
|
||||||
rep.addLine(ne.body, ne.formatted_body)
|
rep.addLine(ne.body, ne.formatted_body)
|
||||||
prev.body = rep.body
|
prev.body = rep.body
|
||||||
prev.formatted_body = rep.formattedBody
|
prev.formatted_body = rep.formattedBody
|
||||||
|
} else if (forceMerge) {
|
||||||
|
throw new Error("Unable to merge events")
|
||||||
} else {
|
} else {
|
||||||
events.push(ne)
|
events.push(ne)
|
||||||
}
|
}
|
||||||
|
|
@ -967,7 +970,8 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
// May only be a section accessory or in an action row (up to 5)
|
// May only be a section accessory or in an action row (up to 5)
|
||||||
if (component.style === DiscordTypes.ButtonStyle.Link) {
|
if (component.style === DiscordTypes.ButtonStyle.Link) {
|
||||||
assert(component.label) // required for Discord to validate link buttons
|
assert(component.label) // required for Discord to validate link buttons
|
||||||
stack.msb.add(`[${component.label} ${component.url}] `, tag`<a href="${component.url}">${component.label}</a> `)
|
const link = await transformContentMessageLinks(component.url)
|
||||||
|
stack.msb.add(`[${component.label} ${link}] `, tag`<a href="${link}">${component.label}</a> `)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -980,7 +984,19 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
|
|
||||||
const {body, formatted_body} = stack.msb.get()
|
const {body, formatted_body} = stack.msb.get()
|
||||||
if (body.trim().length) {
|
if (body.trim().length) {
|
||||||
await addTextEvent(body, formatted_body, "m.text")
|
// 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
const {test} = require("supertape")
|
const {test} = require("supertape")
|
||||||
const {messageToEvent} = require("./message-to-event")
|
const {messageToEvent} = require("./message-to-event")
|
||||||
const data = require("../../../test/data")
|
const data = require("../../../test/data")
|
||||||
|
const {mockGetEffectivePower} = require("../../matrix/utils.test")
|
||||||
|
|
||||||
test("message2event components: pk question mark output", async t => {
|
test("message2event components: pk question mark output", async t => {
|
||||||
const events = await messageToEvent(data.message_with_components.pk_question_mark_response, data.guild.general, {})
|
const events = await messageToEvent(data.message_with_components.pk_question_mark_response, data.guild.general, {})
|
||||||
|
|
@ -77,3 +78,24 @@ test("message2event components: pk question mark output", async t => {
|
||||||
msgtype: "m.text",
|
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: "<blockquote><sub>❭ <a href=\"https://matrix.to/#/@_ooye_cadence:cadence.moe\">cadence</a> used <code>/🔔 Ping author</code></sub></blockquote>Psst, <strong>Red</strong> (<a href=\"https://matrix.to/#/@_ooye_cadence:cadence.moe\">@cadence.worm</a>), you have been pinged by <a href=\"https://matrix.to/#/@_ooye_cadence:cadence.moe\">@cadence.worm</a>.<br><a href=\"https://matrix.to/#/!TqlyQmifxGUggEmdBN:cadence.moe/$l9FMmsEbh9K0NUReeEpWOMZYGRlUOE8yLcm6P-TYHSM?via=cadence.moe\">Jump</a> ",
|
||||||
|
"m.mentions": {}
|
||||||
|
}])
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ function removeReaction(data, reactions, key) {
|
||||||
// Even though the bridge bot only reacted once on Discord-side, multiple Matrix users may have
|
// Even though the bridge bot only reacted once on Discord-side, multiple Matrix users may have
|
||||||
// reacted on Matrix-side. Semantically, we want to remove the reaction from EVERY Matrix user.
|
// reacted on Matrix-side. Semantically, we want to remove the reaction from EVERY Matrix user.
|
||||||
// Also need to clean up the database.
|
// Also need to clean up the database.
|
||||||
const hash = utils.getEventIDHash(event.event_id)
|
const hash = utils.getEventIDHash(eventID)
|
||||||
removals.push({eventID, mxid: null, hash})
|
removals.push({eventID, mxid: null, hash})
|
||||||
}
|
}
|
||||||
if (!lookingAtMatrixReaction && !wantToRemoveMatrixReaction) {
|
if (!lookingAtMatrixReaction && !wantToRemoveMatrixReaction) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
const assert = require("assert").strict
|
const assert = require("assert").strict
|
||||||
const DiscordTypes = require("discord-api-types/v10")
|
const DiscordTypes = require("discord-api-types/v10")
|
||||||
|
const {id: botID} = require("../../addbot")
|
||||||
const {sync, db, select, from} = require("../passthrough")
|
const {sync, db, select, from} = require("../passthrough")
|
||||||
|
|
||||||
/** @type {import("./actions/send-message")}) */
|
/** @type {import("./actions/send-message")}) */
|
||||||
|
|
@ -38,6 +39,8 @@ const removeMember = sync.require("./actions/remove-member")
|
||||||
const vote = sync.require("./actions/poll-vote")
|
const vote = sync.require("./actions/poll-vote")
|
||||||
/** @type {import("../m2d/event-dispatcher")} */
|
/** @type {import("../m2d/event-dispatcher")} */
|
||||||
const matrixEventDispatcher = sync.require("../m2d/event-dispatcher")
|
const matrixEventDispatcher = sync.require("../m2d/event-dispatcher")
|
||||||
|
/** @type {import("../m2d/actions/redact.js")} */
|
||||||
|
const redact = sync.require("../m2d/actions/redact.js")
|
||||||
/** @type {import("../discord/interactions/matrix-info")} */
|
/** @type {import("../discord/interactions/matrix-info")} */
|
||||||
const matrixInfoInteraction = sync.require("../discord/interactions/matrix-info")
|
const matrixInfoInteraction = sync.require("../discord/interactions/matrix-info")
|
||||||
|
|
||||||
|
|
@ -310,13 +313,13 @@ module.exports = {
|
||||||
|
|
||||||
if (!createRoom.existsOrAutocreatable(channel, guild.id)) return // Check that the sending-to room exists or is autocreatable
|
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)
|
const {affected, row} = await speedbump.maybeDoSpeedbump(message.channel_id, message.id, message.author.id)
|
||||||
if (affected) return
|
if (affected) return
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
await sendMessage.sendMessage(message, channel, guild, row)
|
await sendMessage.sendMessage(message, channel, guild, row)
|
||||||
|
|
||||||
retrigger.messageFinishedBridging(message.id)
|
retrigger.finishedBridging(message.id)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -332,12 +335,12 @@ module.exports = {
|
||||||
if (dUtils.isEphemeralMessage(data)) return // Ephemeral messages are for the eyes of the receiver only!
|
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.
|
// 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)
|
const {affected, row} = await speedbump.maybeDoSpeedbump(data.channel_id, data.id, data.author.id)
|
||||||
if (affected) return
|
if (affected) return
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
// Check that the sending-to room exists, and deal with Eventual Consistency(TM)
|
// Check that the sending-to room exists, and deal with Eventual Consistency(TM)
|
||||||
if (retrigger.eventNotFoundThenRetrigger(data.id, module.exports.MESSAGE_UPDATE, client, data)) return
|
if (!await retrigger.waitForMessage(data.id)) return
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {DiscordTypes.GatewayMessageCreateDispatchData} */
|
/** @type {DiscordTypes.GatewayMessageCreateDispatchData} */
|
||||||
|
|
@ -375,6 +378,16 @@ module.exports = {
|
||||||
* @param {DiscordTypes.GatewayMessageReactionRemoveDispatchData | DiscordTypes.GatewayMessageReactionRemoveEmojiDispatchData | DiscordTypes.GatewayMessageReactionRemoveAllDispatchData} data
|
* @param {DiscordTypes.GatewayMessageReactionRemoveDispatchData | DiscordTypes.GatewayMessageReactionRemoveEmojiDispatchData | DiscordTypes.GatewayMessageReactionRemoveAllDispatchData} data
|
||||||
*/
|
*/
|
||||||
async onSomeReactionsRemoved(client, data) {
|
async onSomeReactionsRemoved(client, data) {
|
||||||
|
// Don't attempt to double-bridge our own m2d deleted reactions back to Matrix
|
||||||
|
if ("user_id" in data && data.user_id === botID) {
|
||||||
|
const emojiIdOrName = data.emoji.id || data.emoji.name
|
||||||
|
const i = redact.m2dDeletedReactions.findIndex(x => data.message_id === x.messageID && emojiIdOrName === x.emojiIdOrName)
|
||||||
|
if (i !== -1) {
|
||||||
|
redact.m2dDeletedReactions.splice(i, 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await removeReaction.removeSomeReactions(data)
|
await removeReaction.removeSomeReactions(data)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -384,7 +397,7 @@ module.exports = {
|
||||||
*/
|
*/
|
||||||
async MESSAGE_DELETE(client, data) {
|
async MESSAGE_DELETE(client, data) {
|
||||||
speedbump.onMessageDelete(data.id)
|
speedbump.onMessageDelete(data.id)
|
||||||
if (retrigger.eventNotFoundThenRetrigger(data.id, module.exports.MESSAGE_DELETE, client, data)) return
|
if (!await retrigger.waitForMessage(data.id)) return
|
||||||
await deleteMessage.deleteMessage(data)
|
await deleteMessage.deleteMessage(data)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -432,12 +445,12 @@ module.exports = {
|
||||||
* @param {DiscordTypes.GatewayMessagePollVoteDispatchData} data
|
* @param {DiscordTypes.GatewayMessagePollVoteDispatchData} data
|
||||||
*/
|
*/
|
||||||
async MESSAGE_POLL_VOTE_ADD(client, data) {
|
async MESSAGE_POLL_VOTE_ADD(client, data) {
|
||||||
if (retrigger.eventNotFoundThenRetrigger(data.message_id, module.exports.MESSAGE_POLL_VOTE_ADD, client, data)) return
|
if (!await retrigger.waitForMessage(data.message_id)) return
|
||||||
await vote.addVote(data)
|
await vote.addVote(data)
|
||||||
},
|
},
|
||||||
|
|
||||||
async MESSAGE_POLL_VOTE_REMOVE(client, data) {
|
async MESSAGE_POLL_VOTE_REMOVE(client, data) {
|
||||||
if (retrigger.eventNotFoundThenRetrigger(data.message_id, module.exports.MESSAGE_POLL_VOTE_REMOVE, client, data)) return
|
if (!await retrigger.waitForMessage(data.message_id)) return
|
||||||
await vote.removeVote(data)
|
await vote.removeVote(data)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,16 @@ class From {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pluckUnsafe(col) {
|
||||||
|
/** @type {Pluck<Table, any>} */
|
||||||
|
// @ts-ignore
|
||||||
|
const r = this
|
||||||
|
r.cols = [col]
|
||||||
|
r.makeColsSafe = false
|
||||||
|
r.isPluck = true
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} sql
|
* @param {string} sql
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -68,3 +68,8 @@ test("orm: select unsafe works (to select complex column names that can't be typ
|
||||||
.all()
|
.all()
|
||||||
t.equal(results[0].power_level, 150)
|
t.equal(results[0].power_level, 150)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("orm: pluck unsafe works (to select complex column names that can't be type verified)", t => {
|
||||||
|
const result = from("channel_room").where({guild_id: "112760669178241024"}).pluckUnsafe("count(*)").get()
|
||||||
|
t.equal(result, 7)
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -91,40 +91,32 @@ function registerInteractions() {
|
||||||
async function dispatchInteraction(interaction) {
|
async function dispatchInteraction(interaction) {
|
||||||
const interactionId = interaction.data?.["custom_id"] || interaction.data?.["name"]
|
const interactionId = interaction.data?.["custom_id"] || interaction.data?.["name"]
|
||||||
try {
|
try {
|
||||||
if (interaction.type === DiscordTypes.InteractionType.MessageComponent || interaction.type === DiscordTypes.InteractionType.ModalSubmit) {
|
if (interactionId === "Matrix info") {
|
||||||
// All we get is custom_id, don't know which context the button was clicked in.
|
await matrixInfo.interact(interaction)
|
||||||
// So we namespace these ourselves in the custom_id. Currently the only existing namespace is POLL_.
|
} else if (interactionId === "invite") {
|
||||||
if (interaction.data.custom_id.startsWith("POLL_")) {
|
await invite.interact(interaction)
|
||||||
await poll.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 {
|
} else {
|
||||||
throw new Error(`Unknown message component ${interaction.data.custom_id}`)
|
await reactions.interact(messageInteraction)
|
||||||
}
|
}
|
||||||
|
} 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 {
|
} else {
|
||||||
if (interactionId === "Matrix info") {
|
throw new Error(`Unknown interaction ${interactionId}`)
|
||||||
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) {
|
} catch (e) {
|
||||||
let stackLines = null
|
let stackLines = null
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ const retrigger = sync.require("../../d2m/actions/retrigger")
|
||||||
*/
|
*/
|
||||||
async function addReaction(event) {
|
async function addReaction(event) {
|
||||||
// Wait until the corresponding channel and message have already been bridged
|
// Wait until the corresponding channel and message have already been bridged
|
||||||
if (retrigger.eventNotFoundThenRetrigger(event.content["m.relates_to"].event_id, () => as.emit("type:m.reaction", event))) return
|
if (!await retrigger.waitForEvent(event.content["m.relates_to"].event_id)) return
|
||||||
|
|
||||||
// These will exist because it passed retrigger
|
// These will exist because it passed retrigger
|
||||||
const row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
const row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
||||||
|
|
@ -50,6 +50,8 @@ async function addReaction(event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
db.prepare("REPLACE INTO reaction (hashed_event_id, message_id, encoded_emoji, original_encoding) VALUES (?, ?, ?, ?)").run(utils.getEventIDHash(event.event_id), messageID, discordPreferredEncoding, key)
|
db.prepare("REPLACE INTO reaction (hashed_event_id, message_id, encoded_emoji, original_encoding) VALUES (?, ?, ?, ?)").run(utils.getEventIDHash(event.event_id), messageID, discordPreferredEncoding, key)
|
||||||
|
|
||||||
|
retrigger.finishedBridging(event.event_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.addReaction = addReaction
|
module.exports.addReaction = addReaction
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ const utils = sync.require("../../matrix/utils")
|
||||||
/** @type {import("../../d2m/actions/retrigger")} */
|
/** @type {import("../../d2m/actions/retrigger")} */
|
||||||
const retrigger = sync.require("../../d2m/actions/retrigger")
|
const retrigger = sync.require("../../d2m/actions/retrigger")
|
||||||
|
|
||||||
|
/** @type {{messageID: string, emojiIdOrName: string}[]} */
|
||||||
|
const m2dDeletedReactions = []
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
||||||
*/
|
*/
|
||||||
|
|
@ -24,6 +27,21 @@ async function deleteMessage(event) {
|
||||||
db.prepare("DELETE FROM message_room WHERE message_id = ?").run(rows[0].message_id)
|
db.prepare("DELETE FROM message_room WHERE message_id = ?").run(rows[0].message_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
||||||
|
*/
|
||||||
|
async function removeMessageEvent(event) {
|
||||||
|
// Could be for removing a message or suppressing embeds. For more information, the message needs to be bridged first.
|
||||||
|
if (!await retrigger.waitForEvent(event.redacts)) return
|
||||||
|
|
||||||
|
const row = select("event_message", ["event_type", "event_subtype", "part"], {event_id: event.redacts}).get()
|
||||||
|
if (row && row.event_type === "m.room.message" && row.event_subtype === "m.notice" && row.part === 1) {
|
||||||
|
await suppressEmbeds(event)
|
||||||
|
} else {
|
||||||
|
await deleteMessage(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
||||||
*/
|
*/
|
||||||
|
|
@ -41,11 +59,20 @@ async function suppressEmbeds(event) {
|
||||||
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
||||||
*/
|
*/
|
||||||
async function removeReaction(event) {
|
async function removeReaction(event) {
|
||||||
|
if (!await retrigger.waitForReactionEvent(event.redacts)) return
|
||||||
|
|
||||||
const hash = utils.getEventIDHash(event.redacts)
|
const hash = utils.getEventIDHash(event.redacts)
|
||||||
const row = from("reaction").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
const row = from("reaction").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
||||||
.select("reference_channel_id", "message_id", "encoded_emoji").where({hashed_event_id: hash}).get()
|
.select("reference_channel_id", "message_id", "encoded_emoji").where({hashed_event_id: hash}).get()
|
||||||
if (!row) return
|
if (!row) return
|
||||||
await discord.snow.channel.deleteReactionSelf(row.reference_channel_id, row.message_id, row.encoded_emoji)
|
// See how many Matrix-side reactions there are, and delete if it's the last one
|
||||||
|
const numberOfReactions = from("reaction").where({message_id: row.message_id, encoded_emoji: row.encoded_emoji}).pluckUnsafe("count(*)").get()
|
||||||
|
if (numberOfReactions === 1) {
|
||||||
|
// If a unicode emoji, the name is already the Discord preferred version because that's what was added and stored to encoded_emoji
|
||||||
|
const emojiIdOrName = decodeURIComponent(row.encoded_emoji).split(":").slice(-1)[0]
|
||||||
|
m2dDeletedReactions.push({messageID: row.message_id, emojiIdOrName})
|
||||||
|
await discord.snow.channel.deleteReactionSelf(row.reference_channel_id, row.message_id, row.encoded_emoji)
|
||||||
|
}
|
||||||
db.prepare("DELETE FROM reaction WHERE hashed_event_id = ?").run(hash)
|
db.prepare("DELETE FROM reaction WHERE hashed_event_id = ?").run(hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,18 +81,12 @@ async function removeReaction(event) {
|
||||||
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
||||||
*/
|
*/
|
||||||
async function handle(event) {
|
async function handle(event) {
|
||||||
// If this is for removing a reaction, try it
|
// Don't know if it's a redaction for a reaction or an event, try both at the same time (otherwise waitFor will block)
|
||||||
await removeReaction(event)
|
await Promise.all([
|
||||||
|
removeMessageEvent(event),
|
||||||
// Or, it might be for removing a message or suppressing embeds. But to do that, the message needs to be bridged first.
|
removeReaction(event)
|
||||||
if (retrigger.eventNotFoundThenRetrigger(event.redacts, () => as.emit("type:m.room.redaction", event))) return
|
])
|
||||||
|
|
||||||
const row = select("event_message", ["event_type", "event_subtype", "part"], {event_id: event.redacts}).get()
|
|
||||||
if (row && row.event_type === "m.room.message" && row.event_subtype === "m.notice" && row.part === 1) {
|
|
||||||
await suppressEmbeds(event)
|
|
||||||
} else {
|
|
||||||
await deleteMessage(event)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.handle = handle
|
module.exports.handle = handle
|
||||||
|
module.exports.m2dDeletedReactions = m2dDeletedReactions
|
||||||
|
|
@ -225,7 +225,7 @@ async event => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
await matrixCommandHandler.execute(event)
|
await matrixCommandHandler.execute(event)
|
||||||
}
|
}
|
||||||
retrigger.messageFinishedBridging(event.event_id)
|
retrigger.finishedBridging(event.event_id)
|
||||||
await api.ackEvent(event)
|
await api.ackEvent(event)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
@ -236,7 +236,7 @@ sync.addTemporaryListener(as, "type:m.sticker", guard("m.sticker",
|
||||||
async event => {
|
async event => {
|
||||||
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
||||||
const messageResponses = await sendEvent.sendEvent(event)
|
const messageResponses = await sendEvent.sendEvent(event)
|
||||||
retrigger.messageFinishedBridging(event.event_id)
|
retrigger.finishedBridging(event.event_id)
|
||||||
await api.ackEvent(event)
|
await api.ackEvent(event)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ const mreq = sync.require("./matrix/mreq")
|
||||||
const api = sync.require("./matrix/api")
|
const api = sync.require("./matrix/api")
|
||||||
const file = sync.require("./matrix/file")
|
const file = sync.require("./matrix/file")
|
||||||
const sendEvent = sync.require("./m2d/actions/send-event")
|
const sendEvent = sync.require("./m2d/actions/send-event")
|
||||||
|
const redact = sync.require("./m2d/actions/redact")
|
||||||
const eventDispatcher = sync.require("./d2m/event-dispatcher")
|
const eventDispatcher = sync.require("./d2m/event-dispatcher")
|
||||||
const updatePins = sync.require("./d2m/actions/update-pins")
|
const updatePins = sync.require("./d2m/actions/update-pins")
|
||||||
const speedbump = sync.require("./d2m/actions/speedbump")
|
const speedbump = sync.require("./d2m/actions/speedbump")
|
||||||
|
|
@ -22,7 +23,7 @@ const ks = sync.require("./matrix/kstate")
|
||||||
const setPresence = sync.require("./d2m/actions/set-presence")
|
const setPresence = sync.require("./d2m/actions/set-presence")
|
||||||
const channelWebhook = sync.require("./m2d/actions/channel-webhook")
|
const channelWebhook = sync.require("./m2d/actions/channel-webhook")
|
||||||
const dUtils = sync.require("./discord/utils")
|
const dUtils = sync.require("./discord/utils")
|
||||||
const mUtils = sync.require("./matrix/utils")
|
const mxUtils = sync.require("./matrix/utils")
|
||||||
const guildID = "112760669178241024"
|
const guildID = "112760669178241024"
|
||||||
|
|
||||||
async function ping() {
|
async function ping() {
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ block body
|
||||||
#role-add.s-popover(popover style="display: revert").ws2.px0.py4.bs-lg.overflow-visible
|
#role-add.s-popover(popover style="display: revert").ws2.px0.py4.bs-lg.overflow-visible
|
||||||
.s-popover--arrow.s-popover--arrow__tc
|
.s-popover--arrow.s-popover--arrow__tc
|
||||||
+add-roles-menu(guild, guild_id)
|
+add-roles-menu(guild, guild_id)
|
||||||
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.
|
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.
|
||||||
|
|
||||||
h3.mt32.fs-category Features
|
h3.mt32.fs-category Features
|
||||||
.s-card.d-grid.px0.g16
|
.s-card.d-grid.px0.g16
|
||||||
|
|
@ -191,14 +191,14 @@ block body
|
||||||
label.s-btn.s-btn__muted.ta-left.truncate(for=channel.id)
|
label.s-btn.s-btn__muted.ta-left.truncate(for=channel.id)
|
||||||
+discord(channel, true, "Announcement")
|
+discord(channel, true, "Announcement")
|
||||||
else
|
else
|
||||||
.s-empty-state.p8 All Discord channels are linked.
|
.s-empty-state.p8 No Discord channels available.
|
||||||
.fl-grow1.s-btn-group.fd-column.w30
|
.fl-grow1.s-btn-group.fd-column.w30
|
||||||
each room in unlinkedRooms
|
each room in unlinkedRooms
|
||||||
input.s-btn--radio(type="radio" name="matrix" required id=room.room_id value=room.room_id)
|
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)
|
label.s-btn.s-btn__muted.ta-left.truncate(for=room.room_id)
|
||||||
+matrix(room, true)
|
+matrix(room, true)
|
||||||
else
|
else
|
||||||
.s-empty-state.p8 All Matrix rooms are linked.
|
.s-empty-state.p8 No Matrix rooms available.
|
||||||
input(type="hidden" name="guild_id" value=guild_id)
|
input(type="hidden" name="guild_id" value=guild_id)
|
||||||
div
|
div
|
||||||
button.s-btn.s-btn__icon.s-btn__filled#link-button
|
button.s-btn.s-btn__icon.s-btn__filled#link-button
|
||||||
|
|
|
||||||
183
test/data.js
183
test/data.js
|
|
@ -5473,6 +5473,189 @@ module.exports = {
|
||||||
content: '-# Original Message ID: 1466556003645657118 · <t:1769724599:f>'
|
content: '-# Original Message ID: 1466556003645657118 · <t:1769724599:f>'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
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: {
|
message_update: {
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,8 @@ WITH a (message_id, channel_id) AS (VALUES
|
||||||
('1381212840957972480', '112760669178241024'),
|
('1381212840957972480', '112760669178241024'),
|
||||||
('1401760355339862066', '112760669178241024'),
|
('1401760355339862066', '112760669178241024'),
|
||||||
('1439351590262800565', '1438284564815548418'),
|
('1439351590262800565', '1438284564815548418'),
|
||||||
('1404133238414376971', '112760669178241024'))
|
('1404133238414376971', '112760669178241024'),
|
||||||
|
('1440549403667468320', '1160894080998461480'))
|
||||||
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;
|
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
|
INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES
|
||||||
|
|
@ -143,7 +144,8 @@ 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),
|
('$7P2O_VTQNHvavX5zNJ35DV-dbJB1Ag80tGQP_JzGdhk', 'm.room.message', 'm.text', '1401760355339862066', 0, 0, 0),
|
||||||
('$ielAnR6geu0P1Tl5UXfrbxlIf-SV9jrNprxrGXP3v7M', 'm.room.message', 'm.image', '1439351590262800565', 0, 0, 0),
|
('$ielAnR6geu0P1Tl5UXfrbxlIf-SV9jrNprxrGXP3v7M', 'm.room.message', 'm.image', '1439351590262800565', 0, 0, 0),
|
||||||
('$uUKLcTQvik5tgtTGDKuzn0Ci4zcCvSoUcYn2X7mXm9I', 'm.room.message', 'm.text', '1404133238414376971', 0, 1, 1),
|
('$uUKLcTQvik5tgtTGDKuzn0Ci4zcCvSoUcYn2X7mXm9I', 'm.room.message', 'm.text', '1404133238414376971', 0, 1, 1),
|
||||||
('$LhmoWWvYyn5_AHkfb6FaXmLI6ZOC1kloql5P40YDmIk', 'm.room.message', 'm.notice', '1404133238414376971', 1, 0, 1);
|
('$LhmoWWvYyn5_AHkfb6FaXmLI6ZOC1kloql5P40YDmIk', 'm.room.message', 'm.notice', '1404133238414376971', 1, 0, 1),
|
||||||
|
('$l9FMmsEbh9K0NUReeEpWOMZYGRlUOE8yLcm6P-TYHSM', 'm.room.message', 'm.text', '1440549403667468320', 0, 0, 1);
|
||||||
|
|
||||||
INSERT INTO file (discord_url, mxc_url) VALUES
|
INSERT INTO file (discord_url, mxc_url) VALUES
|
||||||
('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'),
|
('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue