WIP: feature: threads'n'forums #74
2 changed files with 112 additions and 45 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 {discord, db, sync, as, select} = require("../passthrough")
|
||||||
const {tag} = require("@cloudrac3r/html-template-tag")
|
const {tag} = require("@cloudrac3r/html-template-tag")
|
||||||
const {Semaphore} = require("@chriscdn/promise-semaphore")
|
const {Semaphore} = require("@chriscdn/promise-semaphore")
|
||||||
|
const { bridgeThread, handleForums } = require("./converters/threads-and-forums")
|
||||||
|
|
||||||
/** @type {import("./actions/send-event")} */
|
/** @type {import("./actions/send-event")} */
|
||||||
const sendEvent = sync.require("./actions/send-event")
|
const sendEvent = sync.require("./actions/send-event")
|
||||||
|
|
@ -253,51 +254,6 @@ async event => {
|
||||||
await api.ackEvent(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")){
|
|
||||||
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: eventID,
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sync.addTemporaryListener(as, "type:m.sticker", guard("m.sticker",
|
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
|
* @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