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
 | ||||||
| 	} | 	} | ||||||
|  | @ -188,7 +199,6 @@ async function _syncRoom(channelID, shouldActuallySync) { | ||||||
| 
 | 
 | ||||||
| 	return existing.room_id | 	return existing.room_id | ||||||
| } | } | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| async function _unbridgeRoom(channelID) { | async function _unbridgeRoom(channelID) { | ||||||
| 	/** @ts-ignore @type {DiscordTypes.APIGuildChannel} */ | 	/** @ts-ignore @type {DiscordTypes.APIGuildChannel} */ | ||||||
|  |  | ||||||
|  | @ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue