From 06b6a63ee322be5eea464bb925630028978467ec Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 7 Jan 2025 12:23:39 +1300 Subject: [PATCH] Sync pins back from Matrix to Discord --- src/m2d/actions/update-pins.js | 25 ++++++++++++++++++++++++ src/m2d/converters/diff-pins.js | 17 ++++++++++++++++ src/m2d/converters/diff-pins.test.js | 11 +++++++++++ src/m2d/event-dispatcher.js | 29 ++++++++++++++++++++++++++++ src/types.d.ts | 4 ++++ test/test.js | 1 + 6 files changed, 87 insertions(+) create mode 100644 src/m2d/actions/update-pins.js create mode 100644 src/m2d/converters/diff-pins.js create mode 100644 src/m2d/converters/diff-pins.test.js diff --git a/src/m2d/actions/update-pins.js b/src/m2d/actions/update-pins.js new file mode 100644 index 0000000..976e496 --- /dev/null +++ b/src/m2d/actions/update-pins.js @@ -0,0 +1,25 @@ +// @ts-check + +const {sync, from, discord} = require("../../passthrough") + +/** @type {import("../converters/diff-pins")} */ +const diffPins = sync.require("../converters/diff-pins") + +/** + * @param {string[]} pins + * @param {string[]} prev + */ +async function updatePins(pins, prev) { + const diff = diffPins.diffPins(pins, prev) + for (const [event_id, added] of diff) { + const row = from("event_message").join("message_channel", "message_id").where({event_id}).select("channel_id", "message_id").get() + if (!row) continue + if (added) { + discord.snow.channel.addChannelPinnedMessage(row.channel_id, row.message_id, "Message pinned on Matrix") + } else { + discord.snow.channel.removeChannelPinnedMessage(row.channel_id, row.message_id, "Message unpinned on Matrix") + } + } +} + +module.exports.updatePins = updatePins diff --git a/src/m2d/converters/diff-pins.js b/src/m2d/converters/diff-pins.js new file mode 100644 index 0000000..e6e038b --- /dev/null +++ b/src/m2d/converters/diff-pins.js @@ -0,0 +1,17 @@ +// @ts-check + +/** + * @param {string[]} pins + * @param {string[]} prev + * @returns {[string, boolean][]} + */ +function diffPins(pins, prev) { + /** @type {[string, boolean][]} */ + const result = [] + return result.concat( + prev.filter(id => !pins.includes(id)).map(id => [id, false]), // removed + pins.filter(id => !prev.includes(id)).map(id => [id, true]) // added + ) +} + +module.exports.diffPins = diffPins diff --git a/src/m2d/converters/diff-pins.test.js b/src/m2d/converters/diff-pins.test.js new file mode 100644 index 0000000..edb9c47 --- /dev/null +++ b/src/m2d/converters/diff-pins.test.js @@ -0,0 +1,11 @@ +// @ts-check + +const {test} = require("supertape") +const diffPins = require("./diff-pins") + +test("diff pins: diff is as expected", t => { + t.deepEqual( + diffPins.diffPins(["same", "new"], ["same", "old"]), + [["old", false], ["new", true]] + ) +}) diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index 301dcc4..d5d6ed3 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -14,6 +14,8 @@ const sendEvent = sync.require("./actions/send-event") const addReaction = sync.require("./actions/add-reaction") /** @type {import("./actions/redact")} */ const redact = sync.require("./actions/redact") +/** @type {import("./actions/update-pins")}) */ +const updatePins = sync.require("./actions/update-pins") /** @type {import("../matrix/matrix-command-handler")} */ const matrixCommandHandler = sync.require("../matrix/matrix-command-handler") /** @type {import("./converters/utils")} */ @@ -159,6 +161,33 @@ async event => { db.prepare("UPDATE channel_room SET nick = ? WHERE room_id = ?").run(name, event.room_id) })) +sync.addTemporaryListener(as, "type:m.room.pinned_events", guard("m.room.pinned_events", +/** + * @param {Ty.Event.StateOuter} event + */ +async event => { + if (event.state_key !== "") return + if (utils.eventSenderIsFromDiscord(event.sender)) return + const pins = event.content.pinned + if (!Array.isArray(pins)) return + let prev = event.unsigned?.prev_content?.pinned + if (!Array.isArray(prev)) { + if (pins.length === 1) { + /* + In edge cases, prev_content isn't guaranteed to be provided by the server. + If prev_content is missing, we can't diff. Better safe than sorry: we'd like to ignore the change rather than wiping the whole channel's pins on Discord. + However, that would mean if the first ever pin came from Matrix-side, it would be ignored, because there would be no prev_content (it's the first pinned event!) + So to handle that edge case, we assume that if there's exactly 1 entry in `pinned`, this is the first ever pin and it should go through. + */ + prev = [] + } else { + return + } + } + + await updatePins.updatePins(pins, prev) +})) + sync.addTemporaryListener(as, "type:m.room.member", guard("m.room.member", /** * @param {Ty.Event.StateOuter} event diff --git a/src/types.d.ts b/src/types.d.ts index 178a560..b99046b 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -241,6 +241,10 @@ export namespace Event { name?: string } + export type M_Room_PinnedEvents = { + pinned: string[] + } + export type M_Power_Levels = { /** The level required to ban a user. Defaults to 50 if unspecified. */ ban?: number, diff --git a/test/test.js b/test/test.js index 089a0dd..591b64b 100644 --- a/test/test.js +++ b/test/test.js @@ -139,6 +139,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../src/d2m/converters/remove-reaction.test") require("../src/d2m/converters/thread-to-announcement.test") require("../src/d2m/converters/user-to-mxid.test") + require("../src/m2d/converters/diff-pins.test") require("../src/m2d/converters/event-to-message.test") require("../src/m2d/converters/utils.test") require("../src/m2d/converters/emoji-sheet.test")