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.
|
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:
|
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.
|
- 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 "easy mode" web button, REPLACE INTO state 1 and ensureSpace.
|
||||||
- When bot is added through "self-service" web button, REPLACE INTO state 0.
|
- 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.
|
- 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": {
|
"node_modules/better-sqlite3": {
|
||||||
"version": "11.2.1",
|
"version": "11.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.3.0.tgz",
|
||||||
"integrity": "sha512-Xbt1d68wQnUuFIEVsbt6V+RG30zwgbtCGQ4QOcXVrOH0FE4eHk64FWZ9NUfRHS4/x1PXqwz/+KOrnXD7f0WieA==",
|
"integrity": "sha512-iHt9j8NPYF3oKCNOO5ZI4JwThjt3Z6J6XrcwG85VNMVzv1ByqrHWv5VILEbCMFWDsoHhXvQ7oC8vgRXFAKgl9w==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1633,9 +1633,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/discord-api-types": {
|
"node_modules/discord-api-types": {
|
||||||
"version": "0.37.98",
|
"version": "0.37.101",
|
||||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.98.tgz",
|
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.101.tgz",
|
||||||
"integrity": "sha512-xsH4UwmnCQl4KjAf01/p9ck9s+/vDqzHbUxPOBzo8fcVUa/hQG6qInD7Cr44KAuCM+xCxGJFSAUx450pBrX0+g==",
|
"integrity": "sha512-2wizd94t7G3A8U5Phr3AiuL4gSvhqistDwWnlk1VLTit8BI1jWUncFqFQNdPbHqS3661+Nx/iEyIwtVjPuBP3w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/doctypes": {
|
"node_modules/doctypes": {
|
||||||
|
|
|
@ -15,8 +15,6 @@ const api = sync.require("../../matrix/api")
|
||||||
const ks = sync.require("../../matrix/kstate")
|
const ks = sync.require("../../matrix/kstate")
|
||||||
/** @type {import("../../discord/utils")} */
|
/** @type {import("../../discord/utils")} */
|
||||||
const utils = sync.require("../../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:
|
* There are 3 levels of room privacy:
|
||||||
|
@ -95,27 +93,21 @@ function convertNameAndTopic(channel, guild, customName) {
|
||||||
async function channelToKState(channel, guild, di) {
|
async function channelToKState(channel, guild, di) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const parentChannel = discord.channels.get(channel.parent_id)
|
const parentChannel = discord.channels.get(channel.parent_id)
|
||||||
/** Used for membership/permission checks. */
|
const guildRow = select("guild_space", ["space_id", "privacy_level"], {guild_id: guild.id}).get()
|
||||||
let guildSpaceID
|
assert(guildRow)
|
||||||
/** 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 row = select("channel_room", ["nick", "custom_avatar"], {channel_id: channel.id}).get()
|
/** Used for membership/permission checks. */
|
||||||
const customName = row?.nick
|
let guildSpaceID = guildRow.space_id
|
||||||
const customAvatar = row?.custom_avatar
|
/** 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 [convertedName, convertedTopic] = convertNameAndTopic(channel, guild, customName)
|
||||||
|
|
||||||
const avatarEventContent = {}
|
const avatarEventContent = {}
|
||||||
|
@ -125,6 +117,7 @@ async function channelToKState(channel, guild, di) {
|
||||||
avatarEventContent.url = {$url: file.guildIcon(guild)}
|
avatarEventContent.url = {$url: file.guildIcon(guild)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const privacyLevel = guildRow.privacy_level
|
||||||
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"
|
||||||
|
|
||||||
|
@ -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
|
* @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) {
|
function existsOrAutocreatable(channel, guildID) {
|
||||||
const existing = select("channel_room", ["room_id", "thread_parent"], {channel_id: channelID}).get()
|
// 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
|
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()
|
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
|
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:
|
Ensure flow:
|
||||||
1. Get IDs
|
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
|
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)
|
const existing = assertExistsOrAutocreatable(channel, 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")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existing === 1) {
|
if (existing === 1) {
|
||||||
const creation = (async () => {
|
const creation = (async () => {
|
||||||
|
@ -475,3 +504,5 @@ module.exports.postApplyPowerLevels = postApplyPowerLevels
|
||||||
module.exports._convertNameAndTopic = convertNameAndTopic
|
module.exports._convertNameAndTopic = convertNameAndTopic
|
||||||
module.exports._unbridgeRoom = _unbridgeRoom
|
module.exports._unbridgeRoom = _unbridgeRoom
|
||||||
module.exports.unbridgeDeletedChannel = unbridgeDeletedChannel
|
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()
|
const row = select("guild_space", ["space_id", "privacy_level"], {guild_id: guild.id}).get()
|
||||||
|
|
||||||
if (!row) {
|
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 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 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)
|
const spaceID = await createSpace(guild, guildKState)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
const DiscordTypes = require("discord-api-types/v10")
|
const DiscordTypes = require("discord-api-types/v10")
|
||||||
|
const Ty = require("../../types")
|
||||||
const assert = require("assert/strict")
|
const assert = require("assert/strict")
|
||||||
const {discord, sync, db, select, from} = require("../../passthrough")
|
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")
|
const api = sync.require("../../matrix/api")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction
|
* @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction & {channel: DiscordTypes.APIGuildTextChannel}} interaction
|
||||||
* @returns {Promise<DiscordTypes.APIInteractionResponse>}
|
* @returns {Promise<DiscordTypes.APIInteractionResponse>}
|
||||||
*/
|
*/
|
||||||
async function _interact({data, channel, guild_id}) {
|
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
|
// Ensure guild and room are bridged
|
||||||
db.prepare("INSERT OR IGNORE INTO guild_active (guild_id, autocreate) VALUES (?, 1)").run(guild_id)
|
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)
|
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
|
// Check for existing invite to the space
|
||||||
let spaceMember
|
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) {
|
async function interact(interaction) {
|
||||||
await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction))
|
await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction))
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,7 @@ module.exports = {
|
||||||
network: {
|
network: {
|
||||||
id: "112760669178241024",
|
id: "112760669178241024",
|
||||||
displayname: "Psychonauts 3",
|
displayname: "Psychonauts 3",
|
||||||
avatar_url: "mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF"
|
avatar_url: {$url: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024"}
|
||||||
},
|
},
|
||||||
channel: {
|
channel: {
|
||||||
id: "112760669178241024",
|
id: "112760669178241024",
|
||||||
|
|
Loading…
Reference in a new issue