313efb29d8
Fixes a bug where, if multiple Matrix users had used the same reaction on a message, and then one of those Matrix users removed their reactions, the bot would forcibly remove all of that reactions. Now, we check and make sure there are no remaining reactions from Matrix before removal. This also rewrote the retrigger system to be more generic and to use promises instead of re-entry (would lose call stack). Co-authored-by: Cadence Ember <cadence@disroot.org> Reviewed-on: https://gitdab.com/cadence/out-of-your-element/pulls/85
522 lines
20 KiB
JavaScript
522 lines
20 KiB
JavaScript
// @ts-check
|
|
|
|
/*
|
|
* Grab Matrix events we care about, check them, and bridge them.
|
|
*/
|
|
|
|
const util = require("util")
|
|
const Ty = require("../types")
|
|
const {discord, db, sync, as, select} = require("../passthrough")
|
|
const {tag} = require("@cloudrac3r/html-template-tag")
|
|
const {Semaphore} = require("@chriscdn/promise-semaphore")
|
|
|
|
/** @type {import("./actions/send-event")} */
|
|
const sendEvent = sync.require("./actions/send-event")
|
|
/** @type {import("./actions/add-reaction")} */
|
|
const addReaction = sync.require("./actions/add-reaction")
|
|
/** @type {import("./actions/redact")} */
|
|
const redact = sync.require("./actions/redact")
|
|
/** @type {import("./actions/update-pins")}) */
|
|
const updatePins = sync.require("./actions/update-pins")
|
|
/** @type {import("./actions/vote")}) */
|
|
const vote = sync.require("./actions/vote")
|
|
/** @type {import("../matrix/matrix-command-handler")} */
|
|
const matrixCommandHandler = sync.require("../matrix/matrix-command-handler")
|
|
/** @type {import("../matrix/utils")} */
|
|
const utils = sync.require("../matrix/utils")
|
|
/** @type {import("../matrix/api")}) */
|
|
const api = sync.require("../matrix/api")
|
|
/** @type {import("../d2m/actions/create-room")} */
|
|
const createRoom = sync.require("../d2m/actions/create-room")
|
|
/** @type {import("../matrix/room-upgrade")} */
|
|
const roomUpgrade = require("../matrix/room-upgrade")
|
|
/** @type {import("../d2m/actions/retrigger")} */
|
|
const retrigger = sync.require("../d2m/actions/retrigger")
|
|
const {reg} = require("../matrix/read-registration")
|
|
|
|
let lastReportedEvent = 0
|
|
|
|
/**
|
|
* This function is adapted from Evan Kaufman's fantastic work.
|
|
* The original function and my adapted function are both MIT licensed.
|
|
* @url https://github.com/EvanK/npm-loggable-error/
|
|
* @param {number} [depth]
|
|
* @returns {string}
|
|
*/
|
|
function stringifyErrorStack(err, depth = 0) {
|
|
let collapsed = " ".repeat(depth);
|
|
if (!(err instanceof Error)) {
|
|
return collapsed + err
|
|
}
|
|
|
|
// add full stack trace if one exists, otherwise convert to string
|
|
let stackLines = String(err?.stack ?? err).replace(/^/gm, " ".repeat(depth)).trim().split("\n")
|
|
let cloudstormLine = stackLines.findIndex(l => l.includes("/node_modules/cloudstorm/"))
|
|
if (cloudstormLine !== -1) {
|
|
stackLines = stackLines.slice(0, cloudstormLine - 2)
|
|
}
|
|
collapsed += stackLines.join("\n")
|
|
|
|
const props = Object.getOwnPropertyNames(err).filter(p => !["message", "stack"].includes(p))
|
|
|
|
// only break into object notation if we have additional props to dump
|
|
if (props.length) {
|
|
const dedent = " ".repeat(depth);
|
|
const indent = " ".repeat(depth + 2);
|
|
|
|
collapsed += " {\n";
|
|
|
|
// loop and print each (indented) prop name
|
|
for (let property of props) {
|
|
collapsed += `${indent}[${property}]: `;
|
|
|
|
// if another error object, stringify it too
|
|
if (err[property] instanceof Error) {
|
|
collapsed += stringifyErrorStack(err[property], depth + 2).trimStart();
|
|
}
|
|
// otherwise stringify as JSON
|
|
else {
|
|
collapsed += JSON.stringify(err[property]);
|
|
}
|
|
|
|
collapsed += "\n";
|
|
}
|
|
|
|
collapsed += `${dedent}}\n`;
|
|
}
|
|
|
|
return collapsed;
|
|
}
|
|
|
|
function printError(type, source, e, payload) {
|
|
console.error(`Error while processing a ${type} ${source} event:`)
|
|
console.error(e)
|
|
console.dir(payload, {depth: null})
|
|
}
|
|
|
|
/** @param {string} stack */
|
|
function cleanErrorStack(stack) {
|
|
return stack.replace(/(\/webhooks\/[0-9]+\/)[a-zA-Z0-9_-]+/g, "$1(redacted)")
|
|
}
|
|
|
|
/**
|
|
* @param {string} roomID
|
|
* @param {"Discord" | "Matrix"} source
|
|
* @param {any} type
|
|
* @param {any} e
|
|
* @param {any} payload
|
|
*/
|
|
async function sendError(roomID, source, type, e, payload) {
|
|
if (source === "Matrix") {
|
|
printError(type, source, e, payload)
|
|
}
|
|
|
|
if (Date.now() - lastReportedEvent < 5000) return null
|
|
lastReportedEvent = Date.now()
|
|
|
|
let errorIntroLine = e.toString()
|
|
if (e.cause) {
|
|
errorIntroLine += ` (cause: ${e.cause})`
|
|
}
|
|
|
|
const builder = new utils.MatrixStringBuilder()
|
|
|
|
const cloudflareErrorTitle = errorIntroLine.match(/<!DOCTYPE html>.*?<title>discord\.com \| ([^<]*)<\/title>/s)?.[1]
|
|
if (cloudflareErrorTitle) {
|
|
builder.addLine(
|
|
`\u26a0 Matrix event not delivered to Discord. Discord might be down right now. Cloudflare error: ${cloudflareErrorTitle}`,
|
|
`\u26a0 <strong>Matrix event not delivered to Discord</strong><br>Discord might be down right now. Cloudflare error: ${cloudflareErrorTitle}`
|
|
)
|
|
} else {
|
|
// What
|
|
const what = source === "Discord" ? "Bridged event from Discord not delivered" : "Matrix event not delivered to Discord"
|
|
builder.addLine(`\u26a0 ${what}`, `\u26a0 <strong>${what}</strong>`)
|
|
|
|
// Who
|
|
builder.addLine(`Event type: ${type}`)
|
|
|
|
// Why
|
|
builder.addLine(errorIntroLine)
|
|
|
|
// Where
|
|
const stack = cleanErrorStack(stringifyErrorStack(e))
|
|
builder.addLine(`Error trace:\n${stack}`, tag`<details><summary>Error trace</summary><pre>${stack}</pre></details>`)
|
|
|
|
// How
|
|
builder.addLine("", tag`<details><summary>Original payload</summary><pre>${util.inspect(payload, false, 4, false)}</pre></details>`)
|
|
}
|
|
|
|
// Send
|
|
try {
|
|
const errorEventID = await api.sendEvent(roomID, "m.room.message", {
|
|
...builder.get(),
|
|
"moe.cadence.ooye.error": {
|
|
source: source.toLowerCase(),
|
|
payload
|
|
},
|
|
"m.mentions": {
|
|
user_ids: ["@cadence:cadence.moe"]
|
|
}
|
|
})
|
|
// Add reaction indicating that errors may be retried
|
|
await api.sendEvent(roomID, "m.reaction", {
|
|
"m.relates_to": {
|
|
rel_type: "m.annotation",
|
|
event_id: errorEventID,
|
|
key: "🔁"
|
|
}
|
|
})
|
|
} catch (e) {}
|
|
}
|
|
|
|
function guard(type, fn) {
|
|
return async function(event, ...args) {
|
|
try {
|
|
return await fn(event, ...args)
|
|
} catch (e) {
|
|
await sendError(event.room_id, "Matrix", type, e, event)
|
|
}
|
|
}
|
|
}
|
|
|
|
const errorRetrySema = new Semaphore()
|
|
|
|
/**
|
|
* @param {Ty.Event.Outer<Ty.Event.M_Reaction>} reactionEvent
|
|
*/
|
|
async function onRetryReactionAdd(reactionEvent) {
|
|
if (reactionEvent.sender === `@${reg.sender_localpart}:${reg.ooye.server_name}`) return // Don't respond to the bot's own indicative reaction
|
|
const roomID = reactionEvent.room_id
|
|
await errorRetrySema.request(async () => {
|
|
const event = await api.getEvent(roomID, reactionEvent.content["m.relates_to"]?.event_id)
|
|
|
|
// Check that it's a real error from OOYE
|
|
const error = event.content["moe.cadence.ooye.error"]
|
|
if (event.sender !== `@${reg.sender_localpart}:${reg.ooye.server_name}` || !error) return
|
|
|
|
// To stop people injecting misleading messages, the reaction needs to come from either the original sender or a room moderator
|
|
if (reactionEvent.sender !== error.payload.sender) {
|
|
// Check if it's a room moderator
|
|
const {powers: {[reactionEvent.sender]: senderPower}, powerLevels} = await utils.getEffectivePower(roomID, [reactionEvent.sender], api)
|
|
if (senderPower < (powerLevels.state_default ?? 50)) return
|
|
}
|
|
|
|
// Retry
|
|
if (error.source === "matrix") {
|
|
as.emit(`type:${error.payload.type}`, error.payload)
|
|
} else if (error.source === "discord") {
|
|
discord.cloud.emit("event", error.payload)
|
|
}
|
|
|
|
// Redact the error to stop people from executing multiple retries
|
|
await api.redactEvent(roomID, event.event_id)
|
|
}, roomID)
|
|
}
|
|
|
|
sync.addTemporaryListener(as, "type:m.room.message", guard("m.room.message",
|
|
/**
|
|
* @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File} event it is a m.room.message because that's what this listener is filtering for
|
|
*/
|
|
async event => {
|
|
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
|
const messageResponses = await sendEvent.sendEvent(event)
|
|
if (!messageResponses.length) return
|
|
if (event.type === "m.room.message" && event.content.msgtype === "m.text") {
|
|
// @ts-ignore
|
|
await matrixCommandHandler.execute(event)
|
|
}
|
|
retrigger.finishedBridging(event.event_id)
|
|
await api.ackEvent(event)
|
|
}))
|
|
|
|
sync.addTemporaryListener(as, "type:m.sticker", guard("m.sticker",
|
|
/**
|
|
* @param {Ty.Event.Outer_M_Sticker} event it is a m.sticker because that's what this listener is filtering for
|
|
*/
|
|
async event => {
|
|
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
|
const messageResponses = await sendEvent.sendEvent(event)
|
|
retrigger.finishedBridging(event.event_id)
|
|
await api.ackEvent(event)
|
|
}))
|
|
|
|
sync.addTemporaryListener(as, "type:org.matrix.msc3381.poll.start", guard("org.matrix.msc3381.poll.start",
|
|
/**
|
|
* @param {Ty.Event.Outer_Org_Matrix_Msc3381_Poll_Start} event it is a org.matrix.msc3381.poll.start because that's what this listener is filtering for
|
|
*/
|
|
async event => {
|
|
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
|
const messageResponses = await sendEvent.sendEvent(event)
|
|
await api.ackEvent(event)
|
|
}))
|
|
|
|
sync.addTemporaryListener(as, "type:org.matrix.msc3381.poll.response", guard("org.matrix.msc3381.poll.response",
|
|
/**
|
|
* @param {Ty.Event.Outer_Org_Matrix_Msc3381_Poll_Response} event it is a org.matrix.msc3381.poll.response because that's what this listener is filtering for
|
|
*/
|
|
async event => {
|
|
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
|
await vote.updateVote(event) // Matrix votes can't be bridged, so all we do is store it in the database.
|
|
await api.ackEvent(event)
|
|
}))
|
|
|
|
sync.addTemporaryListener(as, "type:org.matrix.msc3381.poll.end", guard("org.matrix.msc3381.poll.end",
|
|
/**
|
|
* @param {Ty.Event.Outer_Org_Matrix_Msc3381_Poll_End} event it is a org.matrix.msc3381.poll.end because that's what this listener is filtering for
|
|
*/
|
|
async event => {
|
|
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
|
const pollEventID = event.content["m.relates_to"]?.event_id
|
|
if (!pollEventID) return // Validity check
|
|
const messageID = select("event_message", "message_id", {event_id: pollEventID, event_type: "org.matrix.msc3381.poll.start", source: 0}).pluck().get()
|
|
if (!messageID) return // Nothing can be done if the parent message was never bridged. Also, Discord-native polls cannot be ended by others, so this only works for polls started on Matrix.
|
|
try {
|
|
var pollEvent = await api.getEvent(event.room_id, pollEventID) // Poll start event must exist for this to be valid
|
|
} catch (e) {
|
|
return
|
|
}
|
|
|
|
// According to the rules, the poll end is only allowed if it was sent by the poll starter, or by someone with redact powers.
|
|
if (pollEvent.sender !== event.sender) {
|
|
const {powerLevels, powers: {[event.sender]: enderPower}} = await utils.getEffectivePower(event.room_id, [event.sender], api)
|
|
if (enderPower < (powerLevels.redact ?? 50)) {
|
|
return // Not allowed
|
|
}
|
|
}
|
|
|
|
const messageResponses = await sendEvent.sendEvent(event)
|
|
await api.ackEvent(event)
|
|
}))
|
|
|
|
sync.addTemporaryListener(as, "type:m.reaction", guard("m.reaction",
|
|
/**
|
|
* @param {Ty.Event.Outer<Ty.Event.M_Reaction>} event it is a m.reaction because that's what this listener is filtering for
|
|
*/
|
|
async event => {
|
|
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
|
if (event.content["m.relates_to"].key === "🔁") {
|
|
// Try to bridge a failed event again?
|
|
await onRetryReactionAdd(event)
|
|
} else {
|
|
matrixCommandHandler.onReactionAdd(event)
|
|
await addReaction.addReaction(event)
|
|
}
|
|
}))
|
|
|
|
sync.addTemporaryListener(as, "type:m.room.redaction", guard("m.room.redaction",
|
|
/**
|
|
* @param {Ty.Event.Outer_M_Room_Redaction} event it is a m.room.redaction because that's what this listener is filtering for
|
|
*/
|
|
async event => {
|
|
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
|
await redact.handle(event)
|
|
await api.ackEvent(event)
|
|
}))
|
|
|
|
sync.addTemporaryListener(as, "type:m.room.avatar", guard("m.room.avatar",
|
|
/**
|
|
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Avatar>} event
|
|
*/
|
|
async event => {
|
|
if (event.state_key !== "") return
|
|
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
|
const url = event.content.url || null
|
|
db.prepare("UPDATE channel_room SET custom_avatar = ? WHERE room_id = ?").run(url, event.room_id)
|
|
}))
|
|
|
|
sync.addTemporaryListener(as, "type:m.room.name", guard("m.room.name",
|
|
/**
|
|
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Name>} event
|
|
*/
|
|
async event => {
|
|
if (event.state_key !== "") return
|
|
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
|
const name = event.content.name || null
|
|
db.prepare("UPDATE channel_room SET nick = ? WHERE room_id = ?").run(name, event.room_id)
|
|
}))
|
|
|
|
sync.addTemporaryListener(as, "type:m.room.topic", guard("m.room.topic",
|
|
/**
|
|
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Topic>} event
|
|
*/
|
|
async event => {
|
|
if (event.state_key !== "") return
|
|
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
|
const customTopic = +!!event.content.topic
|
|
const row = select("channel_room", ["channel_id", "custom_topic"], {room_id: event.room_id}).get()
|
|
if (!row) return
|
|
if (customTopic !== row.custom_topic) db.prepare("UPDATE channel_room SET custom_topic = ? WHERE channel_id = ?").run(customTopic, row.channel_id)
|
|
if (!customTopic) await createRoom.syncRoom(row.channel_id) // if it's cleared we should reset it to whatever's on discord
|
|
}))
|
|
|
|
sync.addTemporaryListener(as, "type:m.room.pinned_events", guard("m.room.pinned_events",
|
|
/**
|
|
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_PinnedEvents>} event
|
|
*/
|
|
async event => {
|
|
if (event.state_key !== "") return
|
|
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
|
const pins = event.content.pinned
|
|
if (!Array.isArray(pins)) return
|
|
let prev = event.unsigned?.prev_content?.pinned
|
|
if (!Array.isArray(prev)) {
|
|
if (pins.length === 1) {
|
|
/*
|
|
In edge cases, prev_content isn't guaranteed to be provided by the server.
|
|
If prev_content is missing, we can't diff. Better safe than sorry: we'd like to ignore the change rather than wiping the whole channel's pins on Discord.
|
|
However, that would mean if the first ever pin came from Matrix-side, it would be ignored, because there would be no prev_content (it's the first pinned event!)
|
|
So to handle that edge case, we assume that if there's exactly 1 entry in `pinned`, this is the first ever pin and it should go through.
|
|
*/
|
|
prev = []
|
|
} else {
|
|
return
|
|
}
|
|
}
|
|
|
|
await updatePins.updatePins(pins, prev)
|
|
await api.ackEvent(event)
|
|
}))
|
|
|
|
|
|
|
|
sync.addTemporaryListener(as, "type:m.space.child", guard("m.space.child",
|
|
/**
|
|
* @param {Ty.Event.StateOuter<Ty.Event.M_Space_Child>} event
|
|
*/
|
|
async event => {
|
|
if (Array.isArray(event.content.via) && event.content.via.length) { // space child is being added
|
|
try {
|
|
// try to join if able, it's okay if it doesn't want, bot will still respond to invites
|
|
await api.joinRoom(event.state_key)
|
|
// if autojoined a child space, store it in invite (otherwise the child space will be impossible to use with self-service in the future)
|
|
const hierarchy = await api.getHierarchy(event.state_key, {limit: 1})
|
|
const roomProperties = hierarchy.rooms?.[0]
|
|
if (roomProperties?.room_id === event.state_key && roomProperties.room_type === "m.space" && roomProperties.name) {
|
|
db.prepare("INSERT OR IGNORE INTO invite (mxid, room_id, type, name, topic, avatar) VALUES (?, ?, ?, ?, ?, ?)")
|
|
.run(event.sender, event.state_key, roomProperties.room_type, roomProperties.name, roomProperties.topic, roomProperties.avatar_url)
|
|
await updateMemberCachePowerLevels(event.state_key) // store privileged users in member_cache so they are also allowed to perform self-service
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
}))
|
|
|
|
sync.addTemporaryListener(as, "type:m.room.member", guard("m.room.member",
|
|
/**
|
|
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Member>} event
|
|
*/
|
|
async event => {
|
|
if (event.state_key[0] !== "@") return
|
|
|
|
if (event.state_key === utils.bot) {
|
|
const upgraded = await roomUpgrade.onBotMembership(event, api, createRoom)
|
|
if (upgraded) return
|
|
}
|
|
|
|
if (event.content.membership === "invite" && event.state_key === utils.bot) {
|
|
// Supposed to be here already?
|
|
const guildID = select("guild_space", "guild_id", {space_id: event.room_id}).pluck().get()
|
|
if (guildID) {
|
|
await api.joinRoom(event.room_id)
|
|
return
|
|
}
|
|
|
|
// We were invited to a room. We should join, and register the invite details for future reference in web.
|
|
try {
|
|
var inviteRoomState = await api.getInviteState(event.room_id, event)
|
|
} catch (e) {
|
|
console.error(e)
|
|
return await api.leaveRoomWithReason(event.room_id, `I wasn't able to find out what this room is. Please report this as a bug. Check console for more details. (${e.toString()})`)
|
|
}
|
|
if (inviteRoomState?.encryption) return await api.leaveRoomWithReason(event.room_id, "Encrypted rooms are not supported for bridging. Please use an unencrypted room.")
|
|
if (!inviteRoomState?.name) return await api.leaveRoomWithReason(event.room_id, `Please only invite me to rooms that have a name/avatar set. Update the room details and reinvite.`)
|
|
await api.joinRoom(event.room_id)
|
|
db.prepare("REPLACE INTO invite (mxid, room_id, type, name, topic, avatar) VALUES (?, ?, ?, ?, ?, ?)").run(event.sender, event.room_id, inviteRoomState.type, inviteRoomState.name, inviteRoomState.topic, inviteRoomState.avatar)
|
|
if (inviteRoomState.avatar) utils.getPublicUrlForMxc(inviteRoomState.avatar) // make sure it's available in the media_proxy allowed URLs
|
|
await updateMemberCachePowerLevels(event.room_id) // store privileged users in member_cache so they are also allowed to perform self-service
|
|
}
|
|
|
|
if (event.content.membership === "leave" || event.content.membership === "ban") {
|
|
// Member is gone
|
|
// if Matrix member, data was cached in member_cache
|
|
db.prepare("DELETE FROM member_cache WHERE room_id = ? and mxid = ?").run(event.room_id, event.state_key)
|
|
// if Discord member (so kicked/banned by Matrix user), data was cached in sim_member
|
|
db.prepare("DELETE FROM sim_member WHERE room_id = ? and mxid = ?").run(event.room_id, event.state_key)
|
|
|
|
// Unregister room's use as a direct chat and/or an invite target if the bot itself left
|
|
if (event.state_key === utils.bot) {
|
|
db.prepare("DELETE FROM direct WHERE room_id = ?").run(event.room_id)
|
|
db.prepare("DELETE FROM invite WHERE room_id = ?").run(event.room_id)
|
|
}
|
|
}
|
|
|
|
if (utils.eventSenderIsFromDiscord(event.state_key)) return
|
|
|
|
const exists = select("channel_room", "room_id", {room_id: event.room_id}) ?? select("guild_space", "space_id", {space_id: event.room_id})
|
|
if (!exists) return // don't cache members in unbridged rooms
|
|
|
|
// Member is here
|
|
let {powers: {[event.state_key]: memberPower}, tombstone} = await utils.getEffectivePower(event.room_id, [event.state_key], api)
|
|
if (memberPower === Infinity) memberPower = tombstone // database storage compatibility
|
|
const displayname = event.content.displayname || null
|
|
const avatar_url = event.content.avatar_url
|
|
db.prepare("INSERT INTO member_cache (room_id, mxid, displayname, avatar_url, power_level) VALUES (?, ?, ?, ?, ?) ON CONFLICT DO UPDATE SET displayname = ?, avatar_url = ?, power_level = ?, missing_profile = NULL").run(
|
|
event.room_id, event.state_key,
|
|
displayname, avatar_url, memberPower,
|
|
displayname, avatar_url, memberPower
|
|
)
|
|
}))
|
|
|
|
sync.addTemporaryListener(as, "type:m.room.power_levels", guard("m.room.power_levels",
|
|
/**
|
|
* @param {Ty.Event.StateOuter<Ty.Event.M_Power_Levels>} event
|
|
*/
|
|
async event => {
|
|
if (event.state_key !== "") return
|
|
await updateMemberCachePowerLevels(event.room_id)
|
|
}))
|
|
|
|
/**
|
|
* @param {string} roomID
|
|
*/
|
|
async function updateMemberCachePowerLevels(roomID) {
|
|
const existingPower = select("member_cache", "mxid", {room_id: roomID}).pluck().all()
|
|
const {powerLevels, allCreators, tombstone} = await utils.getEffectivePower(roomID, [], api)
|
|
const newPower = powerLevels.users || {}
|
|
const newPowerUsers = Object.keys(newPower)
|
|
const relevantUsers = existingPower.concat(newPowerUsers).concat(allCreators)
|
|
for (const mxid of [...new Set(relevantUsers)]) {
|
|
const level = allCreators.includes(mxid) ? tombstone : newPower[mxid] ?? powerLevels.users_default ?? 0
|
|
db.prepare("INSERT INTO member_cache (room_id, mxid, power_level, missing_profile) VALUES (?, ?, ?, 1) ON CONFLICT DO UPDATE SET power_level = ?")
|
|
.run(roomID, mxid, level, level)
|
|
}
|
|
}
|
|
|
|
sync.addTemporaryListener(as, "type:m.room.tombstone", guard("m.room.tombstone",
|
|
/**
|
|
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Tombstone>} event
|
|
*/
|
|
async event => {
|
|
if (event.state_key !== "") return
|
|
if (!event.content.replacement_room) return
|
|
await roomUpgrade.onTombstone(event, api)
|
|
}))
|
|
|
|
sync.addTemporaryListener(as, "type:m.room.encryption", guard("m.room.encryption",
|
|
/**
|
|
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Encryption>} event
|
|
*/
|
|
async event => {
|
|
// Dramatically unbridge rooms if they become encrypted
|
|
if (event.state_key !== "") return
|
|
const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
|
|
if (!channelID) return
|
|
const channel = discord.channels.get(channelID)
|
|
if (!channel) return
|
|
await createRoom.unbridgeChannel(channel, channel["guild_id"], "Encrypted rooms are not supported. This room was removed from the bridge.")
|
|
}))
|
|
|
|
module.exports.stringifyErrorStack = stringifyErrorStack
|
|
module.exports.cleanErrorStack = cleanErrorStack
|
|
module.exports.sendError = sendError
|
|
module.exports.printError = printError
|