explain inflight thread room creation

This commit is contained in:
Cadence Ember 2023-08-21 17:25:51 +12:00
parent d666c0aedb
commit c22f434c1f
4 changed files with 166 additions and 35 deletions

View file

@ -0,0 +1,44 @@
// @ts-check
const assert = require("assert")
const passthrough = require("../../passthrough")
const { discord, sync, db } = passthrough
/** @type {import("../converters/message-to-event")} */
const messageToEvent = sync.require("../converters/message-to-event")
/** @type {import("../../matrix/api")} */
const api = sync.require("../../matrix/api")
/** @type {import("./register-user")} */
const registerUser = sync.require("./register-user")
/** @type {import("../actions/create-room")} */
const createRoom = sync.require("../actions/create-room")
/**
* @param {string} parentRoomID
* @param {string} threadRoomID
* @param {import("discord-api-types/v10").APIThreadChannel} thread
*/
async function announceThread(parentRoomID, threadRoomID, thread) {
/** @type {string?} */
const creatorMxid = db.prepare("SELECT mxid FROM sim WHERE discord_id = ?").pluck().get(thread.owner_id)
/** @type {string?} */
const branchedFromEventID = db.prepare("SELECT event_id FROM event_message WHERE message_id = ?").get(thread.id)
const msgtype = creatorMxid ? "m.emote" : "m.text"
const template = creatorMxid ? "started a thread:" : "Thread started:"
let body = `${template} ${thread.name} https://matrix.to/#/${threadRoomID}`
let html = `${template} <a href="https://matrix.to/#/${threadRoomID}">${thread.name}</a>`
const mentions = {}
await api.sendEvent(parentRoomID, "m.room.message", {
msgtype,
body: `${template} ,
format: "org.matrix.custom.html",
formatted_body: "",
"m.mentions": mentions
}, creatorMxid)
}
module.exports.announceThread = announceThread

View file

@ -12,6 +12,9 @@ const api = sync.require("../../matrix/api")
/** @type {import("../../matrix/kstate")} */ /** @type {import("../../matrix/kstate")} */
const ks = sync.require("../../matrix/kstate") const ks = sync.require("../../matrix/kstate")
/** @type {Map<string, Promise<string>>} channel ID -> Promise<room ID> */
const inflightRoomCreate = new Map()
/** /**
* @param {string} roomID * @param {string} roomID
*/ */
@ -98,23 +101,20 @@ async function channelToKState(channel, guild) {
* @returns {Promise<string>} room ID * @returns {Promise<string>} room ID
*/ */
async function createRoom(channel, guild, spaceID, kstate) { async function createRoom(channel, guild, spaceID, kstate) {
let threadParent = null
if (channel.type === DiscordTypes.ChannelType.PublicThread) threadParent = channel.parent_id
const invite = threadParent ? [] : ["@cadence:cadence.moe"] // TODO
const [convertedName, convertedTopic] = convertNameAndTopic(channel, guild, null) const [convertedName, convertedTopic] = convertNameAndTopic(channel, guild, null)
const roomID = await api.createRoom({ const roomID = await api.createRoom({
name: convertedName, name: convertedName,
topic: convertedTopic, topic: convertedTopic,
preset: "private_chat", preset: "private_chat",
visibility: "private", visibility: "private",
invite: ["@cadence:cadence.moe"], // TODO invite,
initial_state: ks.kstateToState(kstate) initial_state: ks.kstateToState(kstate)
}) })
let threadParent = null
if (channel.type === DiscordTypes.ChannelType.PublicThread) {
/** @type {DiscordTypes.APIThreadChannel} */ // @ts-ignore
const thread = channel
threadParent = thread.parent_id
}
db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent) VALUES (?, ?, ?, NULL, ?)").run(channel.id, roomID, channel.name, threadParent) db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent) VALUES (?, ?, ?, NULL, ?)").run(channel.id, roomID, channel.name, threadParent)
// Put the newly created child into the space // Put the newly created child into the space
@ -162,32 +162,42 @@ async function _syncRoom(channelID, shouldActuallySync) {
assert.ok(channel) assert.ok(channel)
const guild = channelToGuild(channel) const guild = channelToGuild(channel)
if (inflightRoomCreate.has(channelID)) {
await inflightRoomCreate.get(channelID) // just waiting, and then doing a new db query afterwards, is the simplest way of doing it
}
/** @type {{room_id: string, thread_parent: string?}} */ /** @type {{room_id: string, thread_parent: string?}} */
const existing = db.prepare("SELECT room_id, thread_parent from channel_room WHERE channel_id = ?").get(channelID) const existing = db.prepare("SELECT room_id, thread_parent from channel_room WHERE channel_id = ?").get(channelID)
if (!existing) { if (!existing) {
const {spaceID, channelKState} = await channelToKState(channel, guild) const creation = (async () => {
return createRoom(channel, guild, spaceID, channelKState) const {spaceID, channelKState} = await channelToKState(channel, guild)
} else { const roomID = await createRoom(channel, guild, spaceID, channelKState)
if (!shouldActuallySync) { inflightRoomCreate.delete(channelID) // OK to release inflight waiters now. they will read the correct `existing` row
return existing.room_id // only need to ensure room exists, and it does. return the room ID return roomID
} })()
inflightRoomCreate.set(channelID, creation)
console.log(`[room sync] to matrix: ${channel.name}`) return creation // Naturally, the newly created room is already up to date, so we can always skip syncing here.
const {spaceID, channelKState} = await channelToKState(channel, guild)
// sync channel state to room
const roomKState = await roomToKState(existing.room_id)
const roomDiff = ks.diffKState(roomKState, channelKState)
const roomApply = applyKStateDiffToRoom(existing.room_id, roomDiff)
// sync room as space member
const spaceApply = _syncSpaceMember(channel, spaceID, existing.room_id)
await Promise.all([roomApply, spaceApply])
return existing.room_id
} }
if (!shouldActuallySync) {
return existing.room_id // only need to ensure room exists, and it does. return the room ID
}
console.log(`[room sync] to matrix: ${channel.name}`)
const {spaceID, channelKState} = await channelToKState(channel, guild)
// sync channel state to room
const roomKState = await roomToKState(existing.room_id)
const roomDiff = ks.diffKState(roomKState, channelKState)
const roomApply = applyKStateDiffToRoom(existing.room_id, roomDiff)
// sync room as space member
const spaceApply = _syncSpaceMember(channel, spaceID, existing.room_id)
await Promise.all([roomApply, spaceApply])
return existing.room_id
} }
async function _unbridgeRoom(channelID) { async function _unbridgeRoom(channelID) {

View file

@ -102,14 +102,17 @@ module.exports = {
}, },
/** /**
* Announces to the parent room that the thread room has been created.
* See notes.md, "Ignore MESSAGE_UPDATE and bridge THREAD_CREATE as the announcement"
* @param {import("./discord-client")} client * @param {import("./discord-client")} client
* @param {import("discord-api-types/v10").APIChannel} thread * @param {import("discord-api-types/v10").APIThreadChannel} thread
*/ */
async onThreadCreate(client, thread) { async onThreadCreate(client, thread) {
console.log(thread) console.log(thread)
const parentRoomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").get(thread.parent_id) const parentRoomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").get(thread.parent_id)
if (!parentRoomID) return // Not interested in a thread if we aren't interested in its wider channel if (!parentRoomID) return // Not interested in a thread if we aren't interested in its wider channel
await createRoom.syncRoom(thread.id) const threadRoomID = await createRoom.syncRoom(thread.id) // Create room (will share the same inflight as the initial message to the thread)
await announceThread.announceThread(parentRoomID, threadRoomID, thread)
}, },
/** /**

View file

@ -19,10 +19,48 @@ There needs to be a way to easily manually trigger something later. For example,
When viewing this thread, it shows the message branched from at the top, and then the first "real" message right underneath, as separate groups. When viewing this thread, it shows the message branched from at the top, and then the first "real" message right underneath, as separate groups.
### Problem 1
If THREAD_CREATE creates the matrix room, this will still be in-flight when MESSAGE_CREATE ensures the room exists and creates a room too. There will be two rooms created and the bridge falls over.
#### Possible solution: Ignore THREAD_CREATE
Then the room will be implicitly created by the two MESSAGE_CREATEs, which are in series.
#### Possible solution: Store in-flight room creations ✔️
Then the room will definitely only be created once, and we can still handle both events if we want to do special things for THREAD_CREATE.
#### Possible solution: Don't implicitly create rooms
But then old and current threads would never have their messages bridged unless I manually intervene. Don't like that.
### Problem 2
MESSAGE_UPDATE with flags=32 is telling that message to become an announcement of the new thread's creation, but this happens before THREAD_CREATE. The matrix room won't actually exist when we see MESSAGE_UPDATE, therefore we cannot make the MESSAGE_UPDATE link to the new thread.
#### Possible solution: Ignore MESSAGE_UPDATE and bridge THREAD_CREATE as the announcement ✔️
When seeing THREAD_CREATE (if we use solution B above) we could react to it by creating the thread announcement message in the parent channel. This is possible because THREAD_CREATE gives a thread object and that includes the parent channel ID to send the announcement message to.
While the thread announcement message could look more like Discord-side by being an edit of the message it branched off:
> look at my cat
>
> Thread started: [#cat thread]
if the thread branched off a matrix user's message then the bridge wouldn't be able to edit it, so this wouldn't work.
Regardless, it would make the most sense to post a new message like this to the parent room:
> > Reply to: look at my cat
>
> [me] started a new thread: [#cat thread]
## Current manual process for setting up a server ## Current manual process for setting up a server
1. Call createSpace.createSpace(discord.guilds.get(GUILD_ID)) 1. Call createSpace.createSpace(discord.guilds.get(GUILD_ID))
2. Call createRoom.createAllForGuild(GUILD_ID) 2. Call createRoom.createAllForGuild(GUILD_ID) // TODO: Only create rooms that the bridge bot has read permissions in!
3. Edit source code of event-dispatcher.js isGuildAllowed() and add the guild ID to the list 3. Edit source code of event-dispatcher.js isGuildAllowed() and add the guild ID to the list
4. If developing, make sure SSH port forward is activated, then wait for events to sync over! 4. If developing, make sure SSH port forward is activated, then wait for events to sync over!
@ -101,9 +139,9 @@ Can use custom transaction ID (?) to send the original timestamps to Matrix. See
## Webhook message sent ## Webhook message sent
- Consider using the _ooye_bot account to send all webhook messages to prevent extraneous joins? - Consider using the _ooye_bot account to send all webhook messages to prevent extraneous joins?
- Downside: the profile information from the most recently sent message would stick around in the member list. This is toleable. - Downside: the profile information from the most recently sent message would stick around in the member list. This is tolerable.
- Otherwise, could use an account per webhook ID, but if webhook IDs are often deleted and re-created, this could still end up leaving too many accounts in the room. - Otherwise, could use an account per webhook ID, but if webhook IDs are often deleted and re-created, this could still end up leaving too many accounts in the room.
- The original bridge uses an account per webhook display name, which does the most sense in terms of canonical accounts, but leaves too many accounts in the room. - The original bridge uses an account per webhook display name, which makes the most sense in terms of canonical accounts, but leaves too many accounts in the room.
## Message deleted ## Message deleted
@ -113,7 +151,9 @@ Can use custom transaction ID (?) to send the original timestamps to Matrix. See
## Message edited / embeds added ## Message edited / embeds added
1. Look up equivalents on matrix. 1. Look up equivalents on matrix.
2. Replace content on matrix. 2. Transform content.
3. Build replacement event with fallbacks.
4. Send to matrix.
## Reaction added ## Reaction added
@ -148,3 +188,37 @@ Can use custom transaction ID (?) to send the original timestamps to Matrix. See
3. The emojis may now be sent by Matrix users! 3. The emojis may now be sent by Matrix users!
TOSPEC: m2d emoji uploads?? TOSPEC: m2d emoji uploads??
## Issues if the bridge database is rolled back
### channel_room table
- Duplicate rooms will be created on matrix.
### sim table
- Sims will already be registered, registration will fail, all events from those sims will fail.
### sim_member table
- Sims won't be invited because they are already joined, all events from those sims will fail.
### guild_space table
- channelToKState will fail, so channel data differences won't be calculated, so channel/thread creation and sync will fail.
### event_message table
- Events referenced by other events will be dropped, for example
- edits will be ignored
- deletes will be ignored
- reactions will be ignored
- replies won't generate a reply
### file
- Some files like avatars may be re-uploaded to the matrix content repository, secretly taking more storage space on the server.
### webhook
- Some duplicate webhooks may be created.