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")} */
|
/** @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,13 +162,24 @@ 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 creation = (async () => {
|
||||||
const {spaceID, channelKState} = await channelToKState(channel, guild)
|
const {spaceID, channelKState} = await channelToKState(channel, guild)
|
||||||
return createRoom(channel, guild, spaceID, channelKState)
|
const roomID = await createRoom(channel, guild, spaceID, channelKState)
|
||||||
} else {
|
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) {
|
if (!shouldActuallySync) {
|
||||||
return existing.room_id // only need to ensure room exists, and it does. return the room ID
|
return existing.room_id // only need to ensure room exists, and it does. return the room ID
|
||||||
}
|
}
|
||||||
|
@ -187,7 +198,6 @@ async function _syncRoom(channelID, shouldActuallySync) {
|
||||||
await Promise.all([roomApply, spaceApply])
|
await Promise.all([roomApply, spaceApply])
|
||||||
|
|
||||||
return existing.room_id
|
return existing.room_id
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _unbridgeRoom(channelID) {
|
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-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)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
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.
|
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.
|
||||||
|
|
Loading…
Reference in a new issue