i dont even know what this PR is supposed to be about anymore, everyone lost the plot somewhere in the middle of act 2 #74
19 changed files with 691 additions and 328 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -1,17 +1,18 @@
|
||||||
# Secrets
|
# Personal
|
||||||
config.js
|
config.js
|
||||||
registration.yaml
|
registration.yaml
|
||||||
ooye.db*
|
ooye.db*
|
||||||
events.db*
|
events.db*
|
||||||
backfill.db*
|
backfill.db*
|
||||||
custom-webroot
|
custom-webroot
|
||||||
|
icon.svg
|
||||||
|
.devcontainer
|
||||||
|
|
||||||
# Automatically generated
|
# Automatically generated
|
||||||
node_modules
|
node_modules
|
||||||
coverage
|
coverage
|
||||||
test/res/*
|
test/res/*
|
||||||
!test/res/lottie*
|
!test/res/lottie*
|
||||||
icon.svg
|
|
||||||
*~
|
*~
|
||||||
.#*
|
.#*
|
||||||
\#*#
|
\#*#
|
||||||
|
|
|
||||||
9
docs/threads-as-rooms.md
Normal file
9
docs/threads-as-rooms.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
I thought pretty hard about it and I opted to make threads separate rooms because
|
||||||
|
|
||||||
|
1. parity: discord has separate things like permissions and pins for threads, matrix cannot do this at all unless the thread is a separate room
|
||||||
|
2. usage styles: most discord threads I've seen tend to be long-lived, spanning months or years, which isn't suited to matrix because of the timeline
|
||||||
|
- I'm in a discord thread for posting photos of food that gets a couple posts a week and has a timeline going back to 2023
|
||||||
|
3. the timeline: if a matrix room has threads, and you want to scroll back through the timeline of a room OR of one of its threads, the timeline is merged, so you have to download every message linearised and throw them away if they aren't part of the thread you're looking through. it's bad for threads and it's bad for the main room
|
||||||
|
4. it is also very very complex for clients to implement read receipts and typing indicators correctly for the merged timeline. if your client doesn't implement this, or doesn't do it correctly, you have a bad experience. many clients don't. element seems to have done it well enough, but is an exception
|
||||||
|
|
||||||
|
overall in my view, threads-as-rooms has better parity and fewer downsides over native threads. but if there are things you don't like about this approach, I'm happy to discuss and see if we can improve them.
|
||||||
|
|
@ -35,6 +35,7 @@ const PRIVACY_ENUMS = {
|
||||||
ROOM_HISTORY_VISIBILITY: ["shared", "shared", "world_readable"], // any events sent after <value> are visible, but for world_readable anybody can read without even joining
|
ROOM_HISTORY_VISIBILITY: ["shared", "shared", "world_readable"], // any events sent after <value> are visible, but for world_readable anybody can read without even joining
|
||||||
GUEST_ACCESS: ["can_join", "forbidden", "forbidden"], // whether guests can join space if other conditions are met
|
GUEST_ACCESS: ["can_join", "forbidden", "forbidden"], // whether guests can join space if other conditions are met
|
||||||
SPACE_JOIN_RULES: ["invite", "public", "public"],
|
SPACE_JOIN_RULES: ["invite", "public", "public"],
|
||||||
|
/** @type {import("../../types").JoinRule[]} */
|
||||||
ROOM_JOIN_RULES: ["restricted", "public", "public"]
|
ROOM_JOIN_RULES: ["restricted", "public", "public"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,12 +64,13 @@ function convertNameAndTopic(channel, guild, customName) {
|
||||||
const chosenName = customName || (channelPrefix + channel.name);
|
const chosenName = customName || (channelPrefix + channel.name);
|
||||||
const maybeTopicWithPipe = channel.topic ? ` | ${channel.topic}` : '';
|
const maybeTopicWithPipe = channel.topic ? ` | ${channel.topic}` : '';
|
||||||
const maybeTopicWithNewlines = channel.topic ? `${channel.topic}\n\n` : '';
|
const maybeTopicWithNewlines = channel.topic ? `${channel.topic}\n\n` : '';
|
||||||
|
const maybeWithin = parentChannel ? `Within: ${parentChannel.name} (ID: ${parentChannel.id})\n` : '';
|
||||||
const channelIDPart = `Channel ID: ${channel.id}`;
|
const channelIDPart = `Channel ID: ${channel.id}`;
|
||||||
const guildIDPart = `Guild ID: ${guild.id}`;
|
const guildIDPart = `Guild ID: ${guild.id}`;
|
||||||
|
|
||||||
const convertedTopic = customName
|
const convertedTopic = customName
|
||||||
? `#${channel.name}${maybeTopicWithPipe}\n\n${channelIDPart}\n${guildIDPart}`
|
? `#${channel.name}${maybeTopicWithPipe}\n\n${maybeWithin}${channelIDPart}\n${guildIDPart}`
|
||||||
: `${maybeTopicWithNewlines}${channelIDPart}\n${guildIDPart}`;
|
: `${maybeTopicWithNewlines}${maybeWithin}${channelIDPart}\n${guildIDPart}`;
|
||||||
|
|
||||||
return [chosenName, convertedTopic];
|
return [chosenName, convertedTopic];
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +89,7 @@ async function channelToKState(channel, guild, di) {
|
||||||
const guildSpaceID = await createSpace.ensureSpace(guild)
|
const guildSpaceID = await createSpace.ensureSpace(guild)
|
||||||
/** Used as the literal parent on Matrix, for categorisation. Will be the same as `guildSpaceID` unless it's a forum channel's thread, in which case a different space is used to group those threads. */
|
/** Used as the literal parent on Matrix, for categorisation. Will be the same as `guildSpaceID` unless it's a forum channel's thread, in which case a different space is used to group those threads. */
|
||||||
let parentSpaceID = guildSpaceID
|
let parentSpaceID = guildSpaceID
|
||||||
if (parentChannel?.type === DiscordTypes.ChannelType.GuildForum) {
|
if (parentChannel?.type === DiscordTypes.ChannelType.GuildForum || parentChannel?.type === DiscordTypes.ChannelType.GuildMedia) { //TODO: Once Ellie's and Guzio's MSC for room-in-room embedding starts being implemented, make this check for whether THIS channel (not its parent) is a thread of ANY type (not just threads in forum/media channels) - thus making it so that threads always appear embedded under their parent.
|
||||||
parentSpaceID = await ensureRoom(channel.parent_id)
|
parentSpaceID = await ensureRoom(channel.parent_id)
|
||||||
assert(typeof parentSpaceID === "string")
|
assert(typeof parentSpaceID === "string")
|
||||||
}
|
}
|
||||||
|
|
@ -110,7 +112,7 @@ async function channelToKState(channel, guild, di) {
|
||||||
let history_visibility = PRIVACY_ENUMS.ROOM_HISTORY_VISIBILITY[privacyLevel]
|
let history_visibility = PRIVACY_ENUMS.ROOM_HISTORY_VISIBILITY[privacyLevel]
|
||||||
if (channel["thread_metadata"]) history_visibility = "world_readable"
|
if (channel["thread_metadata"]) history_visibility = "world_readable"
|
||||||
|
|
||||||
/** @type {{join_rule: string, allow?: any}} */
|
/** @type {{join_rule: import("../../types").JoinRule, allow?: {type: "m.room_membership", room_id: string}[]}} */
|
||||||
let join_rules = {
|
let join_rules = {
|
||||||
join_rule: "restricted",
|
join_rule: "restricted",
|
||||||
allow: [{
|
allow: [{
|
||||||
|
|
@ -118,6 +120,13 @@ async function channelToKState(channel, guild, di) {
|
||||||
room_id: guildSpaceID
|
room_id: guildSpaceID
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
if (guildSpaceID !== parentSpaceID) {
|
||||||
|
//@ts-ignore - join_rules.allow most certainly IS defined because we literally define it ~5 lines earlier
|
||||||
|
join_rules.allow[1] = {
|
||||||
|
type: "m.room_membership",
|
||||||
|
room_id: parentSpaceID
|
||||||
|
}
|
||||||
|
}
|
||||||
if (PRIVACY_ENUMS.ROOM_JOIN_RULES[privacyLevel] !== "restricted") {
|
if (PRIVACY_ENUMS.ROOM_JOIN_RULES[privacyLevel] !== "restricted") {
|
||||||
join_rules = {join_rule: PRIVACY_ENUMS.ROOM_JOIN_RULES[privacyLevel]}
|
join_rules = {join_rule: PRIVACY_ENUMS.ROOM_JOIN_RULES[privacyLevel]}
|
||||||
}
|
}
|
||||||
|
|
@ -194,7 +203,7 @@ async function channelToKState(channel, guild, di) {
|
||||||
if (hasCustomTopic) delete channelKState["m.room.topic/"]
|
if (hasCustomTopic) delete channelKState["m.room.topic/"]
|
||||||
|
|
||||||
// Make voice channels be a Matrix voice room (MSC3417)
|
// Make voice channels be a Matrix voice room (MSC3417)
|
||||||
if (channel.type === DiscordTypes.ChannelType.GuildVoice) {
|
if (channel.type === DiscordTypes.ChannelType.GuildVoice || channel.type === DiscordTypes.ChannelType.GuildStageVoice) {
|
||||||
creationContent.type = "org.matrix.msc3417.call"
|
creationContent.type = "org.matrix.msc3417.call"
|
||||||
channelKState["org.matrix.msc3401.call/"] = {
|
channelKState["org.matrix.msc3401.call/"] = {
|
||||||
"m.intent": "m.room",
|
"m.intent": "m.room",
|
||||||
|
|
@ -439,12 +448,12 @@ async function _syncRoom(channelID, shouldActuallySync) {
|
||||||
return roomID
|
return roomID
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ensures the room exists. If it doesn't, creates the room with an accurate initial state. Please check that a channel_room entry exists or guild autocreate = 1 before calling this. */
|
/** Ensures the room exists. If it doesn't, creates the room with an accurate initial state. Before calling this, please make sure that: a channel_room entry exists, guild autocreate = 1, or you're operating on a thread.*/
|
||||||
function ensureRoom(channelID) {
|
function ensureRoom(channelID) {
|
||||||
return _syncRoom(channelID, false)
|
return _syncRoom(channelID, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Actually syncs. Gets all room state from the homeserver in order to diff, and uploads the icon to mxc if it has changed. Please check that a channel_room entry exists or guild autocreate = 1 before calling this. */
|
/** Actually syncs. Gets all room state from the homeserver in order to diff, and uploads the icon to mxc if it has changed. Before calling this, please make sure that: a channel_room entry exists, guild autocreate = 1, or you're operating on a thread.*/
|
||||||
function syncRoom(channelID) {
|
function syncRoom(channelID) {
|
||||||
return _syncRoom(channelID, true)
|
return _syncRoom(channelID, true)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
const assert = require("assert").strict
|
|
||||||
|
|
||||||
const passthrough = require("../../passthrough")
|
const passthrough = require("../../passthrough")
|
||||||
const {discord, sync, db, select} = passthrough
|
const {sync, select} = passthrough
|
||||||
/** @type {import("../../matrix/utils")} */
|
/** @type {import("../../matrix/utils")} */
|
||||||
const mxUtils = sync.require("../../matrix/utils")
|
const mxUtils = sync.require("../../matrix/utils")
|
||||||
const {reg} = require("../../matrix/read-registration.js")
|
const {reg} = require("../../matrix/read-registration.js")
|
||||||
|
|
@ -19,24 +17,32 @@ const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex))
|
||||||
*/
|
*/
|
||||||
async function threadToAnnouncement(parentRoomID, threadRoomID, creatorMxid, thread, di) {
|
async function threadToAnnouncement(parentRoomID, threadRoomID, creatorMxid, thread, di) {
|
||||||
const branchedFromEventID = select("event_message", "event_id", {message_id: thread.id}).pluck().get()
|
const branchedFromEventID = select("event_message", "event_id", {message_id: thread.id}).pluck().get()
|
||||||
/** @type {{"m.mentions"?: any, "m.in_reply_to"?: any}} */
|
const ellieMode = false //See: https://matrix.to/#/!PuFmbgRjaJsAZTdSja:cadence.moe?via=cadence.moe&via=chat.untitledzero.dev&via=guziohub.ovh - TL;DR: Ellie's idea was to not leave any sign of Matrix threads existing, while Guzio preferred that the link to created thread-rooms stay within the Matrix thread (to better approximate Discord UI on compatible clients). This settings-option-but-not-really (it's not meant to be changed by end users unless they know what they're doing) would let Cadence switch between both approaches over some time period, to test the feeling of both, and finally land on whichever UX she prefers best. TODO: Remove this toggle (and make the chosen solution permanent) once those tests conclude.
|
||||||
|
|
||||||
|
/** @type {{"m.mentions"?: any, "m.relates_to"?: {event_id?: string, is_falling_back?: boolean, "m.in_reply_to"?: {event_id: string}, rel_type?: "m.replace"|"m.thread"}}} */
|
||||||
const context = {}
|
const context = {}
|
||||||
|
let suffix = ""
|
||||||
if (branchedFromEventID) {
|
if (branchedFromEventID) {
|
||||||
// Need to figure out who sent that event...
|
// Need to figure out who sent that event...
|
||||||
const event = await di.api.getEvent(parentRoomID, branchedFromEventID)
|
const event = await di.api.getEvent(parentRoomID, branchedFromEventID)
|
||||||
|
suffix = "\n[Note: You really should move the conversation to that room, rather than continuing to reply via a Matrix thread. Any messages sent in threads will be DELETED and instead moved to that room by the bot, which makes it the author and strips you from control (edits/deletions) over your own message!]"
|
||||||
context["m.relates_to"] = {"m.in_reply_to": {event_id: event.event_id}}
|
context["m.relates_to"] = {"m.in_reply_to": {event_id: event.event_id}}
|
||||||
if (event.sender && !userRegex.some(rx => event.sender.match(rx))) context["m.mentions"] = {user_ids: [event.sender]}
|
if (event.sender && !userRegex.some(rx => event.sender.match(rx))) context["m.mentions"] = {user_ids: [event.sender]}
|
||||||
|
if (!ellieMode) {
|
||||||
|
//...And actually branch from that event (if configured to do so)
|
||||||
|
suffix = "\n[Note: You really should continue the conversation in that room, rather than in this Matrix thread. Any messages sent here will be DELETED and instead moved there by the bot, which makes it the author and strips you from control (edits/deletions) over your own message!]"
|
||||||
|
context["m.relates_to"] = {"m.in_reply_to": {event_id: event.event_id}, is_falling_back:false, event_id: event.event_id, rel_type: "m.thread"}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const msgtype = creatorMxid ? "m.emote" : "m.text"
|
const msgtype = creatorMxid ? "m.emote" : "m.text"
|
||||||
const template = creatorMxid ? "started a thread:" : "Thread started:"
|
const template = creatorMxid ? "started a thread called" : "New thread started:"
|
||||||
const via = await mxUtils.getViaServersQuery(threadRoomID, di.api)
|
const via = await mxUtils.getViaServersQuery(threadRoomID, di.api)
|
||||||
let body = `${template} ${thread.name} https://matrix.to/#/${threadRoomID}?${via.toString()}`
|
let body = `${template} \"${thread.name}\" in room: https://matrix.to/#/${threadRoomID}?${via.toString()}${suffix}`
|
||||||
|
|
||||||
return {
|
return {
|
||||||
msgtype,
|
msgtype,
|
||||||
body,
|
body,
|
||||||
"m.mentions": {},
|
|
||||||
...context
|
...context
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,7 @@ test("thread2announcement: no known creator, no branched from event", async t =>
|
||||||
}, {api: viaApi})
|
}, {api: viaApi})
|
||||||
t.deepEqual(content, {
|
t.deepEqual(content, {
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
body: "Thread started: test thread https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org",
|
body: "New thread started: \"test thread\" in room: https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org",
|
||||||
"m.mentions": {}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -61,8 +60,7 @@ test("thread2announcement: known creator, no branched from event", async t => {
|
||||||
}, {api: viaApi})
|
}, {api: viaApi})
|
||||||
t.deepEqual(content, {
|
t.deepEqual(content, {
|
||||||
msgtype: "m.emote",
|
msgtype: "m.emote",
|
||||||
body: "started a thread: test thread https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org",
|
body: "started a thread called \"test thread\" in room: https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org",
|
||||||
"m.mentions": {}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -85,12 +83,14 @@ test("thread2announcement: no known creator, branched from discord event", async
|
||||||
})
|
})
|
||||||
t.deepEqual(content, {
|
t.deepEqual(content, {
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
body: "Thread started: test thread https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org",
|
body: "New thread started: \"test thread\" in room: https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org\n[Note: You really should continue the conversation in that room, rather than in this Matrix thread. Any messages sent here will be DELETED and instead moved there by the bot, which makes it the author and strips you from control (edits/deletions) over your own message!]",
|
||||||
"m.mentions": {},
|
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
|
"event_id": "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg",
|
||||||
|
"is_falling_back": false,
|
||||||
"m.in_reply_to": {
|
"m.in_reply_to": {
|
||||||
event_id: "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg"
|
"event_id": "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg",
|
||||||
}
|
},
|
||||||
|
"rel_type": "m.thread",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -114,12 +114,14 @@ test("thread2announcement: known creator, branched from discord event", async t
|
||||||
})
|
})
|
||||||
t.deepEqual(content, {
|
t.deepEqual(content, {
|
||||||
msgtype: "m.emote",
|
msgtype: "m.emote",
|
||||||
body: "started a thread: test thread https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org",
|
body: "started a thread called \"test thread\" in room: https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org\n[Note: You really should continue the conversation in that room, rather than in this Matrix thread. Any messages sent here will be DELETED and instead moved there by the bot, which makes it the author and strips you from control (edits/deletions) over your own message!]",
|
||||||
"m.mentions": {},
|
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
|
"event_id": "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg",
|
||||||
|
"is_falling_back": false,
|
||||||
"m.in_reply_to": {
|
"m.in_reply_to": {
|
||||||
event_id: "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg"
|
"event_id": "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg",
|
||||||
}
|
},
|
||||||
|
"rel_type": "m.thread",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -143,14 +145,51 @@ test("thread2announcement: no known creator, branched from matrix event", async
|
||||||
})
|
})
|
||||||
t.deepEqual(content, {
|
t.deepEqual(content, {
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
body: "Thread started: test thread https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org",
|
body: "New thread started: \"test thread\" in room: https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org\n[Note: You really should continue the conversation in that room, rather than in this Matrix thread. Any messages sent here will be DELETED and instead moved there by the bot, which makes it the author and strips you from control (edits/deletions) over your own message!]",
|
||||||
"m.mentions": {
|
"m.mentions": {
|
||||||
user_ids: ["@cadence:cadence.moe"]
|
user_ids: ["@cadence:cadence.moe"]
|
||||||
},
|
},
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
|
"event_id": "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4",
|
||||||
|
"is_falling_back": false,
|
||||||
"m.in_reply_to": {
|
"m.in_reply_to": {
|
||||||
event_id: "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4"
|
"event_id": "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4",
|
||||||
}
|
},
|
||||||
|
"rel_type": "m.thread",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("thread2announcement: known creator, branched from matrix event", async t => {
|
||||||
|
const content = await threadToAnnouncement("!kLRqKKUQXcibIMtOpl:cadence.moe", "!thread", "@_ooye_crunch_god:cadence.moe", {
|
||||||
|
name: "test thread",
|
||||||
|
id: "1128118177155526666"
|
||||||
|
}, {
|
||||||
|
api: {
|
||||||
|
getEvent: mockGetEvent(t, "!kLRqKKUQXcibIMtOpl:cadence.moe", "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4", {
|
||||||
|
type: "m.room.message",
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "so can you reply to my webhook uwu"
|
||||||
|
},
|
||||||
|
sender: "@cadence:cadence.moe"
|
||||||
|
}),
|
||||||
|
...viaApi
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.deepEqual(content, {
|
||||||
|
msgtype: "m.emote",
|
||||||
|
body: "started a thread called \"test thread\" in room: https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org\n[Note: You really should continue the conversation in that room, rather than in this Matrix thread. Any messages sent here will be DELETED and instead moved there by the bot, which makes it the author and strips you from control (edits/deletions) over your own message!]",
|
||||||
|
"m.mentions": {
|
||||||
|
user_ids: ["@cadence:cadence.moe"]
|
||||||
|
},
|
||||||
|
"m.relates_to": {
|
||||||
|
"event_id": "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4",
|
||||||
|
"is_falling_back": false,
|
||||||
|
"m.in_reply_to": {
|
||||||
|
"event_id": "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4",
|
||||||
|
},
|
||||||
|
"rel_type": "m.thread",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ module.exports = {
|
||||||
const channelID = thread.parent_id || undefined
|
const channelID = thread.parent_id || undefined
|
||||||
const parentRoomID = select("channel_room", "room_id", {channel_id: channelID}).pluck().get()
|
const parentRoomID = select("channel_room", "room_id", {channel_id: channelID}).pluck().get()
|
||||||
if (!parentRoomID) return // Not interested in a thread if we aren't interested in its wider channel (won't autocreate)
|
if (!parentRoomID) return // Not interested in a thread if we aren't interested in its wider channel (won't autocreate)
|
||||||
const threadRoomID = await createRoom.syncRoom(thread.id) // Create room (will share the same inflight as the initial message to the thread)
|
const threadRoomID = await createRoom.ensureRoom(thread.id)
|
||||||
await announceThread.announceThread(parentRoomID, threadRoomID, thread)
|
await announceThread.announceThread(parentRoomID, threadRoomID, thread)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,14 +39,20 @@ async function resolvePendingFiles(message) {
|
||||||
if ("key" in p) {
|
if ("key" in p) {
|
||||||
// Encrypted file
|
// Encrypted file
|
||||||
const d = crypto.createDecipheriv("aes-256-ctr", Buffer.from(p.key, "base64url"), Buffer.from(p.iv, "base64url"))
|
const d = crypto.createDecipheriv("aes-256-ctr", Buffer.from(p.key, "base64url"), Buffer.from(p.iv, "base64url"))
|
||||||
await api.getMedia(p.mxc).then(res => stream.Readable.fromWeb(res.body).pipe(d))
|
await api.getMedia(p.mxc).then(res => stream.Readable.fromWeb(
|
||||||
|
// @ts-ignore
|
||||||
|
res.body
|
||||||
|
).pipe(d))
|
||||||
return {
|
return {
|
||||||
name: p.name,
|
name: p.name,
|
||||||
file: d
|
file: d
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Unencrypted file
|
// Unencrypted file
|
||||||
const body = await api.getMedia(p.mxc).then(res => stream.Readable.fromWeb(res.body))
|
const body = await api.getMedia(p.mxc).then(res => stream.Readable.fromWeb(
|
||||||
|
// @ts-ignore
|
||||||
|
res.body
|
||||||
|
))
|
||||||
return {
|
return {
|
||||||
name: p.name,
|
name: p.name,
|
||||||
file: body
|
file: body
|
||||||
|
|
|
||||||
|
|
@ -471,6 +471,7 @@ async function checkWrittenMentions(content, senderMxid, roomID, guild, di) {
|
||||||
// @ts-ignore - typescript doesn't know about indices yet
|
// @ts-ignore - typescript doesn't know about indices yet
|
||||||
content: content.slice(0, writtenMentionMatch.indices[1][0]-1) + `@everyone` + content.slice(writtenMentionMatch.indices[1][1]),
|
content: content.slice(0, writtenMentionMatch.indices[1][0]-1) + `@everyone` + content.slice(writtenMentionMatch.indices[1][1]),
|
||||||
ensureJoined: [],
|
ensureJoined: [],
|
||||||
|
/**@type {DiscordTypes.AllowedMentionsTypes[]}*/ // @ts-ignore - TypeScript is for whatever reason conviced that "everyone" cannot be assigned to AllowedMentionsTypes, but if you „Go to Definition”, you'll see that "everyone" is a valid enum value.
|
||||||
allowedMentionsParse: ["everyone"],
|
allowedMentionsParse: ["everyone"],
|
||||||
allowedMentionsUsers: []
|
allowedMentionsUsers: []
|
||||||
}
|
}
|
||||||
|
|
@ -545,6 +546,7 @@ async function getL1L2ReplyLine(called = false) {
|
||||||
async function eventToMessage(event, guild, channel, di) {
|
async function eventToMessage(event, guild, channel, di) {
|
||||||
let displayName = event.sender
|
let displayName = event.sender
|
||||||
let avatarURL = undefined
|
let avatarURL = undefined
|
||||||
|
/**@type {DiscordTypes.AllowedMentionsTypes[]}*/ // @ts-ignore - TypeScript is for whatever reason conviced that neither "users" no "roles" cannot be assigned to AllowedMentionsTypes, but if you „Go to Definition”, you'll see that both are valid enum values.
|
||||||
const allowedMentionsParse = ["users", "roles"]
|
const allowedMentionsParse = ["users", "roles"]
|
||||||
const allowedMentionsUsers = []
|
const allowedMentionsUsers = []
|
||||||
/** @type {string[]} */
|
/** @type {string[]} */
|
||||||
|
|
|
||||||
106
src/m2d/converters/threads-and-forums.js
Normal file
106
src/m2d/converters/threads-and-forums.js
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
//@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 Ty = require("../../types")
|
||||||
|
const {discord, sync, select, from} = require("../../passthrough")
|
||||||
|
const DiscordTypes = require("discord-api-types/v10")
|
||||||
|
|
||||||
|
/** @type {import("../../matrix/api")}) */
|
||||||
|
const api = sync.require("../../matrix/api")
|
||||||
|
/** @type {import("../../d2m/actions/create-room")} */
|
||||||
|
const createRoom = sync.require("../../d2m/actions/create-room")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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")){ //see: https://docs.discord.com/developers/topics/opcodes-and-status-codes
|
||||||
|
api.sendEvent(event.room_id, "m.room.message", {
|
||||||
|
body: "Hey, please don't do that! This room is already a thread on Discord (or it could also be a voice-chat or something adjacent) - trying to embed threads inside it, like you just did, will not work. DC users will just see a regular reply, which is distracting and also probably not what you want.",
|
||||||
|
"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>} whether 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 channel = discord.channels.get(channelID)
|
||||||
|
if (channel?.type != DiscordTypes.ChannelType.GuildForum && channel?.type != DiscordTypes.ChannelType.GuildMedia) return false
|
||||||
|
|
||||||
|
const name = computeName(event)
|
||||||
|
let resetNeeded = false
|
||||||
|
try {
|
||||||
|
if(channel.flags && channel.flags & DiscordTypes.ChannelFlags.RequireTag){
|
||||||
|
await discord.snow.channel.updateChannel(channelID, {flags:(channel.flags^DiscordTypes.ChannelFlags.RequireTag)}, "Temporary override of tagging requirements because Matrix threads that can't be tagged yet.")
|
||||||
|
resetNeeded = true
|
||||||
|
}
|
||||||
|
//@ts-ignore the presence of message:{} and the absence of type:11 - that's intended for threads in Forum channels (*and no, createThreadWithMessage wouldn't work here, either - that one takes an ALREADY EXISTING message ID, whereas this needs a new message)
|
||||||
|
await discord.snow.channel.createThreadWithoutMessage(channelID, {name: name.name, message:{content:"**Created by: `"+ event.sender +"`**"}})
|
||||||
|
if (!name.truncated) api.redactEvent(event.room_id, event.event_id) //Don't destroy people's texts - only remove if no truncation is guaranteed.
|
||||||
|
if (resetNeeded) discord.snow.channel.updateChannel(channelID, {flags:channel.flags}, "Restoring flags to their original state.")
|
||||||
|
}
|
||||||
|
catch (e){
|
||||||
|
if (e.message?.includes("50013")){
|
||||||
|
api.sendEvent(event.room_id, "m.room.message", {
|
||||||
|
body: "You can't create threads in this forum right now! This forum is configured to require tags on post (Matrix users can't yet use tags yet), and OOYE doesn't have the permission to edit this channel on Discord (needed to bypass the requirement of tags). Unless this is intentional (see room description - the admins may have left a note), please ask someone on the Discord side to either grant OOYE the necessary permissions, or to remove tagging requirements.",
|
||||||
|
msgtype: "m.text"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else throw e
|
||||||
|
}
|
||||||
|
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")
|
||||||
|
|
@ -156,6 +157,15 @@ async function sendError(roomID, source, type, e, payload) {
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps the function with an automated error catching and reporting mechanism
|
||||||
|
* @template {Ty.Event.Outer<any>} EVENT The event that the wrapped function processes, its first argument.
|
||||||
|
* @template {[]} ARGS Other arguments of the wrapped function
|
||||||
|
* @template RETURNS The output of the wrapped function
|
||||||
|
* @param {string} type Type of the event, during the processing of which the error may occur.
|
||||||
|
* @param {(event: EVENT, ...args: ARGS)=>RETURNS|Promise<RETURNS>} fn Function to wrap
|
||||||
|
* @returns {(event: EVENT, ...args: ARGS)=>Promise<RETURNS|undefined>} Wrapped function
|
||||||
|
*/
|
||||||
function guard(type, fn) {
|
function guard(type, fn) {
|
||||||
return async function(event, ...args) {
|
return async function(event, ...args) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -205,12 +215,35 @@ sync.addTemporaryListener(as, "type:m.room.message", guard("m.room.message",
|
||||||
*/
|
*/
|
||||||
async event => {
|
async event => {
|
||||||
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
||||||
|
if (await handleForums(event)) return
|
||||||
|
|
||||||
|
let processCommands = true
|
||||||
|
if (event.content["m.relates_to"]?.rel_type === "m.thread") {
|
||||||
|
/**@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)) 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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const messageResponses = await sendEvent.sendEvent(event)
|
const messageResponses = await sendEvent.sendEvent(event)
|
||||||
if (!messageResponses.length) return
|
if (!messageResponses.length) return
|
||||||
if (event.type === "m.room.message" && event.content.msgtype === "m.text") {
|
|
||||||
// @ts-ignore
|
if (event.type === "m.room.message" && event.content.msgtype === "m.text" && processCommands) {
|
||||||
await matrixCommandHandler.execute(event)
|
await matrixCommandHandler.parseAndExecute(
|
||||||
|
// @ts-ignore - TypeScript doesn't know that the event.content.msgtype === "m.text" check ensures that event isn't of type Ty.Event.Outer_M_Room_Message_File (which, indeed, wouldn't fit here)
|
||||||
|
event
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
retrigger.messageFinishedBridging(event.event_id)
|
retrigger.messageFinishedBridging(event.event_id)
|
||||||
await api.ackEvent(event)
|
await api.ackEvent(event)
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
|
|
@ -261,8 +261,65 @@ const commands = [{
|
||||||
body: "This command creates a thread on Discord. But you aren't allowed to do this, because if you were a Discord user, you wouldn't have the Create Public Threads permission."
|
body: "This command creates a thread on Discord. But you aren't allowed to do this, because if you were a Discord user, you wouldn't have the Create Public Threads permission."
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const relation = event.content["m.relates_to"]
|
||||||
|
let isFallingBack = false;
|
||||||
|
let branchedFromMxEvent = relation?.["m.in_reply_to"]?.event_id // By default, attempt to branch the thread from the message to which /thread was replying.
|
||||||
|
if (relation?.rel_type === "m.thread") branchedFromMxEvent = relation?.event_id // If /thread was sent inside a Matrix thread, attempt to branch the Discord thread from the message, which that Matrix thread already is branching from.
|
||||||
|
if (!branchedFromMxEvent){
|
||||||
|
branchedFromMxEvent = event.event_id // If /thread wasn't replying to anything (ie. branchedFromMxEvent was undefined at initial assignment), or if the event was somehow malformed (in such a way that that - one way or another - branchedFromMxEvent ended up being undefined, even if according to the spec it shouldn't), branch the thread from the /thread command-message that created it.
|
||||||
|
isFallingBack = true;
|
||||||
|
}
|
||||||
|
const branchedFromDiscordMessage = select("event_message", "message_id", {event_id: branchedFromMxEvent}).pluck().get()
|
||||||
|
|
||||||
await discord.snow.channel.createThreadWithoutMessage(channelID, {type: 11, name: words.slice(1).join(" ")})
|
if (words.length < 2){
|
||||||
|
if (isFallingBack) return api.sendEvent(event.room_id, "m.room.message", {
|
||||||
|
...ctx,
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "**`/thread` usage:**\nRun this command as `/thread [Thread Name]` to create a thread. The message from which said thread will branch, is chosen based on the following rules:\n* If ran stand-alone (not as a reply, nor in a Matrix thread), the created thread will branch from the command-message itself. The `Thread Name` argument must be provided in this case, otherwise you get this help message.\n* If sent as a reply (outside a Matrix thread), the thread will branch from the message to which you replied.\n* If ran inside an existing Matrix thread (regardless of whether it's a reply or not), the created Discord thread will be branching from the same message as the Matrix thread already is.",
|
||||||
|
format: "org.matrix.custom.html",
|
||||||
|
formatted_body: "<strong><code>/thread</code> usage:</strong><br>Run this command as <code>/thread [Thread Name]</code> to create a thread on Discord (and optionally Matrix, if one doesn't exist already). The message from which said thread will branch, is chosen based on the following rules:<br><ul><li>If ran stand-alone (not as a reply, nor in a Matrix thread), the created thread will branch from the command-message itself. The <code>Thread Name</code> argument must be provided in this case, otherwise you get this help message.</li><li>If sent as a reply (outside a Matrix thread), the thread will branch from the message to which you replied.</li><li>If ran inside an existing Matrix thread (regardless of whether it's a reply or not), the created Discord thread will be branching from the same message as the Matrix thread already is.</li></ul>"
|
||||||
|
})
|
||||||
|
words[1] = (await api.getEvent(event.room_id, branchedFromMxEvent)).content.body.replaceAll("\n", " ")
|
||||||
|
words[1] = words[1].length < 100 ? words[1] : words[1].slice(0, 96) + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (branchedFromDiscordMessage) return await discord.snow.channel.createThreadWithMessage(channelID, branchedFromDiscordMessage, {name: words.slice(1).join(" ")}) //can't just return the promise directly like in 99% of other cases here in commands, otherwise the error-handling below will not work
|
||||||
|
else {return api.sendEvent(event.room_id, "m.room.message", {
|
||||||
|
...ctx,
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "⚠️ Couldn't find a Discord representation of the message from which you're trying to branch this thread (event ID `"+branchedFromMxEvent+"` on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported.",
|
||||||
|
format: "org.matrix.custom.html",
|
||||||
|
formatted_body: "⚠️ Couldn't find a Discord representation of the message from which you're trying to branch this thread (event ID <code>"+branchedFromMxEvent+"</code> on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported."
|
||||||
|
})};
|
||||||
|
}
|
||||||
|
catch (e){
|
||||||
|
/**@type {string|undefined} */
|
||||||
|
let err = e.message // see: https://docs.discord.com/developers/topics/opcodes-and-status-codes
|
||||||
|
|
||||||
|
if (err?.includes("160004")) {
|
||||||
|
if (isFallingBack) throw e; //Discord claims that there already exists a thread for the message ran this command was ran on, but that doesn't make logical sense, as it doesn't seem like it was ran on any message. Either the Matrix client did something funny with reply/thread tags, or this is a logic error on our side. At any rate, this 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.
|
||||||
|
const thread = mxUtils.getThreadRoomFromThreadEvent(branchedFromMxEvent)
|
||||||
|
return api.sendEvent(event.room_id, "m.room.message", {
|
||||||
|
...ctx,
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "There already exists a Discord thread for the message you ran this command on" + (thread ? " - you may join its bridged room here: https://matrix.to/#/"+thread+"?"+(await mxUtils.getViaServersQuery(thread, api)).toString() : ", so a new one cannot be crated. However, it seems like that thread isn't bridged to any Matrix rooms. Please ask the space/server admins to rectify this issue by creating the bridge. (If you're said admin and you can see that said bridge already exists, but this error message is still showing up, please report that as a bug.)")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (err?.includes("50024")) return api.sendEvent(event.room_id, "m.room.message", {
|
||||||
|
...ctx,
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "You cannot create threads in a Discord channel of the type, to which this Matrix room is bridged to. It could be something like a VC, or perhaps... Did you try to embed a thread inside a thread, silly?"
|
||||||
|
})
|
||||||
|
if (err?.includes("50035")) return api.sendEvent(event.room_id, "m.room.message", {
|
||||||
|
...ctx,
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "Specified thread name is too long - thread creation failed. Please yap a bit less in the title, the thread body is for that. ;)"
|
||||||
|
})
|
||||||
|
|
||||||
|
throw e //Some other error happened, one that OOYE didn't anticipate the possibility of? It should be reported to us 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.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}, {
|
}, {
|
||||||
|
|
@ -321,8 +378,11 @@ const commands = [{
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
|
||||||
/** @type {CommandExecute} */
|
/**
|
||||||
async function execute(event) {
|
* @param {Ty.Event.Outer_M_Room_Message} event
|
||||||
|
* @returns {Promise<any>|undefined} the executed command's in-process promise or undefined if no command execution was performed
|
||||||
|
*/
|
||||||
|
function parseAndExecute(event) {
|
||||||
let realBody = event.content.body
|
let realBody = event.content.body
|
||||||
while (realBody.startsWith("> ")) {
|
while (realBody.startsWith("> ")) {
|
||||||
const i = realBody.indexOf("\n")
|
const i = realBody.indexOf("\n")
|
||||||
|
|
@ -342,8 +402,8 @@ async function execute(event) {
|
||||||
const command = commands.find(c => c.aliases.includes(commandName))
|
const command = commands.find(c => c.aliases.includes(commandName))
|
||||||
if (!command) return
|
if (!command) return
|
||||||
|
|
||||||
await command.execute(event, realBody, words)
|
return command.execute(event, realBody, words)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.execute = execute
|
module.exports.parseAndExecute = parseAndExecute
|
||||||
module.exports.onReactionAdd = onReactionAdd
|
module.exports.onReactionAdd = onReactionAdd
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ const assert = require("assert").strict
|
||||||
const Ty = require("../types")
|
const Ty = require("../types")
|
||||||
const {tag} = require("@cloudrac3r/html-template-tag")
|
const {tag} = require("@cloudrac3r/html-template-tag")
|
||||||
const passthrough = require("../passthrough")
|
const passthrough = require("../passthrough")
|
||||||
const {db} = passthrough
|
const {db, select} = passthrough
|
||||||
|
|
||||||
const {reg} = require("./read-registration")
|
const {reg} = require("./read-registration")
|
||||||
const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex))
|
const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex))
|
||||||
|
|
@ -385,6 +385,16 @@ async function setUserPowerCascade(spaceID, mxid, power, api) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {undefined|string?} eventID
|
||||||
|
*/ //^For some reason, „?” doesn't include Undefined and it needs to be explicitly specified
|
||||||
|
function getThreadRoomFromThreadEvent(eventID){
|
||||||
|
if (!eventID) return eventID;
|
||||||
|
const threadID = select("event_message", "message_id", {event_id: eventID}).pluck().get() //Discord thread ID === its message ID
|
||||||
|
if (!threadID) return threadID;
|
||||||
|
return select("channel_room", "room_id", {channel_id: threadID}).pluck().get()
|
||||||
|
}
|
||||||
|
|
||||||
module.exports.bot = bot
|
module.exports.bot = bot
|
||||||
module.exports.BLOCK_ELEMENTS = BLOCK_ELEMENTS
|
module.exports.BLOCK_ELEMENTS = BLOCK_ELEMENTS
|
||||||
module.exports.eventSenderIsFromDiscord = eventSenderIsFromDiscord
|
module.exports.eventSenderIsFromDiscord = eventSenderIsFromDiscord
|
||||||
|
|
@ -400,3 +410,4 @@ module.exports.removeCreatorsFromPowerLevels = removeCreatorsFromPowerLevels
|
||||||
module.exports.getEffectivePower = getEffectivePower
|
module.exports.getEffectivePower = getEffectivePower
|
||||||
module.exports.setUserPower = setUserPower
|
module.exports.setUserPower = setUserPower
|
||||||
module.exports.setUserPowerCascade = setUserPowerCascade
|
module.exports.setUserPowerCascade = setUserPowerCascade
|
||||||
|
module.exports.getThreadRoomFromThreadEvent = getThreadRoomFromThreadEvent
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
const {select} = require("../passthrough")
|
const {select} = require("../passthrough")
|
||||||
const {test} = require("supertape")
|
const {test} = require("supertape")
|
||||||
const {eventSenderIsFromDiscord, getEventIDHash, MatrixStringBuilder, getViaServers, roomHasAtLeastVersion, removeCreatorsFromPowerLevels, setUserPower} = require("./utils")
|
const {eventSenderIsFromDiscord, getEventIDHash, MatrixStringBuilder, getViaServers, roomHasAtLeastVersion, removeCreatorsFromPowerLevels, setUserPower, getThreadRoomFromThreadEvent} = require("./utils")
|
||||||
const util = require("util")
|
const util = require("util")
|
||||||
|
|
||||||
/** @param {string[]} mxids */
|
/** @param {string[]} mxids */
|
||||||
|
|
@ -417,4 +417,23 @@ test("set user power: privileged users must demote themselves", async t => {
|
||||||
t.equal(called, 3)
|
t.equal(called, 3)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("getThreadRoomFromThreadEvent: real message with a thread", t => {
|
||||||
|
const room = getThreadRoomFromThreadEvent("$fdD9o7NxMA4VPexlAiIx2CB9JbsiGhJeyJgnZG7U5xg")
|
||||||
|
t.equal(room, "!FuDZhlOAtqswlyxzeR:cadence.moe")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("getThreadRoomFromThreadEvent: real message, but without a thread", t => {
|
||||||
|
const room = getThreadRoomFromThreadEvent("$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4")
|
||||||
|
const msg = "Expected null/undefined, got: "+room
|
||||||
|
if(room) t.fail(msg);
|
||||||
|
else t.pass(msg)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("getThreadRoomFromThreadEvent: fake message", t => {
|
||||||
|
const room = getThreadRoomFromThreadEvent("$ThisEvent-IdDoesNotExistInTheDatabase4Sure")
|
||||||
|
const msg = "Expected null/undefined, got: "+room
|
||||||
|
if(room) t.fail(msg);
|
||||||
|
else t.pass(msg)
|
||||||
|
})
|
||||||
|
|
||||||
module.exports.mockGetEffectivePower = mockGetEffectivePower
|
module.exports.mockGetEffectivePower = mockGetEffectivePower
|
||||||
|
|
|
||||||
23
src/types.d.ts
vendored
23
src/types.d.ts
vendored
|
|
@ -190,11 +190,12 @@ export namespace Event {
|
||||||
format?: "org.matrix.custom.html"
|
format?: "org.matrix.custom.html"
|
||||||
formatted_body?: string,
|
formatted_body?: string,
|
||||||
"m.relates_to"?: {
|
"m.relates_to"?: {
|
||||||
"m.in_reply_to": {
|
event_id?: string
|
||||||
|
is_falling_back?: boolean
|
||||||
|
"m.in_reply_to"?: {
|
||||||
event_id: string
|
event_id: string
|
||||||
}
|
}
|
||||||
rel_type?: "m.replace"
|
rel_type?: "m.replace"|"m.thread"
|
||||||
event_id?: string
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,11 +211,12 @@ export namespace Event {
|
||||||
info?: any
|
info?: any
|
||||||
"page.codeberg.everypizza.msc4193.spoiler"?: boolean
|
"page.codeberg.everypizza.msc4193.spoiler"?: boolean
|
||||||
"m.relates_to"?: {
|
"m.relates_to"?: {
|
||||||
"m.in_reply_to": {
|
event_id?: string
|
||||||
|
is_falling_back?: boolean
|
||||||
|
"m.in_reply_to"?: {
|
||||||
event_id: string
|
event_id: string
|
||||||
}
|
}
|
||||||
rel_type?: "m.replace"
|
rel_type?: "m.replace"|"m.thread"
|
||||||
event_id?: string
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -246,11 +248,12 @@ export namespace Event {
|
||||||
},
|
},
|
||||||
info?: any
|
info?: any
|
||||||
"m.relates_to"?: {
|
"m.relates_to"?: {
|
||||||
"m.in_reply_to": {
|
event_id?: string
|
||||||
|
is_falling_back?: boolean
|
||||||
|
"m.in_reply_to"?: {
|
||||||
event_id: string
|
event_id: string
|
||||||
}
|
}
|
||||||
rel_type?: "m.replace"
|
rel_type?: "m.replace"|"m.thread"
|
||||||
event_id?: string
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -502,6 +505,8 @@ export namespace R {
|
||||||
|
|
||||||
export type Membership = "invite" | "knock" | "join" | "leave" | "ban"
|
export type Membership = "invite" | "knock" | "join" | "leave" | "ban"
|
||||||
|
|
||||||
|
export type JoinRule = "public" | "knock" | "invite" | "private" | "restricted" | "knock_restricted"
|
||||||
|
|
||||||
export type Pagination<T> = {
|
export type Pagination<T> = {
|
||||||
chunk: T[]
|
chunk: T[]
|
||||||
next_batch?: string
|
next_batch?: string
|
||||||
|
|
|
||||||
5
src/web/pug/explain.pug
Normal file
5
src/web/pug/explain.pug
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
extends includes/template.pug
|
||||||
|
|
||||||
|
block body
|
||||||
|
.ta-center.wmx5.p48.mx-auto#ok
|
||||||
|
p.mt24.fs-body2= msg
|
||||||
|
|
@ -1,266 +1,270 @@
|
||||||
extends includes/template.pug
|
extends includes/template.pug
|
||||||
include includes/default-roles-list.pug
|
include includes/default-roles-list.pug
|
||||||
|
|
||||||
mixin badge-readonly
|
mixin badge-readonly
|
||||||
.s-badge.s-badge__xs.s-badge__icon.s-badge__muted
|
.s-badge.s-badge__xs.s-badge__icon.s-badge__muted
|
||||||
!= icons.Icons.IconEyeSm
|
!= icons.Icons.IconEyeSm
|
||||||
| Read-only
|
| Read-only
|
||||||
|
|
||||||
mixin badge-private
|
mixin badge-private
|
||||||
.s-badge.s-badge__xs.s-badge__icon.s-badge__warning
|
.s-badge.s-badge__xs.s-badge__icon.s-badge__warning
|
||||||
!= icons.Icons.IconLockSm
|
!= icons.Icons.IconLockSm
|
||||||
| Private
|
| Private
|
||||||
|
|
||||||
mixin discord(channel, radio=false)
|
mixin discord(channel, radio=false)
|
||||||
//- Previously, we passed guild.roles as the second parameter, but this doesn't quite match Discord's behaviour. See issue #42 for why this was changed.
|
//- Previously, we passed guild.roles as the second parameter, but this doesn't quite match Discord's behaviour. See issue #42 for why this was changed.
|
||||||
//- Basically we just want to assign badges based on the channel overwrites, without considering the guild's base permissions. /shrug
|
//- Basically we just want to assign badges based on the channel overwrites, without considering the guild's base permissions. /shrug
|
||||||
- let permissions = dUtils.getPermissions(guild_id, [], [{id: guild_id, name: "@everyone", permissions: 1<<10 | 1<<11}], null, channel.permission_overwrites)
|
- let permissions = dUtils.getPermissions(guild_id, [], [{id: guild_id, name: "@everyone", permissions: 1<<10 | 1<<11}], null, channel.permission_overwrites)
|
||||||
.s-user-card.s-user-card__small
|
.s-user-card.s-user-card__small
|
||||||
if !dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.ViewChannel)
|
if !dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.ViewChannel)
|
||||||
!= icons.Icons.IconLock
|
!= icons.Icons.IconLock
|
||||||
else if channel.type === 5
|
else if channel.type === 5
|
||||||
!= icons.Icons.IconBullhorn
|
!= icons.Icons.IconBullhorn
|
||||||
else if channel.type === 2
|
else if channel.type === 2
|
||||||
!= icons.Icons.IconPhone
|
!= icons.Icons.IconPhone
|
||||||
else if channel.type === 11 || channel.type === 12
|
else if channel.type === 11 || channel.type === 12
|
||||||
!= icons.Icons.IconCollection
|
!= icons.Icons.IconCollection
|
||||||
else
|
else
|
||||||
include includes/hash.svg
|
include includes/hash.svg
|
||||||
.s-user-card--info.ws-nowrap
|
.s-user-card--info.ws-nowrap
|
||||||
if radio
|
if radio
|
||||||
= channel.name
|
= channel.name
|
||||||
else
|
else
|
||||||
.s-user-card--link.fs-body1
|
.s-user-card--link.fs-body1
|
||||||
a(href=`https://discord.com/channels/${channel.guild_id}/${channel.id}`)= channel.name
|
a(href=`https://discord.com/channels/${channel.guild_id}/${channel.id}`)= channel.name
|
||||||
if channel.parent_id
|
if channel.parent_id
|
||||||
.s-user-card--location= discord.channels.get(channel.parent_id).name
|
.s-user-card--location= discord.channels.get(channel.parent_id).name
|
||||||
if !dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.ViewChannel)
|
if !dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.ViewChannel)
|
||||||
+badge-private
|
+badge-private
|
||||||
else if !dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.SendMessages)
|
else if !dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.SendMessages)
|
||||||
+badge-readonly
|
+badge-readonly
|
||||||
|
|
||||||
mixin matrix(row, radio=false, badge="")
|
mixin matrix(row, radio=false, badge="")
|
||||||
.s-user-card.s-user-card__small
|
.s-user-card.s-user-card__small
|
||||||
!= icons.Icons.IconMessage
|
!= icons.Icons.IconMessage
|
||||||
.s-user-card--info.ws-nowrap
|
.s-user-card--info.ws-nowrap
|
||||||
if radio
|
if radio
|
||||||
= row.nick || row.name
|
= row.nick || row.name
|
||||||
else
|
else
|
||||||
.s-user-card--link.fs-body1
|
.s-user-card--link.fs-body1
|
||||||
a(href=`https://matrix.to/#/${row.room_id}`)= row.nick || row.name
|
a(href=`https://matrix.to/#/${row.room_id}`)= row.nick || row.name
|
||||||
if row.join_rule === "invite"
|
if row.join_rule === "invite"
|
||||||
+badge-private
|
+badge-private
|
||||||
|
|
||||||
block body
|
block body
|
||||||
.s-page-title.mb24
|
.s-page-title.mb24
|
||||||
h1.s-page-title--header= guild.name
|
h1.s-page-title--header= guild.name
|
||||||
|
|
||||||
.d-flex.g16(class="sm:fw-wrap")
|
.d-flex.g16(class="sm:fw-wrap")
|
||||||
.fl-grow1
|
.fl-grow1
|
||||||
h2.fs-headline1 Invite a Matrix user
|
h2.fs-headline1 Invite a Matrix user
|
||||||
|
|
||||||
form.d-grid.g-af-column.gy4.gx8.jc-start(method="post" action=rel("/api/invite") hx-post=rel("/api/invite") hx-trigger="submit" hx-swap="none" hx-on::after-request="if (event.detail.successful) this.reset()" hx-disabled-elt="input, button" hx-indicator="#invite-button")
|
form.d-grid.g-af-column.gy4.gx8.jc-start(method="post" action=rel("/api/invite") hx-post=rel("/api/invite") hx-trigger="submit" hx-swap="none" hx-on::after-request="if (event.detail.successful) this.reset()" hx-disabled-elt="input, button" hx-indicator="#invite-button")
|
||||||
label.s-label(for="mxid") Matrix ID
|
label.s-label(for="mxid") Matrix ID
|
||||||
input.fl-grow1.s-input.wmx3#mxid(name="mxid" required placeholder="@user:example.org" pattern="@([^:]+):([a-z0-9:\\-]+\\.[a-z0-9.:\\-]+)")
|
input.fl-grow1.s-input.wmx3#mxid(name="mxid" required placeholder="@user:example.org" pattern="@([^:]+):([a-z0-9:\\-]+\\.[a-z0-9.:\\-]+)")
|
||||||
label.s-label(for="permissions") Permissions
|
label.s-label(for="permissions") Permissions
|
||||||
.s-select
|
.s-select
|
||||||
select#permissions(name="permissions")
|
select#permissions(name="permissions")
|
||||||
option(value="default") Default
|
option(value="default") Default
|
||||||
option(value="moderator") Moderator
|
option(value="moderator") Moderator
|
||||||
option(value="admin") Admin
|
option(value="admin") Admin
|
||||||
input(type="hidden" name="guild_id" value=guild_id)
|
input(type="hidden" name="guild_id" value=guild_id)
|
||||||
.grid--row-start2
|
.grid--row-start2
|
||||||
button.s-btn.s-btn__filled#invite-button Invite
|
button.s-btn.s-btn__filled#invite-button Invite
|
||||||
div
|
div
|
||||||
.s-card.d-flex.ai-center.jc-center(style="min-width: 132px; min-height: 132px;")
|
.s-card.d-flex.ai-center.jc-center(style="min-width: 132px; min-height: 132px;")
|
||||||
button.s-btn(class=space_id ? "s-btn__muted" : "s-btn__filled" hx-get=rel(`/qr?guild_id=${guild_id}`) hx-indicator="closest button" hx-swap="outerHTML" hx-disabled-elt="this") Show QR
|
button.s-btn(class=space_id ? "s-btn__muted" : "s-btn__filled" hx-get=rel(`/qr?guild_id=${guild_id}`) hx-indicator="closest button" hx-swap="outerHTML" hx-disabled-elt="this") Show QR
|
||||||
|
|
||||||
if space_id
|
if space_id
|
||||||
h2.mt48.fs-headline1 Server settings
|
h2.mt48.fs-headline1 Server settings
|
||||||
h3.mt32.fs-category How Matrix users join
|
h3.mt32.fs-category How Matrix users join
|
||||||
span#privacy-level-loading
|
span#privacy-level-loading
|
||||||
.s-card
|
.s-card
|
||||||
form(hx-post=rel("/api/privacy-level") hx-trigger="change" hx-indicator="#privacy-level-loading" hx-disabled-elt="input")
|
form(hx-post=rel("/api/privacy-level") hx-trigger="change" hx-indicator="#privacy-level-loading" hx-disabled-elt="input")
|
||||||
input(type="hidden" name="guild_id" value=guild_id)
|
input(type="hidden" name="guild_id" value=guild_id)
|
||||||
|
|
||||||
.s-toggle-switch.s-toggle-switch__multiple.s-toggle-switch__incremental.d-grid.gx16.ai-center(style="grid-template-columns: auto 1fr")
|
.s-toggle-switch.s-toggle-switch__multiple.s-toggle-switch__incremental.d-grid.gx16.ai-center(style="grid-template-columns: auto 1fr")
|
||||||
input(type="radio" name="privacy_level" value="directory" id="privacy-level-directory" checked=(privacy_level === 2))
|
input(type="radio" name="privacy_level" value="directory" id="privacy-level-directory" checked=(privacy_level === 2))
|
||||||
label.d-flex.gx8.jc-center.grid--row-start3(for="privacy-level-directory")
|
label.d-flex.gx8.jc-center.grid--row-start3(for="privacy-level-directory")
|
||||||
!= icons.Icons.IconPlusSm
|
!= icons.Icons.IconPlusSm
|
||||||
!= icons.Icons.IconInternationalSm
|
!= icons.Icons.IconInternationalSm
|
||||||
.fl-grow1 Directory
|
.fl-grow1 Directory
|
||||||
|
|
||||||
input(type="radio" name="privacy_level" value="link" id="privacy-level-link" checked=(privacy_level === 1))
|
input(type="radio" name="privacy_level" value="link" id="privacy-level-link" checked=(privacy_level === 1))
|
||||||
label.d-flex.gx8.jc-center.grid--row-start2(for="privacy-level-link")
|
label.d-flex.gx8.jc-center.grid--row-start2(for="privacy-level-link")
|
||||||
!= icons.Icons.IconPlusSm
|
!= icons.Icons.IconPlusSm
|
||||||
!= icons.Icons.IconLinkSm
|
!= icons.Icons.IconLinkSm
|
||||||
.fl-grow1 Link
|
.fl-grow1 Link
|
||||||
|
|
||||||
input(type="radio" name="privacy_level" value="invite" id="privacy-level-invite" checked=(privacy_level === 0))
|
input(type="radio" name="privacy_level" value="invite" id="privacy-level-invite" checked=(privacy_level === 0))
|
||||||
label.d-flex.gx8.jc-center.grid--row-start1(for="privacy-level-invite")
|
label.d-flex.gx8.jc-center.grid--row-start1(for="privacy-level-invite")
|
||||||
svg.svg-icon(width="14" height="14" viewBox="0 0 14 14")
|
svg.svg-icon(width="14" height="14" viewBox="0 0 14 14")
|
||||||
!= icons.Icons.IconLockSm
|
!= icons.Icons.IconLockSm
|
||||||
.fl-grow1 Invite
|
.fl-grow1 Invite
|
||||||
|
|
||||||
p.s-description.m0 In-app direct invite from another user
|
p.s-description.m0 In-app direct invite from another user
|
||||||
p.s-description.m0 Shareable invite links, like Discord
|
p.s-description.m0 Shareable invite links, like Discord
|
||||||
p.s-description.m0 Publicly listed in directory, like Discord server discovery
|
p.s-description.m0 Publicly listed in directory, like Discord server discovery
|
||||||
|
|
||||||
h3.mt32.fs-category Default roles
|
h3.mt32.fs-category Default roles
|
||||||
.s-card
|
.s-card
|
||||||
form(method="post" action=rel("/api/default-roles") hx-post=rel("/api/default-roles") hx-sync="this:drop" hx-indicator="#add-role-loading" hx-target="#default-roles-list" hx-select="#default-roles-list" hx-swap="outerHTML")#default-roles
|
form(method="post" action=rel("/api/default-roles") hx-post=rel("/api/default-roles") hx-sync="this:drop" hx-indicator="#add-role-loading" hx-target="#default-roles-list" hx-select="#default-roles-list" hx-swap="outerHTML")#default-roles
|
||||||
input(type="hidden" name="guild_id" value=guild_id)
|
input(type="hidden" name="guild_id" value=guild_id)
|
||||||
.d-flex.fw-wrap.g4
|
.d-flex.fw-wrap.g4
|
||||||
.s-tag.s-tag__md.fs-body1.s-tag__required @everyone
|
.s-tag.s-tag__md.fs-body1.s-tag__required @everyone
|
||||||
|
|
||||||
+default-roles-list(guild, guild_id)
|
+default-roles-list(guild, guild_id)
|
||||||
|
|
||||||
button(type="button" popovertarget="role-add").s-btn__dropdown.s-tag.s-tag__md.fs-body1.p0
|
button(type="button" popovertarget="role-add").s-btn__dropdown.s-tag.s-tag__md.fs-body1.p0
|
||||||
.s-tag--dismiss.m1
|
.s-tag--dismiss.m1
|
||||||
!= icons.Icons.IconPlusSm
|
!= icons.Icons.IconPlusSm
|
||||||
|
|
||||||
#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-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.
|
||||||
|
|
||||||
h3.mt32.fs-category Features
|
h3.mt32.fs-category Features
|
||||||
.s-card.d-grid.px0.g16
|
.s-card.d-grid.px0.g16
|
||||||
form.d-flex.ai-center.g16
|
form.d-flex.ai-center.g16
|
||||||
#url-preview-loading.p8
|
#url-preview-loading.p8
|
||||||
- let value = !!select("guild_space", "url_preview", {guild_id}).pluck().get()
|
- let value = !!select("guild_space", "url_preview", {guild_id}).pluck().get()
|
||||||
input(type="hidden" name="guild_id" value=guild_id)
|
input(type="hidden" name="guild_id" value=guild_id)
|
||||||
input.s-toggle-switch#url-preview(name="url_preview" type="checkbox" hx-post=rel("/api/url-preview") hx-indicator="#url-preview-loading" hx-disabled-elt="this" checked=value autocomplete="off")
|
input.s-toggle-switch#url-preview(name="url_preview" type="checkbox" hx-post=rel("/api/url-preview") hx-indicator="#url-preview-loading" hx-disabled-elt="this" checked=value autocomplete="off")
|
||||||
label.s-label.fl-grow1(for="url-preview")
|
label.s-label.fl-grow1(for="url-preview")
|
||||||
| Show Discord's URL previews on Matrix
|
| Show Discord's URL previews on Matrix
|
||||||
p.s-description Shows info about links posted to chat. Discord's previews are generally better quality than Synapse's, especially for social media and videos.
|
p.s-description Shows info about links posted to chat. Discord's previews are generally better quality than Synapse's, especially for social media and videos.
|
||||||
|
|
||||||
form.d-flex.ai-center.g16
|
form.d-flex.ai-center.g16
|
||||||
#presence-loading.p8
|
#presence-loading.p8
|
||||||
- value = !!select("guild_space", "presence", {guild_id}).pluck().get()
|
- value = !!select("guild_space", "presence", {guild_id}).pluck().get()
|
||||||
input(type="hidden" name="guild_id" value=guild_id)
|
input(type="hidden" name="guild_id" value=guild_id)
|
||||||
input.s-toggle-switch#presence(name="presence" type="checkbox" hx-post=rel("/api/presence") hx-indicator="#presence-loading" hx-disabled-elt="this" checked=value autocomplete="off")
|
input.s-toggle-switch#presence(name="presence" type="checkbox" hx-post=rel("/api/presence") hx-indicator="#presence-loading" hx-disabled-elt="this" checked=value autocomplete="off")
|
||||||
label.s-label(for="presence")
|
label.s-label(for="presence")
|
||||||
| Show online statuses on Matrix
|
| Show online statuses on Matrix
|
||||||
p.s-description This might cause lag on really big Discord servers.
|
p.s-description This might cause lag on really big Discord servers.
|
||||||
|
|
||||||
form.d-flex.ai-center.g16
|
form.d-flex.ai-center.g16
|
||||||
#webhook-profile-loading.p8
|
#webhook-profile-loading.p8
|
||||||
- value = !!select("guild_space", "webhook_profile", {guild_id}).pluck().get()
|
- value = !!select("guild_space", "webhook_profile", {guild_id}).pluck().get()
|
||||||
input(type="hidden" name="guild_id" value=guild_id)
|
input(type="hidden" name="guild_id" value=guild_id)
|
||||||
input.s-toggle-switch#webhook-profile(name="webhook_profile" type="checkbox" hx-post=rel("/api/webhook-profile") hx-indicator="#webhook-profile-loading" hx-disabled-elt="this" checked=value autocomplete="off")
|
input.s-toggle-switch#webhook-profile(name="webhook_profile" type="checkbox" hx-post=rel("/api/webhook-profile") hx-indicator="#webhook-profile-loading" hx-disabled-elt="this" checked=value autocomplete="off")
|
||||||
label.s-label(for="webhook-profile")
|
label.s-label(for="webhook-profile")
|
||||||
| Create persistent Matrix sims for webhooks
|
| Create persistent Matrix sims for webhooks
|
||||||
p.s-description Useful when using other Discord bridges. Otherwise, not ideal, as sims will clutter the Matrix user list and will never be cleaned up.
|
p.s-description Useful when using other Discord bridges. Otherwise, not ideal, as sims will clutter the Matrix user list and will never be cleaned up.
|
||||||
|
|
||||||
if space_id
|
if space_id
|
||||||
h2.mt48.fs-headline1 Channel setup
|
h2.mt48.fs-headline1 Channel setup
|
||||||
|
|
||||||
h3.mt32.fs-category Linked channels
|
h3.mt32.fs-category Linked channels
|
||||||
.s-card.bs-sm.p0
|
.s-card.bs-sm.p0
|
||||||
form.s-table-container(method="post" action=rel("/api/unlink"))
|
form.s-table-container(method="post" action=rel("/api/unlink"))
|
||||||
input(type="hidden" name="guild_id" value=guild_id)
|
input(type="hidden" name="guild_id" value=guild_id)
|
||||||
table.s-table.s-table__bx-simple
|
table.s-table.s-table__bx-simple
|
||||||
each row in linkedChannelsWithDetails
|
each row in linkedChannelsWithDetails
|
||||||
tr
|
tr
|
||||||
td.w40: +discord(row.channel)
|
td.w40: +discord(row.channel)
|
||||||
td.p2: button.s-btn.s-btn__muted.s-btn__xs(name="channel_id" cx-prevent-default hx-post=rel("/api/unlink") hx-confirm="Do you want to unlink these channels?\nIt may take a moment to clean up Matrix resources." value=row.channel.id hx-indicator="this" hx-disabled-elt="this")!= icons.Icons.IconLinkSm
|
td.p2: button.s-btn.s-btn__muted.s-btn__xs(name="channel_id" cx-prevent-default hx-post=rel("/api/unlink") hx-confirm="Do you want to unlink these channels?\nIt may take a moment to clean up Matrix resources." value=row.channel.id hx-indicator="this" hx-disabled-elt="this")!= icons.Icons.IconLinkSm
|
||||||
td: +matrix(row)
|
td: +matrix(row)
|
||||||
else
|
else
|
||||||
tr
|
tr
|
||||||
td(colspan="3")
|
td(colspan="3")
|
||||||
.s-empty-state No channels linked between Discord and Matrix yet...
|
.s-empty-state No channels linked between Discord and Matrix yet...
|
||||||
|
|
||||||
h3.fs-category.mt32 Auto-create
|
h3.fs-category.mt32 Auto-create
|
||||||
.s-card.d-grid.px0
|
.s-card.d-grid.px0
|
||||||
form.d-flex.ai-center.g16
|
form.d-flex.ai-center.g16
|
||||||
#autocreate-loading.p8
|
#autocreate-loading.p8
|
||||||
- let value = !!select("guild_active", "autocreate", {guild_id}).pluck().get()
|
- let value = !!select("guild_active", "autocreate", {guild_id}).pluck().get()
|
||||||
input(type="hidden" name="guild_id" value=guild_id)
|
input(type="hidden" name="guild_id" value=guild_id)
|
||||||
input.s-toggle-switch#autocreate(name="autocreate" type="checkbox" hx-post=rel("/api/autocreate") hx-indicator="#autocreate-loading" hx-disabled-elt="this" checked=value autocomplete="off")
|
input.s-toggle-switch#autocreate(name="autocreate" type="checkbox" hx-post=rel("/api/autocreate") hx-indicator="#autocreate-loading" hx-disabled-elt="this" checked=value autocomplete="off")
|
||||||
label.s-label.fl-grow1(for="autocreate")
|
label.s-label.fl-grow1(for="autocreate")
|
||||||
| Create new Matrix rooms automatically
|
| Create new Matrix rooms automatically
|
||||||
p.s-description If you want, OOYE can automatically create new Matrix rooms and link them when an unlinked Discord channel is spoken in.
|
p.s-description If you want, OOYE can automatically create new Matrix rooms and link them when an unlinked Discord channel is spoken in.
|
||||||
|
|
||||||
if space_id
|
if space_id
|
||||||
h3.mt32.fs-category Manually link channels
|
h3.mt32.fs-category Manually link channels
|
||||||
form.d-flex.g16.ai-start(hx-post=rel("/api/link") hx-trigger="submit" hx-disabled-elt="input, button" hx-indicator="#link-button")
|
form.d-flex.g16.ai-start(hx-post=rel("/api/link") hx-trigger="submit" hx-disabled-elt="input, button" hx-indicator="#link-button")
|
||||||
.fl-grow2.s-btn-group.fd-column.w40
|
.fl-grow2.s-btn-group.fd-column.w40
|
||||||
each channel in unlinkedChannels
|
each channel in unlinkedChannels
|
||||||
input.s-btn--radio(type="radio" name="discord" required id=channel.id value=channel.id)
|
input.s-btn--radio(type="radio" name="discord" required id=channel.id value=channel.id)
|
||||||
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 All Discord channels are linked.
|
||||||
.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 All Matrix rooms are linked.
|
||||||
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
|
||||||
!= icons.Icons.IconMerge
|
!= icons.Icons.IconMerge
|
||||||
= ` Link`
|
= ` Link`
|
||||||
|
|
||||||
h3.mt32.fs-category Unlink server
|
h3.mt32.fs-category Unlink server
|
||||||
form.s-card.d-flex.gx16.ai-center(method="post" action=rel("/api/unlink-space"))
|
form.s-card.d-flex.gx16.ai-center(method="post" action=rel("/api/unlink-space"))
|
||||||
input(type="hidden" name="guild_id" value=guild.id)
|
input(type="hidden" name="guild_id" value=guild.id)
|
||||||
.fl-grow1.s-prose.s-prose__sm.lh-lg
|
.fl-grow1.s-prose.s-prose__sm.lh-lg
|
||||||
p.fc-medium.
|
p.fc-medium.
|
||||||
Not using this bridge, or just made a mistake? You can unlink the whole server and all its channels.#[br]
|
Not using this bridge, or just made a mistake? You can unlink the whole server and all its channels.#[br]
|
||||||
This may take a minute to process. Please be patient and wait until the page refreshes.
|
This may take a minute to process. Please be patient and wait until the page refreshes.
|
||||||
div
|
div
|
||||||
button.s-btn.s-btn__icon.s-btn__danger.s-btn__outlined(cx-prevent-default hx-post=rel("/api/unlink-space") hx-confirm="Do you want to unlink this server and all its channels?\nIt may take a minute to clean up Matrix resources." hx-indicator="this" hx-disabled-elt="this")
|
button.s-btn.s-btn__icon.s-btn__danger.s-btn__outlined(cx-prevent-default hx-post=rel("/api/unlink-space") hx-confirm="Do you want to unlink this server and all its channels?\nIt may take a minute to clean up Matrix resources." hx-indicator="this" hx-disabled-elt="this")
|
||||||
!= icons.Icons.IconUnsync
|
!= icons.Icons.IconUnsync
|
||||||
span.ml4= ` Unlink`
|
span.ml4= ` Unlink`
|
||||||
|
|
||||||
if space_id
|
if space_id
|
||||||
details.mt48
|
details.mt48
|
||||||
summary Debug room list
|
summary Debug room list
|
||||||
.d-grid.grid__2.gx24
|
.d-grid.grid__2.gx24
|
||||||
div
|
div
|
||||||
h3.mt24 Channels
|
h3.mt24 Channels
|
||||||
p Channels are read from the channel_room table and then merged with the discord.channels memory cache to make the merged list. Anything in memory cache that's not in channel_room is considered unlinked.
|
p Channels are read from the channel_room table and then merged with the discord.channels memory cache to make the merged list. Anything in memory cache that's not in channel_room is considered unlinked.
|
||||||
div
|
div
|
||||||
h3.mt24 Rooms
|
h3.mt24 Rooms
|
||||||
p Rooms use the same merged list as channels, based on augmented channel_room data. Then, rooms are read from the space. Anything in the space that's not merged is considered unlinked.
|
p Rooms use the same merged list as channels, based on augmented channel_room data. Then, rooms are read from the space. Anything in the space that's not merged is considered unlinked.
|
||||||
div
|
div
|
||||||
h3.mt24 Unavailable channels: Deleted from Discord
|
h3.mt24 Unavailable channels: Deleted from Discord
|
||||||
.s-card.p0
|
.s-card.p0
|
||||||
ul.my8.ml24
|
ul.my8.ml24
|
||||||
each row in removedUncachedChannels
|
each row in removedUncachedChannels
|
||||||
li: a(href=`https://discord.com/channels/${guild_id}/${row.id}`)= row.nick || row.name
|
li: a(href=`https://discord.com/channels/${guild_id}/${row.id}`)= row.nick || row.name
|
||||||
h3.mt24 Unavailable channels: Wrong type
|
h3.mt24 Unavailable channels: Wrong type
|
||||||
.s-card.p0
|
.s-card.p0
|
||||||
ul.my8.ml24
|
ul.my8.ml24
|
||||||
each row in removedWrongTypeChannels
|
each row in removedWrongTypeChannels
|
||||||
li: a(href=`https://discord.com/channels/${guild_id}/${row.id}`) (#{row.type}) #{row.name}
|
li
|
||||||
h3.mt24 Unavailable channels: Discord bot can't access
|
a(href=`https://discord.com/channels/${guild_id}/${row.id}`) (#{row.type}) #{row.name}
|
||||||
.s-card.p0
|
span |
|
||||||
ul.my8.ml24
|
a(href=rel(`/explain?type=${row.type}`)) Why?
|
||||||
each row in removedPrivateChannels
|
h3.mt24 Unavailable channels: Discord bot can't access
|
||||||
li: a(href=`https://discord.com/channels/${guild_id}/${row.id}`)= row.name
|
.s-card.p0
|
||||||
div- // Rooms
|
ul.my8.ml24
|
||||||
h3.mt24 Unavailable rooms: Already linked
|
each row in removedPrivateChannels
|
||||||
.s-card.p0
|
li: a(href=`https://discord.com/channels/${guild_id}/${row.id}`)= row.name
|
||||||
ul.my8.ml24
|
div- // Rooms
|
||||||
each row in removedLinkedRooms
|
h3.mt24 Unavailable rooms: Already linked
|
||||||
li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name
|
.s-card.p0
|
||||||
h3.mt24 Unavailable rooms: Encryption not supported
|
ul.my8.ml24
|
||||||
.s-card.p0
|
each row in removedLinkedRooms
|
||||||
ul.my8.ml24
|
li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name
|
||||||
each row in removedEncryptedRooms
|
h3.mt24 Unavailable rooms: Encryption not supported
|
||||||
li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name
|
.s-card.p0
|
||||||
h3.mt24 Unavailable rooms: Wrong type
|
ul.my8.ml24
|
||||||
.s-card.p0
|
each row in removedEncryptedRooms
|
||||||
ul.my8.ml24
|
li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name
|
||||||
each row in removedWrongTypeRooms
|
h3.mt24 Unavailable rooms: Root space
|
||||||
li: a(href=`https://matrix.to/#/${row.room_id}`) (#{row.room_type}) #{row.name}
|
.s-card.p0
|
||||||
h3.mt24 Unavailable rooms: Archived thread
|
ul.my8.ml24
|
||||||
.s-card.p0
|
each row in removedRootSpaceRooms
|
||||||
ul.my8.ml24
|
li: a(href=`https://matrix.to/#/${row.room_id}`) (#{row.room_type}) #{row.name}
|
||||||
each row in removedArchivedThreadRooms
|
h3.mt24 Unavailable rooms: Archived thread
|
||||||
li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name
|
p If you still want to link with any of these rooms (eg. you accidentally unlinked it and want to bring it back, or you're migrating from a different bridge that happens to use OOYE's prefixes), please remove the [⛓️] or [🔒⛓️] prefix in Matrix's room settings and refresh the page.
|
||||||
|
.s-card.p0
|
||||||
|
ul.my8.ml24
|
||||||
|
each row in removedArchivedThreadRooms
|
||||||
|
li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ const schema = {
|
||||||
}),
|
}),
|
||||||
inviteNonce: z.object({
|
inviteNonce: z.object({
|
||||||
nonce: z.string()
|
nonce: z.string()
|
||||||
|
}),
|
||||||
|
explain: z.object({
|
||||||
|
type: z.string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,6 +56,27 @@ function getAPI(event) {
|
||||||
/** @type {LRUCache<string, string>} nonce to guild id */
|
/** @type {LRUCache<string, string>} nonce to guild id */
|
||||||
const validNonce = new LRUCache({max: 200})
|
const validNonce = new LRUCache({max: 200})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TYPING = Channels on which Discord messages can be sent. They should be bridgeable to anything other than an m.space (because if it did end up as a space, no one would be able to actually see the text messages sent there).
|
||||||
|
* SPACE = Channels on which Discord messages cannot be received. They should be bridgeable to m.space only (because not only does m.space make sending messages impossible on any sane client (thus preventing Discord-caused errors), but it also just-so-happens that both currently-existing message-unsupporting channel types (Categories and School hubs) are sort of "indexes", which fits nicely to m.space).
|
||||||
|
* MIXED = Forum-like channels. They can be bridged to both m.space and anything other than an m.space - hence the name.
|
||||||
|
* @type {Map<DiscordTypes.ChannelType, {type: "TYPING"|"MIXED"|"SPACE", humanName:string, unsupported?: string}>}*/
|
||||||
|
const linkRules = new Map([
|
||||||
|
[0, {type: "TYPING", humanName:"Normal text channels"}],
|
||||||
|
[1, {type: "TYPING", humanName:"Normal DMs", unsupported: "OOYE won't support DMs until a good way of doing it can be figured out. Please see https://gitdab.com/cadence/out-of-your-element#caveats for more."}],
|
||||||
|
[2, {type: "TYPING", humanName:"Normal VCs"}],
|
||||||
|
[3, {type: "TYPING", humanName:"Group DMs", unsupported: "OOYE won't support DMs until a good way of doing it can be figured out. Please see https://gitdab.com/cadence/out-of-your-element#caveats for more."}],
|
||||||
|
[4, {type: "SPACE", humanName:"Categories", unsupported: "There is no concept of categories on Matrix."}], //...at least officially. In practice, some clients will render sub-spaces as categories. TODO: Bridge categories to sub-spaces.
|
||||||
|
[5, {type: "TYPING", humanName:"Announcement text channels"}],
|
||||||
|
[10, {type: "TYPING", humanName:"Announcement threads"}],
|
||||||
|
[11, {type: "TYPING", humanName:"Normal threads"}],
|
||||||
|
[12, {type: "TYPING", humanName:"Private threads"}],
|
||||||
|
[13, {type: "TYPING", humanName:"Stage VCs"}],
|
||||||
|
[14, {type: "SPACE", humanName:"School hubs", unsupported: "Bots cannot be members of school hubs. How in the sweet hell did you manage to put OOYE on one, anyway??? ~~Emma, please stop breaking Discord API in cursed ways again.~~"}],
|
||||||
|
[15, {type: "MIXED", humanName:"Normal forums"}],
|
||||||
|
[16, {type: "MIXED", humanName:"Media forums"}],
|
||||||
|
])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{type: number, parent_id?: string | null, position?: number}} channel
|
* @param {{type: number, parent_id?: string | null, position?: number}} channel
|
||||||
* @param {Map<string, {type: number, parent_id?: string | null, position?: number}>} channels
|
* @param {Map<string, {type: number, parent_id?: string | null, position?: number}>} channels
|
||||||
|
|
@ -94,8 +118,9 @@ function getPosition(channel, channels) {
|
||||||
* @param {DiscordTypes.APIGuild} guild
|
* @param {DiscordTypes.APIGuild} guild
|
||||||
* @param {Ty.R.Hierarchy[]} rooms
|
* @param {Ty.R.Hierarchy[]} rooms
|
||||||
* @param {string[]} roles
|
* @param {string[]} roles
|
||||||
|
* @param {string?} space
|
||||||
*/
|
*/
|
||||||
function getChannelRoomsLinks(guild, rooms, roles) {
|
function getChannelRoomsLinks(guild, rooms, roles, space) {
|
||||||
let channelIDs = discord.guildChannelMap.get(guild.id)
|
let channelIDs = discord.guildChannelMap.get(guild.id)
|
||||||
assert(channelIDs)
|
assert(channelIDs)
|
||||||
|
|
||||||
|
|
@ -112,7 +137,10 @@ function getChannelRoomsLinks(guild, rooms, roles) {
|
||||||
let unlinkedChannelIDs = channelIDs.filter(c => !linkedChannelIDs.includes(c))
|
let unlinkedChannelIDs = channelIDs.filter(c => !linkedChannelIDs.includes(c))
|
||||||
/** @type {DiscordTypes.APIGuildChannel[]} */ // @ts-ignore
|
/** @type {DiscordTypes.APIGuildChannel[]} */ // @ts-ignore
|
||||||
let unlinkedChannels = unlinkedChannelIDs.map(c => discord.channels.get(c))
|
let unlinkedChannels = unlinkedChannelIDs.map(c => discord.channels.get(c))
|
||||||
let removedWrongTypeChannels = dUtils.filterTo(unlinkedChannels, c => c && [0, 5].includes(c.type))
|
let removedWrongTypeChannels = dUtils.filterTo(unlinkedChannels, c => {
|
||||||
|
const rule = linkRules.get(c?.type)
|
||||||
|
return rule && !rule.unsupported
|
||||||
|
})
|
||||||
let removedPrivateChannels = dUtils.filterTo(unlinkedChannels, c => {
|
let removedPrivateChannels = dUtils.filterTo(unlinkedChannels, c => {
|
||||||
const permissions = dUtils.getPermissions(guild.id, roles, guild.roles, botID, c["permission_overwrites"])
|
const permissions = dUtils.getPermissions(guild.id, roles, guild.roles, botID, c["permission_overwrites"])
|
||||||
return dUtils.hasSomePermissions(permissions, ["Administrator", "ViewChannel"])
|
return dUtils.hasSomePermissions(permissions, ["Administrator", "ViewChannel"])
|
||||||
|
|
@ -122,7 +150,7 @@ function getChannelRoomsLinks(guild, rooms, roles) {
|
||||||
let linkedRoomIDs = linkedChannels.map(c => c.room_id)
|
let linkedRoomIDs = linkedChannels.map(c => c.room_id)
|
||||||
let unlinkedRooms = [...rooms]
|
let unlinkedRooms = [...rooms]
|
||||||
let removedLinkedRooms = dUtils.filterTo(unlinkedRooms, r => !linkedRoomIDs.includes(r.room_id))
|
let removedLinkedRooms = dUtils.filterTo(unlinkedRooms, r => !linkedRoomIDs.includes(r.room_id))
|
||||||
let removedWrongTypeRooms = dUtils.filterTo(unlinkedRooms, r => !r.room_type)
|
let removedRootSpaceRooms = dUtils.filterTo(unlinkedRooms, r => r.room_id !== space)
|
||||||
let removedEncryptedRooms = dUtils.filterTo(unlinkedRooms, r => !r.encryption && !r["im.nheko.summary.encryption"])
|
let removedEncryptedRooms = dUtils.filterTo(unlinkedRooms, r => !r.encryption && !r["im.nheko.summary.encryption"])
|
||||||
// https://discord.com/developers/docs/topics/threads#active-archived-threads
|
// https://discord.com/developers/docs/topics/threads#active-archived-threads
|
||||||
// need to filter out linked archived threads from unlinkedRooms, will just do that by comparing against the name
|
// need to filter out linked archived threads from unlinkedRooms, will just do that by comparing against the name
|
||||||
|
|
@ -130,7 +158,7 @@ function getChannelRoomsLinks(guild, rooms, roles) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
linkedChannelsWithDetails, unlinkedChannels, unlinkedRooms,
|
linkedChannelsWithDetails, unlinkedChannels, unlinkedRooms,
|
||||||
removedUncachedChannels, removedWrongTypeChannels, removedPrivateChannels, removedLinkedRooms, removedWrongTypeRooms, removedArchivedThreadRooms, removedEncryptedRooms
|
removedUncachedChannels, removedWrongTypeChannels, removedPrivateChannels, removedLinkedRooms, removedRootSpaceRooms, removedArchivedThreadRooms, removedEncryptedRooms
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,17 +199,25 @@ as.router.get("/guild", defineEventHandler(async event => {
|
||||||
|
|
||||||
// Easy mode guild that hasn't been linked yet - need to remove elements that would require an existing space
|
// Easy mode guild that hasn't been linked yet - need to remove elements that would require an existing space
|
||||||
if (!row.space_id) {
|
if (!row.space_id) {
|
||||||
const links = getChannelRoomsLinks(guild, [], roles)
|
const links = getChannelRoomsLinks(guild, [], roles, row.space_id)
|
||||||
return pugSync.render(event, "guild.pug", {guild, guild_id, ...links, ...row})
|
return pugSync.render(event, "guild.pug", {guild, guild_id, ...links, ...row})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linked guild
|
// Linked guild
|
||||||
const api = getAPI(event)
|
const api = getAPI(event)
|
||||||
const rooms = await api.getFullHierarchy(row.space_id)
|
const rooms = await api.getFullHierarchy(row.space_id)
|
||||||
const links = getChannelRoomsLinks(guild, rooms, roles)
|
const links = getChannelRoomsLinks(guild, rooms, roles, row.space_id)
|
||||||
return pugSync.render(event, "guild.pug", {guild, guild_id, ...links, ...row})
|
return pugSync.render(event, "guild.pug", {guild, guild_id, ...links, ...row})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
as.router.get("/explain", defineEventHandler(async event => {
|
||||||
|
const {type} = await getValidatedQuery(event, schema.explain.parse)
|
||||||
|
const rule = linkRules.get(Number.parseInt(type))
|
||||||
|
if (!rule) return pugSync.render(event, "explain.pug", {msg: "You cannot bridge to type-" + type + " channels because OOYE doesn't even know what they are."})
|
||||||
|
else if (rule.unsupported) return pugSync.render(event, "explain.pug", {msg: "You cannot bridge to " + rule.humanName + " (type-" + type + " channels) because: " + rule.unsupported})
|
||||||
|
else return pugSync.render(event, "explain.pug", {msg: "You can bridge to " + rule.humanName + " (type-" + type + " channels) just fine. Why are you even here?"})
|
||||||
|
}))
|
||||||
|
|
||||||
as.router.get("/qr", defineEventHandler(async event => {
|
as.router.get("/qr", defineEventHandler(async event => {
|
||||||
const {guild_id} = await getValidatedQuery(event, schema.qr.parse)
|
const {guild_id} = await getValidatedQuery(event, schema.qr.parse)
|
||||||
const managed = await auth.getManagedGuilds(event)
|
const managed = await auth.getManagedGuilds(event)
|
||||||
|
|
@ -267,3 +303,4 @@ as.router.post("/api/invite", defineEventHandler(async event => {
|
||||||
|
|
||||||
module.exports._getPosition = getPosition
|
module.exports._getPosition = getPosition
|
||||||
module.exports.getInviteTargetSpaces = getInviteTargetSpaces
|
module.exports.getInviteTargetSpaces = getInviteTargetSpaces
|
||||||
|
module.exports.linkRules = linkRules
|
||||||
|
|
|
||||||
|
|
@ -173,8 +173,9 @@ as.router.post("/api/link", defineEventHandler(async event => {
|
||||||
const row = from("channel_room").select("channel_id", "room_id").and("WHERE channel_id = ? OR room_id = ?").get(channel.id, parsedBody.matrix)
|
const row = from("channel_room").select("channel_id", "room_id").and("WHERE channel_id = ? OR room_id = ?").get(channel.id, parsedBody.matrix)
|
||||||
if (row) throw createError({status: 400, message: "Bad Request", data: `Channel ID ${row.channel_id} or room ID ${parsedBody.matrix} are already bridged and cannot be reused`})
|
if (row) throw createError({status: 400, message: "Bad Request", data: `Channel ID ${row.channel_id} or room ID ${parsedBody.matrix} are already bridged and cannot be reused`})
|
||||||
|
|
||||||
// Check room is part of the guild's space
|
// Check whether the room is an actual room or a space, and if it's a part of the guild's space
|
||||||
let foundRoom = false
|
let foundRoom = false
|
||||||
|
let foundSpace = false
|
||||||
/** @type {string[]?} */
|
/** @type {string[]?} */
|
||||||
let foundVia = null
|
let foundVia = null
|
||||||
for await (const room of api.generateFullHierarchy(spaceID)) {
|
for await (const room of api.generateFullHierarchy(spaceID)) {
|
||||||
|
|
@ -186,13 +187,21 @@ as.router.post("/api/link", defineEventHandler(async event => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// When finding a room during iteration, see if it was the requested room (to confirm that the room is in the space)
|
// When finding a room during iteration, see if it was the requested room (to confirm that the room is in the space)
|
||||||
if (room.room_id === parsedBody.matrix && !room.room_type) {
|
if (room.room_id === parsedBody.matrix) {
|
||||||
foundRoom = true
|
foundRoom = true
|
||||||
|
// And also, now that we know that the room object is our intended room - we can test for its type.
|
||||||
|
if (room.room_type && room.room_type === "m.space") foundSpace = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (foundRoom && foundVia) break
|
if (foundRoom && foundVia) break
|
||||||
}
|
}
|
||||||
if (!foundRoom) throw createError({status: 400, message: "Bad Request", data: "Matrix room needs to be part of the bridged space"})
|
if (!foundRoom) throw createError({status: 400, message: "Bad Request", data: "Matrix room needs to be part of the bridged space"})
|
||||||
|
|
||||||
|
// Ensure link rules are upheld
|
||||||
|
const rule = guildRoute.linkRules.get(channel.type)
|
||||||
|
if (!rule || rule.unsupported) throw createError({status:400, message: "Bad Request", data: "You cannot bridge to " + (rule ? (rule.humanName+" (type-"+channel.type+" channels)") : ("unknown-type ("+channel.type+") channels")) + " because: " + (rule ? rule.unsupported : "OOYE doesn't even know what they are.")})
|
||||||
|
else if (foundSpace && rule.type === "TYPING") throw createError({status: 400, message: "Bad Request", data: "Matrix room cannot be of type m.space when bridging to "+rule.humanName})
|
||||||
|
else if (!foundSpace && rule.type === "SPACE") throw createError({status: 400, message: "Bad Request", data: "Matrix room must be of type m.space when bridging to "+rule.humanName})
|
||||||
|
|
||||||
// Check room exists and bridge is joined
|
// Check room exists and bridge is joined
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -95,12 +95,14 @@ WITH a (message_id, channel_id) AS (VALUES
|
||||||
('1381212840957972480', '112760669178241024'),
|
('1381212840957972480', '112760669178241024'),
|
||||||
('1401760355339862066', '112760669178241024'),
|
('1401760355339862066', '112760669178241024'),
|
||||||
('1439351590262800565', '1438284564815548418'),
|
('1439351590262800565', '1438284564815548418'),
|
||||||
('1404133238414376971', '112760669178241024'))
|
('1404133238414376971', '112760669178241024'),
|
||||||
|
('1162005314908999790', '1100319550446252084'))
|
||||||
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
|
||||||
('$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg', 'm.room.message', 'm.text', '1126786462646550579', 0, 0, 1),
|
('$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg', 'm.room.message', 'm.text', '1126786462646550579', 0, 0, 1),
|
||||||
('$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4', 'm.room.message', 'm.text', '1128118177155526666', 0, 0, 0),
|
('$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4', 'm.room.message', 'm.text', '1128118177155526666', 0, 0, 0),
|
||||||
|
('$fdD9o7NxMA4VPexlAiIx2CB9JbsiGhJeyJgnZG7U5xg', 'm.room.message', 'm.text', '1162005314908999790', 0, 0, 1),
|
||||||
('$zXSlyI78DQqQwwfPUSzZ1b-nXzbUrCDljJgnGDdoI10', 'm.room.message', 'm.text', '1141619794500649020', 0, 0, 1),
|
('$zXSlyI78DQqQwwfPUSzZ1b-nXzbUrCDljJgnGDdoI10', 'm.room.message', 'm.text', '1141619794500649020', 0, 0, 1),
|
||||||
('$fdD9OZ55xg3EAsfvLZza5tMhtjUO91Wg3Otuo96TplY', 'm.room.message', 'm.text', '1141206225632112650', 0, 0, 1),
|
('$fdD9OZ55xg3EAsfvLZza5tMhtjUO91Wg3Otuo96TplY', 'm.room.message', 'm.text', '1141206225632112650', 0, 0, 1),
|
||||||
('$mtR8cJqM4fKno1bVsm8F4wUVqSntt2sq6jav1lyavuA', 'm.room.message', 'm.text', '1141501302736695316', 0, 1, 1),
|
('$mtR8cJqM4fKno1bVsm8F4wUVqSntt2sq6jav1lyavuA', 'm.room.message', 'm.text', '1141501302736695316', 0, 1, 1),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue