Backfill missed pins and pins from the past
This commit is contained in:
		| @@ -1,22 +1,37 @@ | |||||||
| // @ts-check | // @ts-check | ||||||
|  |  | ||||||
| const passthrough = require("../../passthrough") | const passthrough = require("../../passthrough") | ||||||
| const {discord, sync} = passthrough | const {discord, sync, db} = passthrough | ||||||
| /** @type {import("../converters/pins-to-list")} */ | /** @type {import("../converters/pins-to-list")} */ | ||||||
| const pinsToList = sync.require("../converters/pins-to-list") | const pinsToList = sync.require("../converters/pins-to-list") | ||||||
| /** @type {import("../../matrix/api")} */ | /** @type {import("../../matrix/api")} */ | ||||||
| const api = sync.require("../../matrix/api") | const api = sync.require("../../matrix/api") | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @param {string} channelID |  * @template {string | null | undefined} T | ||||||
|  * @param {string} roomID |  * @param {T} timestamp | ||||||
|  |  * @returns {T extends string ? number : null} | ||||||
|  */ |  */ | ||||||
| async function updatePins(channelID, roomID) { | function convertTimestamp(timestamp) { | ||||||
| 	const pins = await discord.snow.channel.getChannelPinnedMessages(channelID) | 	// @ts-ignore | ||||||
| 	const eventIDs = pinsToList.pinsToList(pins) | 	return typeof timestamp === "string" ? Math.floor(new Date(timestamp).getTime() / 1000) : null | ||||||
| 	await api.sendState(roomID, "m.room.pinned_events", "", { |  | ||||||
| 		pinned: eventIDs |  | ||||||
| 	}) |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @param {string} channelID | ||||||
|  |  * @param {string} roomID | ||||||
|  |  * @param {number?} convertedTimestamp | ||||||
|  |  */ | ||||||
|  | async function updatePins(channelID, roomID, convertedTimestamp) { | ||||||
|  | 	const pins = await discord.snow.channel.getChannelPinnedMessages(channelID) | ||||||
|  | 	const eventIDs = pinsToList.pinsToList(pins) | ||||||
|  | 	if (pins.length === eventIDs.length || eventIDs.length) { | ||||||
|  | 		await api.sendState(roomID, "m.room.pinned_events", "", { | ||||||
|  | 			pinned: eventIDs | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | 	db.prepare("UPDATE channel_room SET last_bridged_pin_timestamp = ? WHERE channel_id = ?").run(convertedTimestamp || 0, channelID) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports.convertTimestamp = convertTimestamp | ||||||
| module.exports.updatePins = updatePins | module.exports.updatePins = updatePins | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ test("pins2list: converts known IDs, ignores unknown IDs", t => { | |||||||
| 	const result = pinsToList(data.pins.faked) | 	const result = pinsToList(data.pins.faked) | ||||||
| 	t.deepEqual(result, [ | 	t.deepEqual(result, [ | ||||||
| 		"$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qdFv4", | 		"$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qdFv4", | ||||||
| 		"$mtR8cJqM4fKno1bVsm8F4wUVqSntt2sq6jav1lyavuAno", | 		"$mtR8cJqM4fKno1bVsm8F4wUVqSntt2sq6jav1lyavuA", | ||||||
| 		"$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg" | 		"$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg" | ||||||
| 	]) | 	]) | ||||||
| }) | }) | ||||||
|   | |||||||
| @@ -43,6 +43,7 @@ const utils = { | |||||||
| 			} | 			} | ||||||
| 			if (listen === "full") { | 			if (listen === "full") { | ||||||
| 				eventDispatcher.checkMissedExpressions(message.d) | 				eventDispatcher.checkMissedExpressions(message.d) | ||||||
|  | 				eventDispatcher.checkMissedPins(client, message.d) | ||||||
| 				eventDispatcher.checkMissedMessages(client, message.d) | 				eventDispatcher.checkMissedMessages(client, message.d) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| @@ -94,6 +95,13 @@ const utils = { | |||||||
| 			client.channels.set(message.d.id, message.d) | 			client.channels.set(message.d.id, message.d) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 		} else if (message.t === "CHANNEL_PINS_UPDATE") { | ||||||
|  | 			const channel = client.channels.get(message.d.channel_id) | ||||||
|  | 			if (channel) { | ||||||
|  | 				channel["last_pin_timestamp"] = message.d.last_pin_timestamp | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  |  | ||||||
| 		} else if (message.t === "GUILD_DELETE") { | 		} else if (message.t === "GUILD_DELETE") { | ||||||
| 			client.guilds.delete(message.d.id) | 			client.guilds.delete(message.d.id) | ||||||
| 			const channels = client.guildChannelMap.get(message.d.id) | 			const channels = client.guildChannelMap.get(message.d.id) | ||||||
|   | |||||||
| @@ -25,9 +25,15 @@ const createSpace = sync.require("./actions/create-space") | |||||||
| const updatePins = sync.require("./actions/update-pins") | const updatePins = sync.require("./actions/update-pins") | ||||||
| /** @type {import("../matrix/api")}) */ | /** @type {import("../matrix/api")}) */ | ||||||
| const api = sync.require("../matrix/api") | const api = sync.require("../matrix/api") | ||||||
|  | /** @type {import("../discord/utils")} */ | ||||||
|  | const utils = sync.require("../discord/utils") | ||||||
| /** @type {import("../discord/discord-command-handler")}) */ | /** @type {import("../discord/discord-command-handler")}) */ | ||||||
| const discordCommandHandler = sync.require("../discord/discord-command-handler") | const discordCommandHandler = sync.require("../discord/discord-command-handler") | ||||||
|  |  | ||||||
|  | /** @type {any} */ // @ts-ignore bad types from semaphore | ||||||
|  | const Semaphore = require("@chriscdn/promise-semaphore") | ||||||
|  | const checkMissedPinsSema = new Semaphore() | ||||||
|  |  | ||||||
| let lastReportedEvent = 0 | let lastReportedEvent = 0 | ||||||
|  |  | ||||||
| // Grab Discord events we care about for the bridge, check them, and pass them on | // Grab Discord events we care about for the bridge, check them, and pass them on | ||||||
| @@ -103,6 +109,14 @@ module.exports = { | |||||||
| 			const latestWasBridged = prepared.get(channel.last_message_id) | 			const latestWasBridged = prepared.get(channel.last_message_id) | ||||||
| 			if (latestWasBridged) continue | 			if (latestWasBridged) continue | ||||||
|  |  | ||||||
|  | 			// Permissions check | ||||||
|  | 			const member = guild.members.find(m => m.user?.id === client.user.id) | ||||||
|  | 			if (!member) return | ||||||
|  | 			if (!("permission_overwrites" in channel)) continue | ||||||
|  | 			const permissions = utils.getPermissions(member.roles, guild.roles, client.user.id, channel.permission_overwrites) | ||||||
|  | 			const wants = BigInt(1 << 10) | BigInt(1 << 16) // VIEW_CHANNEL + READ_MESSAGE_HISTORY | ||||||
|  | 			if ((permissions & wants) !== wants) continue // We don't have permission to look back in this channel | ||||||
|  |  | ||||||
| 			/** More recent messages come first. */ | 			/** More recent messages come first. */ | ||||||
| 			// console.log(`[check missed messages] in ${channel.id} (${guild.name} / ${channel.name}) because its last message ${channel.last_message_id} is not in the database`) | 			// console.log(`[check missed messages] in ${channel.id} (${guild.name} / ${channel.name}) because its last message ${channel.last_message_id} is not in the database`) | ||||||
| 			let messages | 			let messages | ||||||
| @@ -132,6 +146,34 @@ module.exports = { | |||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * When logging back in, check if the pins on Matrix-side are up to date. If they aren't, update all pins. | ||||||
|  | 	 * Rather than query every room on Matrix-side, we cache the latest pinned message in the database and compare against that. | ||||||
|  | 	 * @param {import("./discord-client")} client | ||||||
|  | 	 * @param {DiscordTypes.GatewayGuildCreateDispatchData} guild | ||||||
|  | 	 */ | ||||||
|  | 	async checkMissedPins(client, guild) { | ||||||
|  | 		if (guild.unavailable) return | ||||||
|  | 		const member = guild.members.find(m => m.user?.id === client.user.id) | ||||||
|  | 		if (!member) return | ||||||
|  | 		for (const channel of guild.channels) { | ||||||
|  | 			if (!("last_pin_timestamp" in channel) || !channel.last_pin_timestamp) continue // Only care about channels that have pins | ||||||
|  | 			if (!("permission_overwrites" in channel)) continue | ||||||
|  | 			const lastPin = updatePins.convertTimestamp(channel.last_pin_timestamp) | ||||||
|  |  | ||||||
|  | 			// Permissions check | ||||||
|  | 			const permissions = utils.getPermissions(member.roles, guild.roles, client.user.id, channel.permission_overwrites) | ||||||
|  | 			const wants = BigInt(1 << 10) | BigInt(1 << 16) // VIEW_CHANNEL + READ_MESSAGE_HISTORY | ||||||
|  | 			if ((permissions & wants) !== wants) continue // We don't have permission to look up the pins in this channel | ||||||
|  |  | ||||||
|  | 			const row = select("channel_room", ["room_id", "last_bridged_pin_timestamp"], {channel_id: channel.id}).get() | ||||||
|  | 			if (!row) continue // Only care about already bridged channels | ||||||
|  | 			if (row.last_bridged_pin_timestamp == null || lastPin > row.last_bridged_pin_timestamp) { | ||||||
|  | 				checkMissedPinsSema.request(() => updatePins.updatePins(channel.id, row.room_id, lastPin)) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * When logging back in, check if we missed any changes to emojis or stickers. Apply the changes if so. | 	 * When logging back in, check if we missed any changes to emojis or stickers. Apply the changes if so. | ||||||
| 	 * @param {DiscordTypes.GatewayGuildCreateDispatchData} guild | 	 * @param {DiscordTypes.GatewayGuildCreateDispatchData} guild | ||||||
| @@ -183,7 +225,8 @@ module.exports = { | |||||||
| 	async onChannelPinsUpdate(client, data) { | 	async onChannelPinsUpdate(client, data) { | ||||||
| 		const roomID = select("channel_room", "room_id", {channel_id: data.channel_id}).pluck().get() | 		const roomID = select("channel_room", "room_id", {channel_id: data.channel_id}).pluck().get() | ||||||
| 		if (!roomID) return // No target room to update pins in | 		if (!roomID) return // No target room to update pins in | ||||||
| 		await updatePins.updatePins(data.channel_id, roomID) | 		const convertedTimestamp = updatePins.convertTimestamp(data.last_pin_timestamp) | ||||||
|  | 		await updatePins.updatePins(data.channel_id, roomID, convertedTimestamp) | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								db/migrations/0008-add-last-bridged-pin-timestamp.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								db/migrations/0008-add-last-bridged-pin-timestamp.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | BEGIN TRANSACTION; | ||||||
|  |  | ||||||
|  | ALTER TABLE channel_room ADD COLUMN last_bridged_pin_timestamp INTEGER; | ||||||
|  |  | ||||||
|  | COMMIT; | ||||||
							
								
								
									
										1
									
								
								db/orm-defs.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								db/orm-defs.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -6,6 +6,7 @@ export type Models = { | |||||||
| 		nick: string | null | 		nick: string | null | ||||||
| 		thread_parent: string | null | 		thread_parent: string | null | ||||||
| 		custom_avatar: string | null | 		custom_avatar: string | null | ||||||
|  | 		last_bridged_pin_timestamp: number | null | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	event_message: { | 	event_message: { | ||||||
|   | |||||||
| @@ -34,7 +34,7 @@ function getPermissions(userRoles, guildRoles, userID, channelOverwrites) { | |||||||
| 			// Role deny | 			// Role deny | ||||||
| 			overwrite => userRoles.includes(overwrite.id) && (allowed &= ~BigInt(overwrite.deny)), | 			overwrite => userRoles.includes(overwrite.id) && (allowed &= ~BigInt(overwrite.deny)), | ||||||
| 			// Role allow | 			// Role allow | ||||||
| 			overwrite => userRoles.includes(overwrite.id) && (allowed |= ~BigInt(overwrite.allow)), | 			overwrite => userRoles.includes(overwrite.id) && (allowed |= BigInt(overwrite.allow)), | ||||||
| 			// User deny | 			// User deny | ||||||
| 			overwrite => overwrite.id === userID && (allowed &= ~BigInt(overwrite.deny)), | 			overwrite => overwrite.id === userID && (allowed &= ~BigInt(overwrite.deny)), | ||||||
| 			// User allow | 			// User allow | ||||||
|   | |||||||
| @@ -18,6 +18,67 @@ test("discord utils: converts snowflake to timestamp", t => { | |||||||
| 	t.equal(utils.snowflakeToTimestampExact("86913608335773696"), 1440792219004) | 	t.equal(utils.snowflakeToTimestampExact("86913608335773696"), 1440792219004) | ||||||
| }) | }) | ||||||
|  |  | ||||||
| test("discerd utils: converts timestamp to snowflake", t => { | test("discord utils: converts timestamp to snowflake", t => { | ||||||
| 	t.match(utils.timestampToSnowflakeInexact(1440792219004), /^869136083357.....$/) | 	t.match(utils.timestampToSnowflakeInexact(1440792219004), /^869136083357.....$/) | ||||||
| }) | }) | ||||||
|  |  | ||||||
|  | test("getPermissions: channel overwrite to allow role works", t => { | ||||||
|  | 	const guildRoles = [ | ||||||
|  | 		{ | ||||||
|  | 			version: 1695412489043, | ||||||
|  | 			unicode_emoji: null, | ||||||
|  | 			tags: {}, | ||||||
|  | 			position: 0, | ||||||
|  | 			permissions: "559623605571137", | ||||||
|  | 			name: "@everyone", | ||||||
|  | 			mentionable: false, | ||||||
|  | 			managed: false, | ||||||
|  | 			id: "1154868424724463687", | ||||||
|  | 			icon: null, | ||||||
|  | 			hoist: false, | ||||||
|  | 			flags: 0, | ||||||
|  | 			color: 0 | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			version: 1695412604262, | ||||||
|  | 			unicode_emoji: null, | ||||||
|  | 			tags: { bot_id: "466378653216014359" }, | ||||||
|  | 			position: 1, | ||||||
|  | 			permissions: "536995904", | ||||||
|  | 			name: "PluralKit", | ||||||
|  | 			mentionable: false, | ||||||
|  | 			managed: true, | ||||||
|  | 			id: "1154868908336099444", | ||||||
|  | 			icon: null, | ||||||
|  | 			hoist: false, | ||||||
|  | 			flags: 0, | ||||||
|  | 			color: 0 | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			version: 1698778936921, | ||||||
|  | 			unicode_emoji: null, | ||||||
|  | 			tags: {}, | ||||||
|  | 			position: 1, | ||||||
|  | 			permissions: "536870912", | ||||||
|  | 			name: "web hookers", | ||||||
|  | 			mentionable: false, | ||||||
|  | 			managed: false, | ||||||
|  | 			id: "1168988246680801360", | ||||||
|  | 			icon: null, | ||||||
|  | 			hoist: false, | ||||||
|  | 			flags: 0, | ||||||
|  | 			color: 0 | ||||||
|  | 		} | ||||||
|  | 	] | ||||||
|  | 	const userRoles = [ "1168988246680801360" ] | ||||||
|  | 	const userID = "684280192553844747" | ||||||
|  | 	const overwrites = [ | ||||||
|  | 		{ type: 0, id: "1154868908336099444", deny: "0", allow: "1024" }, | ||||||
|  | 		{ type: 0, id: "1154868424724463687", deny: "1024", allow: "0" }, | ||||||
|  | 		{ type: 0, id: "1168988246680801360", deny: "0", allow: "1024" }, | ||||||
|  | 		{ type: 1, id: "353373325575323648", deny: "0", allow: "1024" } | ||||||
|  | 	] | ||||||
|  | 	const permissions = utils.getPermissions(userRoles, guildRoles, userID, overwrites) | ||||||
|  | 	const want = BigInt(1 << 10 | 1 << 16) | ||||||
|  | 	t.equal((permissions & want), want) | ||||||
|  | }) | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								stdin.js
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								stdin.js
									
									
									
									
									
								
							| @@ -16,6 +16,7 @@ const api = sync.require("./matrix/api") | |||||||
| const file = sync.require("./matrix/file") | const file = sync.require("./matrix/file") | ||||||
| const sendEvent = sync.require("./m2d/actions/send-event") | const sendEvent = sync.require("./m2d/actions/send-event") | ||||||
| const eventDispatcher = sync.require("./d2m/event-dispatcher") | const eventDispatcher = sync.require("./d2m/event-dispatcher") | ||||||
|  | const updatePins = sync.require("./d2m/actions/update-pins") | ||||||
| const ks = sync.require("./matrix/kstate") | const ks = sync.require("./matrix/kstate") | ||||||
| const guildID = "112760669178241024" | const guildID = "112760669178241024" | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Cadence Ember
					Cadence Ember