diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index 0fb102b..2a76f56 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -35,6 +35,7 @@ const PRIVACY_ENUMS = { ROOM_HISTORY_VISIBILITY: ["shared", "shared", "world_readable"], // any events sent after 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 SPACE_JOIN_RULES: ["invite", "public", "public"], + /** @type {import("../../types").JoinRule[]} */ ROOM_JOIN_RULES: ["restricted", "public", "public"] } @@ -88,7 +89,7 @@ async function channelToKState(channel, guild, di) { 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. */ 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) assert(typeof parentSpaceID === "string") } @@ -111,7 +112,7 @@ async function channelToKState(channel, guild, di) { let history_visibility = PRIVACY_ENUMS.ROOM_HISTORY_VISIBILITY[privacyLevel] 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 = { join_rule: "restricted", allow: [{ @@ -119,6 +120,13 @@ async function channelToKState(channel, guild, di) { 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") { join_rules = {join_rule: PRIVACY_ENUMS.ROOM_JOIN_RULES[privacyLevel]} } diff --git a/src/types.d.ts b/src/types.d.ts index 494cba2..db209ce 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -505,6 +505,8 @@ export namespace R { export type Membership = "invite" | "knock" | "join" | "leave" | "ban" +export type JoinRule = "public" | "knock" | "invite" | "private" | "restricted" | "knock_restricted" + export type Pagination = { chunk: T[] next_batch?: string diff --git a/src/web/pug/explain.pug b/src/web/pug/explain.pug new file mode 100644 index 0000000..d280ba5 --- /dev/null +++ b/src/web/pug/explain.pug @@ -0,0 +1,5 @@ +extends includes/template.pug + +block body + .ta-center.wmx5.p48.mx-auto#ok + p.mt24.fs-body2= msg diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index 9791ae3..990477a 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -238,6 +238,8 @@ block body ul.my8.ml24 each row in removedWrongTypeChannels li: a(href=`https://discord.com/channels/${guild_id}/${row.id}`) (#{row.type}) #{row.name} + "|" + a(href=`/explain?type=${row.type}`) Why? h3.mt24 Unavailable channels: Discord bot can't access .s-card.p0 ul.my8.ml24 @@ -254,13 +256,13 @@ block body ul.my8.ml24 each row in removedEncryptedRooms li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name - h3.mt24 Unavailable rooms: Wrong type - .s-card.p0 - ul.my8.ml24 - each row in removedWrongTypeRooms - li: a(href=`https://matrix.to/#/${row.room_id}`) (#{row.room_type}) #{row.name} - h3.mt24 Unavailable rooms: Archived thread - .s-card.p0 - ul.my8.ml24 - each row in removedArchivedThreadRooms - li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name +// h3.mt24 Unavailable rooms: Wrong type +// .s-card.p0 +// ul.my8.ml24 +// each row in removedWrongTypeRooms +// li: a(href=`https://matrix.to/#/${row.room_id}`) (#{row.room_type}) #{row.name} +// h3.mt24 Unavailable rooms: Archived thread +// .s-card.p0 +// ul.my8.ml24 +// each row in removedArchivedThreadRooms +// li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name diff --git a/src/web/routes/guild.js b/src/web/routes/guild.js index 94f1a5c..7a7c4ac 100644 --- a/src/web/routes/guild.js +++ b/src/web/routes/guild.js @@ -38,6 +38,9 @@ const schema = { }), inviteNonce: z.object({ nonce: z.string() + }), + explain: z.object({ + type: z.number() }) } @@ -53,6 +56,27 @@ function getAPI(event) { /** @type {LRUCache} nonce to guild id */ 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}*/ +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"}], //Currently testing: Letting users bridge threads however they like. In case this doesn't work: , unsupported: "Threads must be bridged automatically, to ensure proper lifecycle management (so that archiving threads won't break them). Please send a message in this thread to bridge it automatically." + [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 {Map} channels @@ -112,7 +136,10 @@ function getChannelRoomsLinks(guild, rooms, roles) { let unlinkedChannelIDs = channelIDs.filter(c => !linkedChannelIDs.includes(c)) /** @type {DiscordTypes.APIGuildChannel[]} */ // @ts-ignore let unlinkedChannels = unlinkedChannelIDs.map(c => discord.channels.get(c)) - let removedWrongTypeChannels = dUtils.filterTo(unlinkedChannels, c => c && [0, 2, 5, 13, 15, 16].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 => { const permissions = dUtils.getPermissions(guild.id, roles, guild.roles, botID, c["permission_overwrites"]) return dUtils.hasSomePermissions(permissions, ["Administrator", "ViewChannel"]) @@ -122,11 +149,11 @@ function getChannelRoomsLinks(guild, rooms, roles) { let linkedRoomIDs = linkedChannels.map(c => c.room_id) let unlinkedRooms = [...rooms] let removedLinkedRooms = dUtils.filterTo(unlinkedRooms, r => !linkedRoomIDs.includes(r.room_id)) - let removedWrongTypeRooms = dUtils.filterTo(unlinkedRooms, r => !(r.room_type && r.room_type === "m.space")) + let removedWrongTypeRooms = [] let removedEncryptedRooms = dUtils.filterTo(unlinkedRooms, r => !r.encryption && !r["im.nheko.summary.encryption"]) // 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 - let removedArchivedThreadRooms = dUtils.filterTo(unlinkedRooms, r => r.name && !r.name.match(/^\[(🔒)?⛓️\]/)) + let removedArchivedThreadRooms = [] //dUtils.filterTo(unlinkedRooms, r => r.name && !r.name.match(/^\[(🔒)?⛓️\]/)) COMMENTED OUT - Currently testing: Letting users bridge threads however they like. return { linkedChannelsWithDetails, unlinkedChannels, unlinkedRooms, @@ -182,6 +209,12 @@ as.router.get("/guild", defineEventHandler(async event => { 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 msg = type+" is unsupported" //TODO: actually explain (I'm sure I messed something up anyway, I'll leave it for now) + return pugSync.render(event, "explain.pug", {msg}) +})) + as.router.get("/qr", defineEventHandler(async event => { const {guild_id} = await getValidatedQuery(event, schema.qr.parse) const managed = await auth.getManagedGuilds(event) @@ -267,3 +300,4 @@ as.router.post("/api/invite", defineEventHandler(async event => { module.exports._getPosition = getPosition module.exports.getInviteTargetSpaces = getInviteTargetSpaces +module.exports.linkRules = linkRules diff --git a/src/web/routes/link.js b/src/web/routes/link.js index cf01f9f..2992249 100644 --- a/src/web/routes/link.js +++ b/src/web/routes/link.js @@ -173,7 +173,7 @@ 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) 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 an actual room (not space) and 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 foundSpace = false /** @type {string[]?} */ @@ -198,7 +198,12 @@ as.router.post("/api/link", defineEventHandler(async event => { } } if (!foundRoom) throw createError({status: 400, message: "Bad Request", data: "Matrix room needs to be part of the bridged space"}) - else if (!foundRoom) throw createError({status: 400, message: "Bad Request", data: "Matrix room cannot be of type m.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 " + (rule ? (rule.humanName+"(type-" + channel.type+" channels)") : ("unknown-type ("+channel.type+") channels")) + " because: " + (rule ? rule.unsupported : "OOYE doesn't even know what they are yet.")}) + 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 try {