explain inflight thread room creation
This commit is contained in:
parent
d666c0aedb
commit
c22f434c1f
4 changed files with 166 additions and 35 deletions
44
d2m/actions/announce-thread.js
Normal file
44
d2m/actions/announce-thread.js
Normal 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
|
|
@ -12,6 +12,9 @@ const api = sync.require("../../matrix/api")
|
|||
/** @type {import("../../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
|
||||
*/
|
||||
|
@ -98,23 +101,20 @@ async function channelToKState(channel, guild) {
|
|||
* @returns {Promise<string>} room ID
|
||||
*/
|
||||
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 roomID = await api.createRoom({
|
||||
name: convertedName,
|
||||
topic: convertedTopic,
|
||||
preset: "private_chat",
|
||||
visibility: "private",
|
||||
invite: ["@cadence:cadence.moe"], // TODO
|
||||
invite,
|
||||
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)
|
||||
|
||||
// Put the newly created child into the space
|
||||
|
@ -162,32 +162,42 @@ async function _syncRoom(channelID, shouldActuallySync) {
|
|||
assert.ok(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?}} */
|
||||
const existing = db.prepare("SELECT room_id, thread_parent from channel_room WHERE channel_id = ?").get(channelID)
|
||||
|
||||
if (!existing) {
|
||||
const {spaceID, channelKState} = await channelToKState(channel, guild)
|
||||
return createRoom(channel, guild, spaceID, channelKState)
|
||||
} else {
|
||||
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
|
||||
const creation = (async () => {
|
||||
const {spaceID, channelKState} = await channelToKState(channel, guild)
|
||||
const roomID = await createRoom(channel, guild, spaceID, channelKState)
|
||||
inflightRoomCreate.delete(channelID) // OK to release inflight waiters now. they will read the correct `existing` row
|
||||
return roomID
|
||||
})()
|
||||
inflightRoomCreate.set(channelID, creation)
|
||||
return creation // Naturally, the newly created room is already up to date, so we can always skip syncing here.
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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-api-types/v10").APIChannel} thread
|
||||
* @param {import("discord-api-types/v10").APIThreadChannel} thread
|
||||
*/
|
||||
async onThreadCreate(client, thread) {
|
||||
console.log(thread)
|
||||
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
|
||||
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)
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
82
notes.md
82
notes.md
|
@ -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.
|
||||
|
||||
### 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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
|
@ -113,7 +151,9 @@ Can use custom transaction ID (?) to send the original timestamps to Matrix. See
|
|||
## Message edited / embeds added
|
||||
|
||||
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
|
||||
|
||||
|
@ -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!
|
||||
|
||||
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.
|
||||
|
|
Loading…
Reference in a new issue