forked from cadence/out-of-your-element
Compare commits
4 commits
3a74dfb78f
...
d52794e22c
| Author | SHA1 | Date | |
|---|---|---|---|
| d52794e22c | |||
| 86c58f169e | |||
| 10b6cf5bdb | |||
| b1513a6fd1 |
2 changed files with 120 additions and 46 deletions
111
src/m2d/converters/threads-and-forums.js
Normal file
111
src/m2d/converters/threads-and-forums.js
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
//@ts-check
|
||||
|
||||
/*
|
||||
* Misc. utils for transforming various Matrix events (eg. those sent in Forum-bridged channels; those sent) so that they're usable as threads, and for creating said threads.
|
||||
*/
|
||||
|
||||
const util = require("util")
|
||||
const Ty = require("../../types")
|
||||
const {discord, db, sync, as, select, from} = require("../../passthrough")
|
||||
const {tag} = require("@cloudrac3r/html-template-tag")
|
||||
const {Semaphore} = require("@chriscdn/promise-semaphore")
|
||||
const assert = require("assert").strict
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
|
||||
/** @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")
|
||||
|
||||
/**
|
||||
* @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File} event Used for determining the branching-point and the title; any relation data will be stripped and its room_id will mutate to the target thread-room, if one gets created.
|
||||
* @returns {Promise<boolean>} whether a thread-room was created
|
||||
*/
|
||||
async function bridgeThread(event) {
|
||||
/** @type {string} */ // @ts-ignore
|
||||
const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
|
||||
const channel = discord.channels.get(channelID)
|
||||
const guildID = channel?.["guild_id"]
|
||||
if (!guildID) return false; //Room not bridged? We don't care. It's a Matrix-native room, let Matrix users have standard Matrix-native threads there.
|
||||
|
||||
const threadEventID = event.content["m.relates_to"]?.event_id
|
||||
if (!threadEventID) throw new Error("There was an event sent inside SOME Matrix thread, but it lacked any information as to what thread it actually was!"); //An „ugly error” is justified because if something like this DOES happen, then that means that it should be reported to us, as there is some broken client out there that we should account for.
|
||||
const messageID = select("event_message", "message_id", {event_id: threadEventID}).pluck().get()
|
||||
if (!messageID) return false; //Message not bridged? Too bad! Discord users will just see normal replies, and Matrix uses won't get a thread-room. We COULD technically create a "headless" thread on Discord side and bridge it to a new thread-room, but that comes with a whole host of complications on its own (notably: what do we do if the message gets bridged later (by reaction emoji), and then hypothetically gets its own thread; and: getThreadRoomFromThreadEvent will have to be much more complex than a simple DB call (probably a whole new DB table would have to be created, just to hold these Matrix-branched-but-headless-on-Discord threads) because the simple „MX event --(db)--> Discord Message --(Discord spec)--> Discord thread” relation would no longer hold true), which may not be worth it, as an unbridged message in a bridged channel is already an edge-case (so it seems pointless to introduce a whole bunch of edgier-cases that handling this edge-case "properly" would bring).
|
||||
|
||||
try {
|
||||
event.room_id = await createRoom.ensureRoom((await discord.snow.channel.createThreadWithMessage(channelID, messageID, {name: computeName(event, await api.getEvent(event.room_id, threadEventID)).name})).id)
|
||||
return true;
|
||||
}
|
||||
catch (e){
|
||||
if (e.message?.includes("50024")){
|
||||
api.sendEvent(event.room_id, "m.room.message", {
|
||||
body: "Hey, please don't do that! This room is already a thread on Discord - trying to embed threads inside it will not work. The user will just see a regular reply.",
|
||||
"m.mentions": { "user_ids": [event.sender]},
|
||||
"m.relates_to": {
|
||||
event_id: threadEventID,
|
||||
is_falling_back: false,
|
||||
"m.in_reply_to": { event_id: event.event_id },
|
||||
rel_type: "m.thread"
|
||||
},
|
||||
msgtype: "m.text"
|
||||
})
|
||||
|
||||
return false;
|
||||
}
|
||||
else throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File} event
|
||||
* @returns {Promise<boolean>} true if a forum-thread-room was created
|
||||
*/
|
||||
async function handleForums(event) {
|
||||
if (event.content.body === "/thread") return false; //Let the help be shown normally
|
||||
|
||||
const row = from("channel_room").where({room_id: event.room_id}).select("channel_id").get()
|
||||
/** @type {string}*/ //@ts-ignore the possibility that it's undefined: get() will return back an undefined if it's fed one (so that's undefined-safe), and createThreadWithoutMessage() won't be reached because "undefined" is neither DiscordTypes.ChannelType.GuildMedia nor DiscordTypes.ChannelType.GuildForum, so the guard clause kicks in.
|
||||
let channelID = row?.channel_id
|
||||
const type = discord.channels.get(channelID)?.type
|
||||
if (type != DiscordTypes.ChannelType.GuildForum && type != DiscordTypes.ChannelType.GuildMedia) return false
|
||||
|
||||
const name = computeName(event)
|
||||
await discord.snow.channel.createThreadWithoutMessage(channelID, {name: name.name, type:11})
|
||||
if (!name.truncated) api.redactEvent(event.room_id, event.event_id) //Don't destroy people's texts - only remove if no truncation is guaranteed.
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File} from event
|
||||
* @param {Ty.Event.Outer<any> | null | false | undefined} fallback Reuses the "from" param value if empty.
|
||||
* @returns {{name: string, truncated: boolean}}
|
||||
*/
|
||||
function computeName(from, fallback=null){
|
||||
let name = from.content.body
|
||||
if (name.startsWith("/thread ") && name.length > 8) name = name.substring(8);
|
||||
else name = (fallback ? fallback : from).content.body;
|
||||
return name.length < 100 ? {name: name.replaceAll("\n", " "), truncated: false} : {name: name.slice(0, 96).replaceAll("\n", " ") + "...", truncated: true}
|
||||
}
|
||||
|
||||
module.exports.handleForums = handleForums
|
||||
module.exports.bridgeThread = bridgeThread
|
||||
|
|
@ -9,6 +9,7 @@ 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")
|
||||
const { bridgeThread, handleForums } = require("./converters/threads-and-forums")
|
||||
|
||||
/** @type {import("./actions/send-event")} */
|
||||
const sendEvent = sync.require("./actions/send-event")
|
||||
|
|
@ -210,34 +211,27 @@ sync.addTemporaryListener(as, "type:m.room.message", guard("m.room.message",
|
|||
*/
|
||||
async event => {
|
||||
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
||||
if (await handleForums(event)) return
|
||||
|
||||
let processCommands = true
|
||||
if (event.content["m.relates_to"]?.rel_type === "m.thread") {
|
||||
const toRedact = event.room_id
|
||||
/**@type {string|null} */
|
||||
let toRedact = event.room_id
|
||||
const bridgedTo = utils.getThreadRoomFromThreadEvent(event.content["m.relates_to"].event_id)
|
||||
processCommands = false
|
||||
|
||||
if (bridgedTo) event.room_id = bridgedTo;
|
||||
else if (await bridgeThread(event)) {
|
||||
else if (!await bridgeThread(event)) toRedact = null; //Don't remove anything, if there is nowhere to relocate it to.
|
||||
|
||||
if (toRedact) {
|
||||
api.redactEvent(toRedact, event.event_id)
|
||||
event.content["m.relates_to"] = undefined
|
||||
api.sendEvent(event.room_id, event.type, {...event.content, body: event.content.body+"\n ~ "+event.sender, formatted_body: event.content.formatted_body ? event.content.formatted_body+"<br> ~ "+event.sender :undefined })
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const messageResponses = await sendEvent.sendEvent(event)
|
||||
if (!messageResponses.length) return
|
||||
}
|
||||
catch (e){ //This had to have been caught outside the regular guard()->sendError() loop, otherwise commands wouldn't get processed.
|
||||
if (e.message?.includes("220001")) { //see: https://docs.discord.com/developers/topics/opcodes-and-status-codes
|
||||
if(!event.content.body.startsWith("/thread")) api.sendEvent(event.room_id, "m.room.message", {
|
||||
msgtype: "m.text",
|
||||
body: "You cannot send regular messages in rooms bridged to forum channels! Please create a /thread instead."
|
||||
})
|
||||
}
|
||||
else throw e
|
||||
}
|
||||
const messageResponses = await sendEvent.sendEvent(event)
|
||||
if (!messageResponses.length) return
|
||||
|
||||
if (event.type === "m.room.message" && event.content.msgtype === "m.text" && processCommands) {
|
||||
await matrixCommandHandler.parseAndExecute(
|
||||
|
|
@ -250,37 +244,6 @@ async event => {
|
|||
await api.ackEvent(event)
|
||||
}))
|
||||
|
||||
/**
|
||||
* @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File} event Used for determining the branching-point and the title; any relation data will be stripped and its room_id will mutate to the target thread-room, if one gets created.
|
||||
* @returns {Promise<boolean>} whether a thread-room was created
|
||||
*/
|
||||
async function bridgeThread(event) {
|
||||
/** @type {string} */ // @ts-ignore
|
||||
const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
|
||||
const channel = discord.channels.get(channelID)
|
||||
const guildID = channel?.["guild_id"]
|
||||
if (!guildID) return false; //Room not bridged? We don't care. It's a Matrix-native room, let Matrix users have standard Matrix-native threads there.
|
||||
|
||||
const eventID = event.content["m.relates_to"]?.event_id
|
||||
if (!eventID) throw new Error("There was an event sent inside SOME Matrix thread, but it lacked any information as to what thread it actually was!"); //An „ugly error” is justified because if something like this DOES happen, then that means that it should be reported to us, as there is some broken client out there that we should account for.
|
||||
const messageID = select("event_message", "message_id", {event_id: eventID}).pluck().get()
|
||||
if (!messageID) return false; //Message not bridged? Too bad! Discord users will just see normal replies, and Matrix uses won't get a thread-room. We COULD technically create a "headless" thread on Discord side and bridge it to a new thread-room, but that comes with a whole host of complications on its own (notably: what do we do if the message gets bridged later (by reaction emoji), and then hypothetically gets its own thread; and: getThreadRoomFromThreadEvent will have to be much more complex than a simple DB call (probably a whole new DB table would have to be created, just to hold these Matrix-branched-but-headless-on-Discord threads) because the simple „MX event --(db)--> Discord Message --(Discord spec)--> Discord thread” relation would no longer hold true), which may not be worth it, as an unbridged message in a bridged channel is already an edge-case (so it seems pointless to introduce a whole bunch of edgier-cases that handling this edge-case "properly" would bring).
|
||||
|
||||
let name = event.content.body
|
||||
if (name.startsWith("/thread ") && name.length > 8) name = name.substring(8);
|
||||
else name = (await api.getEvent(event.room_id, eventID)).content.body;
|
||||
name = name.length < 100 ? name.replaceAll("\n", " ") : name.slice(0, 96).replaceAll("\n", " ") + "..."
|
||||
|
||||
try {
|
||||
event.room_id = await createRoom.ensureRoom((await discord.snow.channel.createThreadWithMessage(channelID, messageID, {name})).id)
|
||||
return true;
|
||||
}
|
||||
catch (e){
|
||||
if (e.message?.includes("50024")) return false; //Tried to created a thread in a thread? Too bad! Discord users will just see normal replies, and Matrix uses won't get a thread-room. Same case as for message-not-bridged, except there at least exists a HYPOTHETICAL solution (just one so unwieldly that it's nonsensical to dedicate resources to), wheres here I don't know what could possibly be done at all.
|
||||
else throw e; //In here (unlike in matrix-command-handler.js), there are much fewer things that could "intentionally" go wrong (both thread double-creation and too-long names shouldn't be possible due to earlier checks). As such, if anything breaks, it should be reported to OOYE for further investigation, which the user should do when encountering an "ugly error" (if they follow the „every error should be reported” directive), so this is re-thrown as-is (no stacktrace-breaking exception wrapping) to be turned into such an "ugly error" upstream.
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue