Adapt createRoom/space/invite to self-service
This commit is contained in:
parent
312ea69d73
commit
b0a0e62a86
6 changed files with 96 additions and 46 deletions
|
@ -61,6 +61,8 @@ So there will be 3 states of whether a guild is self-service or not. At first, i
|
|||
|
||||
Pressing buttons on web or using the /invite command on a guild will insert a row into guild_active, allowing it to be bridged.
|
||||
|
||||
One more thing. Before v3, when a Matrix room was autocreated it would autocreate the space as well, if it needed to. But now, since nothing will be created until the user takes an action, the guild will always be created directly in response to a request. So room creation can now trust that the guild exists already.
|
||||
|
||||
So here's all the technical changes needed to support self-service in v3:
|
||||
|
||||
- New guild_active table showing whether, and how, a guild is bridged.
|
||||
|
@ -68,3 +70,5 @@ So here's all the technical changes needed to support self-service in v3:
|
|||
- When bot is added through "easy mode" web button, REPLACE INTO state 1 and ensureSpace.
|
||||
- When bot is added through "self-service" web button, REPLACE INTO state 0.
|
||||
- Event dispatcher will only ensureRoom if the guild_active state is 1.
|
||||
- createRoom can trust that the space exists because we check that in a calling function.
|
||||
- createRoom will only create other dependencies if the guild is autocreate.
|
||||
|
|
12
package-lock.json
generated
12
package-lock.json
generated
|
@ -1217,9 +1217,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "11.2.1",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.2.1.tgz",
|
||||
"integrity": "sha512-Xbt1d68wQnUuFIEVsbt6V+RG30zwgbtCGQ4QOcXVrOH0FE4eHk64FWZ9NUfRHS4/x1PXqwz/+KOrnXD7f0WieA==",
|
||||
"version": "11.3.0",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.3.0.tgz",
|
||||
"integrity": "sha512-iHt9j8NPYF3oKCNOO5ZI4JwThjt3Z6J6XrcwG85VNMVzv1ByqrHWv5VILEbCMFWDsoHhXvQ7oC8vgRXFAKgl9w==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -1633,9 +1633,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/discord-api-types": {
|
||||
"version": "0.37.98",
|
||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.98.tgz",
|
||||
"integrity": "sha512-xsH4UwmnCQl4KjAf01/p9ck9s+/vDqzHbUxPOBzo8fcVUa/hQG6qInD7Cr44KAuCM+xCxGJFSAUx450pBrX0+g==",
|
||||
"version": "0.37.101",
|
||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.101.tgz",
|
||||
"integrity": "sha512-2wizd94t7G3A8U5Phr3AiuL4gSvhqistDwWnlk1VLTit8BI1jWUncFqFQNdPbHqS3661+Nx/iEyIwtVjPuBP3w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/doctypes": {
|
||||
|
|
|
@ -15,8 +15,6 @@ const api = sync.require("../../matrix/api")
|
|||
const ks = sync.require("../../matrix/kstate")
|
||||
/** @type {import("../../discord/utils")} */
|
||||
const utils = sync.require("../../discord/utils")
|
||||
/** @type {import("./create-space")}) */
|
||||
const createSpace = sync.require("./create-space") // watch out for the require loop
|
||||
|
||||
/**
|
||||
* There are 3 levels of room privacy:
|
||||
|
@ -95,27 +93,21 @@ function convertNameAndTopic(channel, guild, customName) {
|
|||
async function channelToKState(channel, guild, di) {
|
||||
// @ts-ignore
|
||||
const parentChannel = discord.channels.get(channel.parent_id)
|
||||
/** Used for membership/permission checks. */
|
||||
let guildSpaceID
|
||||
/** 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
|
||||
let privacyLevel
|
||||
if (parentChannel?.type === DiscordTypes.ChannelType.GuildForum) { // it's a forum channel's thread, so use a different space to group those threads
|
||||
guildSpaceID = await createSpace.ensureSpace(guild)
|
||||
parentSpaceID = await ensureRoom(channel.parent_id)
|
||||
privacyLevel = select("guild_space", "privacy_level", {space_id: guildSpaceID}).pluck().get()
|
||||
} else { // otherwise use the guild's space like usual
|
||||
parentSpaceID = await createSpace.ensureSpace(guild)
|
||||
guildSpaceID = parentSpaceID
|
||||
privacyLevel = select("guild_space", "privacy_level", {space_id: parentSpaceID}).pluck().get()
|
||||
}
|
||||
assert(typeof parentSpaceID === "string")
|
||||
assert(typeof guildSpaceID === "string")
|
||||
assert(typeof privacyLevel === "number")
|
||||
const guildRow = select("guild_space", ["space_id", "privacy_level"], {guild_id: guild.id}).get()
|
||||
assert(guildRow)
|
||||
|
||||
const row = select("channel_room", ["nick", "custom_avatar"], {channel_id: channel.id}).get()
|
||||
const customName = row?.nick
|
||||
const customAvatar = row?.custom_avatar
|
||||
/** Used for membership/permission checks. */
|
||||
let guildSpaceID = guildRow.space_id
|
||||
/** 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) {
|
||||
parentSpaceID = await ensureRoom(channel.parent_id)
|
||||
assert(typeof parentSpaceID === "string")
|
||||
}
|
||||
|
||||
const channelRow = select("channel_room", ["nick", "custom_avatar"], {channel_id: channel.id}).get()
|
||||
const customName = channelRow?.nick
|
||||
const customAvatar = channelRow?.custom_avatar
|
||||
const [convertedName, convertedTopic] = convertNameAndTopic(channel, guild, customName)
|
||||
|
||||
const avatarEventContent = {}
|
||||
|
@ -125,6 +117,7 @@ async function channelToKState(channel, guild, di) {
|
|||
avatarEventContent.url = {$url: file.guildIcon(guild)}
|
||||
}
|
||||
|
||||
const privacyLevel = guildRow.privacy_level
|
||||
let history_visibility = PRIVACY_ENUMS.ROOM_HISTORY_VISIBILITY[privacyLevel]
|
||||
if (channel["thread_metadata"]) history_visibility = "world_readable"
|
||||
|
||||
|
@ -280,16 +273,60 @@ function channelToGuild(channel) {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {string} channelID
|
||||
* This function handles whether it's allowed to bridge messages in this channel, and if so, where to.
|
||||
* This has to account for whether self-service is enabled for the guild or not.
|
||||
* This also has to account for different channel types, like forum channels (which need the
|
||||
* parent forum to already exist, and ignore the self-service setting), or thread channels (which
|
||||
* need the parent channel to already exist, and ignore the self-service setting).
|
||||
* @param {DiscordTypes.APIGuildTextChannel | DiscordTypes.APIThreadChannel} channel text channel or thread
|
||||
* @param {string} guildID
|
||||
* @returns obj if bridged; 1 if autocreatable; null/undefined if guild is not bridged; 0 if self-service and not autocreatable thread
|
||||
*/
|
||||
function existsOrAutocreatable(channelID, guildID) {
|
||||
const existing = select("channel_room", ["room_id", "thread_parent"], {channel_id: channelID}).get()
|
||||
function existsOrAutocreatable(channel, guildID) {
|
||||
// 1. If the channel is already linked somewhere, it's always okay to bridge to that destination, no matter what. Yippee!
|
||||
const existing = select("channel_room", ["room_id", "thread_parent"], {channel_id: channel.id}).get()
|
||||
if (existing) return existing
|
||||
|
||||
// 2. If the guild is an autocreate guild, it's always okay to bridge to that destination, and
|
||||
// we'll need to create any dependent resources recursively.
|
||||
const autocreate = select("guild_active", "autocreate", {guild_id: guildID}).pluck().get()
|
||||
if (autocreate === 1) return autocreate
|
||||
|
||||
// 3. If the guild is not approved for bridging yet, we can't bridge there.
|
||||
// They need to decide one way or another whether it's self-service before we can continue.
|
||||
if (autocreate == null) return autocreate
|
||||
|
||||
// 4. If we got here, the guild is in self-service mode.
|
||||
// New channels won't be able to create new rooms. But forum threads or channel threads could be fine.
|
||||
if ([DiscordTypes.ChannelType.PublicThread, DiscordTypes.ChannelType.PrivateThread, DiscordTypes.ChannelType.AnnouncementThread].includes(channel.type)) {
|
||||
// In self-service mode, threads rely on the parent resource already existing.
|
||||
/** @type {DiscordTypes.APIGuildTextChannel} */ // @ts-ignore
|
||||
const parent = discord.channels.get(channel.parent_id)
|
||||
assert(parent)
|
||||
const parentExisting = existsOrAutocreatable(parent, guildID)
|
||||
if (parentExisting) return 1 // Autocreatable
|
||||
}
|
||||
|
||||
// 5. If we got here, the guild is in self-service mode and the channel is truly not bridged.
|
||||
return autocreate
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DiscordTypes.APIGuildTextChannel | DiscordTypes.APIThreadChannel} channel text channel or thread
|
||||
* @param {string} guildID
|
||||
* @returns obj if bridged; 1 if autocreatable. (throws if not autocreatable)
|
||||
*/
|
||||
function assertExistsOrAutocreatable(channel, guildID) {
|
||||
const existing = existsOrAutocreatable(channel, guildID)
|
||||
if (existing === 0) {
|
||||
throw new Error(`Guild ${guildID} is self-service, so won't create a Matrix room for channel ${channel.id}`)
|
||||
}
|
||||
if (!existing) {
|
||||
throw new Error(`Guild ${guildID} is not bridged, so won't create a Matrix room for channel ${channel.id}`)
|
||||
}
|
||||
return existing
|
||||
}
|
||||
|
||||
/*
|
||||
Ensure flow:
|
||||
1. Get IDs
|
||||
|
@ -323,15 +360,7 @@ async function _syncRoom(channelID, shouldActuallySync) {
|
|||
await inflightRoomCreate.get(channelID) // just waiting, and then doing a new db query afterwards, is the simplest way of doing it
|
||||
}
|
||||
|
||||
const existing = existsOrAutocreatable(channelID, guild.id)
|
||||
|
||||
if (existing === 0) {
|
||||
throw new Error("refusing to create a new matrix room when autocreate is deactivated")
|
||||
}
|
||||
|
||||
if (!existing) {
|
||||
throw new Error("refusing to craete a new matrix room when there is no guild_active entry")
|
||||
}
|
||||
const existing = assertExistsOrAutocreatable(channel, guild.id)
|
||||
|
||||
if (existing === 1) {
|
||||
const creation = (async () => {
|
||||
|
@ -475,3 +504,5 @@ module.exports.postApplyPowerLevels = postApplyPowerLevels
|
|||
module.exports._convertNameAndTopic = convertNameAndTopic
|
||||
module.exports._unbridgeRoom = _unbridgeRoom
|
||||
module.exports.unbridgeDeletedChannel = unbridgeDeletedChannel
|
||||
module.exports.assertExistsOrAutocreatable = assertExistsOrAutocreatable
|
||||
module.exports.existsOrAutocreatable = existsOrAutocreatable
|
||||
|
|
|
@ -92,6 +92,9 @@ async function _syncSpace(guild, shouldActuallySync) {
|
|||
const row = select("guild_space", ["space_id", "privacy_level"], {guild_id: guild.id}).get()
|
||||
|
||||
if (!row) {
|
||||
const autocreate = select("guild_active", "autocreate", {guild_id: guild.id}).pluck().get()
|
||||
assert.equal(autocreate, 1, `refusing to implicitly create guild ${guild.id}. set the guild_active data first before calling ensureSpace/syncSpace.`)
|
||||
|
||||
const creation = (async () => {
|
||||
const guildKState = await guildToKState(guild, createRoom.DEFAULT_PRIVACY_LEVEL) // New spaces will have to use the default privacy level; we obviously can't look up the existing entry
|
||||
const spaceID = await createSpace(guild, guildKState)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// @ts-check
|
||||
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const Ty = require("../../types")
|
||||
const assert = require("assert/strict")
|
||||
const {discord, sync, db, select, from} = require("../../passthrough")
|
||||
|
||||
|
@ -12,7 +13,7 @@ const createSpace = sync.require("../../d2m/actions/create-space")
|
|||
const api = sync.require("../../matrix/api")
|
||||
|
||||
/**
|
||||
* @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction
|
||||
* @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction & {channel: DiscordTypes.APIGuildTextChannel}} interaction
|
||||
* @returns {Promise<DiscordTypes.APIInteractionResponse>}
|
||||
*/
|
||||
async function _interact({data, channel, guild_id}) {
|
||||
|
@ -29,12 +30,23 @@ async function _interact({data, channel, guild_id}) {
|
|||
}
|
||||
}
|
||||
|
||||
const guild = discord.guilds.get(guild_id)
|
||||
assert(guild)
|
||||
|
||||
// Ensure guild and room are bridged
|
||||
db.prepare("INSERT OR IGNORE INTO guild_active (guild_id, autocreate) VALUES (?, 1)").run(guild_id)
|
||||
const existing = createRoom.existsOrAutocreatable(channel, guild_id)
|
||||
if (existing === 0) return {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: "This channel isn't bridged, so you can't invite Matrix users yet. Try turning on automatic room-creation or link a Matrix room in the website.",
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
}
|
||||
}
|
||||
assert(existing) // can't be null or undefined as we just inserted the guild_active row
|
||||
|
||||
const spaceID = await createSpace.ensureSpace(guild)
|
||||
const roomID = await createRoom.ensureRoom(channel.id)
|
||||
assert(roomID)
|
||||
const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get()
|
||||
assert(spaceID)
|
||||
|
||||
// Check for existing invite to the space
|
||||
let spaceMember
|
||||
|
@ -115,7 +127,7 @@ async function _interactButton({channel, message}) {
|
|||
}
|
||||
}
|
||||
|
||||
/** @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction */
|
||||
/** @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction & {channel: DiscordTypes.APIGuildTextChannel}} interaction */
|
||||
async function interact(interaction) {
|
||||
await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction))
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ module.exports = {
|
|||
network: {
|
||||
id: "112760669178241024",
|
||||
displayname: "Psychonauts 3",
|
||||
avatar_url: "mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF"
|
||||
avatar_url: {$url: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024"}
|
||||
},
|
||||
channel: {
|
||||
id: "112760669178241024",
|
||||
|
|
Loading…
Reference in a new issue