From c1cbdfee82edb1e5ea3829cf62a9d0336f362473 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 25 Sep 2023 23:15:36 +1300 Subject: [PATCH] Other kinds of reaction removals --- d2m/actions/remove-reaction.js | 75 +++++++++++++++++++++++++++++++--- d2m/discord-packets.js | 6 +++ d2m/event-dispatcher.js | 19 ++++++++- m2d/actions/add-reaction.js | 40 +++--------------- m2d/converters/emoji.js | 53 ++++++++++++++++++++++++ 5 files changed, 151 insertions(+), 42 deletions(-) create mode 100644 m2d/converters/emoji.js diff --git a/d2m/actions/remove-reaction.js b/d2m/actions/remove-reaction.js index 70ae5c3..632d5e2 100644 --- a/d2m/actions/remove-reaction.js +++ b/d2m/actions/remove-reaction.js @@ -9,6 +9,10 @@ const {discord, sync, db, select} = passthrough const api = sync.require("../../matrix/api") /** @type {import("../converters/emoji-to-key")} */ const emojiToKey = sync.require("../converters/emoji-to-key") +/** @type {import("../../m2d/converters/utils")} */ +const utils = sync.require("../../m2d/converters/utils") +/** @type {import("../../m2d/converters/emoji")} */ +const emoji = sync.require("../../m2d/converters/emoji") /** * @param {import("discord-api-types/v10").GatewayMessageReactionRemoveDispatchData} data @@ -18,16 +22,77 @@ async function removeReaction(data) { if (!roomID) return const eventIDForMessage = select("event_message", "event_id", "WHERE message_id = ? AND part = 0").pluck().get(data.message_id) if (!eventIDForMessage) return - const mxid = select("sim", "mxid", "WHERE discord_id = ?").pluck().get(data.user_id) - if (!mxid) return /** @type {Ty.Pagination>} */ const relations = await api.getRelations(roomID, eventIDForMessage, "m.annotation") const key = await emojiToKey.emojiToKey(data.emoji) - const eventIDForReaction = relations.chunk.find(e => e.sender === mxid && e.content["m.relates_to"].key === key) - if (!eventIDForReaction) return - await api.redactEvent(roomID, eventIDForReaction.event_id, mxid) + const wantToRemoveMatrixReaction = data.user_id === discord.application.id + for (const event of relations.chunk) { + if (event.content["m.relates_to"].key === key) { + const lookingAtMatrixReaction = !utils.eventSenderIsFromDiscord(event.sender) + if (lookingAtMatrixReaction && wantToRemoveMatrixReaction) { + // We are removing a Matrix user's reaction, so we need to redact from the correct user ID (not @_ooye_matrix_bridge). + // Even though the bridge bot only reacted once on Discord-side, multiple Matrix users may have + // reacted on Matrix-side. Semantically, we want to remove the reaction from EVERY Matrix user. + await api.redactEvent(roomID, event.event_id) + // Clean up the database + const hash = utils.getEventIDHash(event.event_id) + db.prepare("DELETE FROM reaction WHERE hashed_event_id = ?").run(hash) + } + if (!lookingAtMatrixReaction && !wantToRemoveMatrixReaction) { + // We are removing a Discord user's reaction, so we just make the sim user remove it. + const mxid = select("sim", "mxid", "WHERE discord_id = ?").pluck().get(data.user_id) + await api.redactEvent(roomID, event.event_id, mxid) + } + } + } +} + +/** + * @param {import("discord-api-types/v10").GatewayMessageReactionRemoveEmojiDispatchData} data + */ +async function removeEmojiReaction(data) { + const roomID = select("channel_room", "room_id", "WHERE channel_id = ?").pluck().get(data.channel_id) + if (!roomID) return + const eventIDForMessage = select("event_message", "event_id", "WHERE message_id = ? AND part = 0").pluck().get(data.message_id) + if (!eventIDForMessage) return + + /** @type {Ty.Pagination>} */ + const relations = await api.getRelations(roomID, eventIDForMessage, "m.annotation") + const key = await emojiToKey.emojiToKey(data.emoji) + + for (const event of relations.chunk) { + if (event.content["m.relates_to"].key === key) { + const mxid = utils.eventSenderIsFromDiscord(event.sender) ? event.sender : undefined + await api.redactEvent(roomID, event.event_id, mxid) + } + } + + const discordPreferredEncoding = emoji.encodeEmoji(key, undefined) + db.prepare("DELETE FROM reaction WHERE message_id = ? AND encoded_emoji = ?").run(data.message_id, discordPreferredEncoding) +} + +/** + * @param {import("discord-api-types/v10").GatewayMessageReactionRemoveAllDispatchData} data + */ +async function removeAllReactions(data) { + const roomID = select("channel_room", "room_id", "WHERE channel_id = ?").pluck().get(data.channel_id) + if (!roomID) return + const eventIDForMessage = select("event_message", "event_id", "WHERE message_id = ? AND part = 0").pluck().get(data.message_id) + if (!eventIDForMessage) return + + /** @type {Ty.Pagination>} */ + const relations = await api.getRelations(roomID, eventIDForMessage, "m.annotation") + + for (const event of relations.chunk) { + const mxid = utils.eventSenderIsFromDiscord(event.sender) ? event.sender : undefined + await api.redactEvent(roomID, event.event_id, mxid) + } + + db.prepare("DELETE FROM reaction WHERE message_id = ?").run(data.message_id) } module.exports.removeReaction = removeReaction +module.exports.removeEmojiReaction = removeEmojiReaction +module.exports.removeAllReactions = removeAllReactions diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index 8c5a24a..979f756 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -158,6 +158,12 @@ const utils = { } else if (message.t === "MESSAGE_REACTION_REMOVE") { await eventDispatcher.onReactionRemove(client, message.d) + + } else if (message.t === "MESSAGE_REACTION_REMOVE_EMOJI") { + await eventDispatcher.onReactionEmojiRemove(client, message.d) + + } else if (message.t === "MESSAGE_REACTION_REMOVE_ALL") { + await eventDispatcher.onRemoveAllReactions(client, message.d) } } catch (e) { // Let OOYE try to handle errors too diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index a415b86..b33fac4 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -214,13 +214,28 @@ module.exports = { /** * @param {import("./discord-client")} client - * @param {import("discord-api-types/v10").GatewayMessageReactionAddDispatchData} data + * @param {import("discord-api-types/v10").GatewayMessageReactionRemoveDispatchData} data */ async onReactionRemove(client, data) { - if (data.user_id === client.user.id) return // m2d reactions are added by the discord bot user - do not reflect them back to matrix. await removeReaction.removeReaction(data) }, + /** + * @param {import("./discord-client")} client + * @param {import("discord-api-types/v10").GatewayMessageReactionRemoveEmojiDispatchData} data + */ + async onReactionEmojiRemove(client, data) { + await removeReaction.removeEmojiReaction(data) + }, + + /** + * @param {import("./discord-client")} client + * @param {import("discord-api-types/v10").GatewayMessageReactionRemoveAllDispatchData} data + */ + async onRemoveAllReactions(client, data) { + await removeReaction.removeAllReactions(data) + }, + /** * @param {import("./discord-client")} client * @param {import("discord-api-types/v10").GatewayMessageDeleteDispatchData} data diff --git a/m2d/actions/add-reaction.js b/m2d/actions/add-reaction.js index 7cfbc61..bd8eeff 100644 --- a/m2d/actions/add-reaction.js +++ b/m2d/actions/add-reaction.js @@ -7,6 +7,8 @@ const passthrough = require("../../passthrough") const {discord, sync, db, select} = passthrough /** @type {import("../converters/utils")} */ const utils = sync.require("../converters/utils") +/** @type {import("../converters/emoji")} */ +const emoji = sync.require("../converters/emoji") /** * @param {Ty.Event.Outer} event @@ -17,41 +19,9 @@ async function addReaction(event) { const messageID = select("event_message", "message_id", "WHERE event_id = ? AND part = 0").pluck().get(event.content["m.relates_to"].event_id) // 0 = primary if (!messageID) return // Nothing can be done if the parent message was never bridged. - const emoji = event.content["m.relates_to"].key // TODO: handle custom text or emoji reactions - let discordPreferredEncoding - if (emoji.startsWith("mxc://")) { - // Custom emoji - let row = select("emoji", ["id", "name"], "WHERE mxc_url = ?").get(emoji) - if (!row && event.content.shortcode) { - // Use the name to try to find a known emoji with the same name. - const name = event.content.shortcode.replace(/^:|:$/g, "") - row = select("emoji", ["id", "name"], "WHERE name = ?").get(name) - } - if (!row) { - // We don't have this emoji and there's no realistic way to just-in-time upload a new emoji somewhere. - // Sucks! - return - } - // Cool, we got an exact or a candidate emoji. - discordPreferredEncoding = encodeURIComponent(`${row.name}:${row.id}`) - } else { - // Default emoji - // https://github.com/discord/discord-api-docs/issues/2723#issuecomment-807022205 ???????????? - const encoded = encodeURIComponent(emoji) - const encodedTrimmed = encoded.replace(/%EF%B8%8F/g, "") - - const forceTrimmedList = [ - "%F0%9F%91%8D", // 👍 - "%E2%AD%90" // ⭐ - ] - - discordPreferredEncoding = - ( forceTrimmedList.includes(encodedTrimmed) ? encodedTrimmed - : encodedTrimmed !== encoded && [...emoji].length === 2 ? encoded - : encodedTrimmed) - - console.log("add reaction from matrix:", emoji, encoded, encodedTrimmed, "chosen:", discordPreferredEncoding) - } + const key = event.content["m.relates_to"].key // TODO: handle custom text or emoji reactions + const discordPreferredEncoding = emoji.encodeEmoji(key, event.content.shortcode) + if (!discordPreferredEncoding) return await discord.snow.channel.createReaction(channelID, messageID, discordPreferredEncoding) // acting as the discord bot itself diff --git a/m2d/converters/emoji.js b/m2d/converters/emoji.js new file mode 100644 index 0000000..a505cd1 --- /dev/null +++ b/m2d/converters/emoji.js @@ -0,0 +1,53 @@ +// @ts-check + +const assert = require("assert").strict +const Ty = require("../../types") + +const passthrough = require("../../passthrough") +const {sync, select} = passthrough + +/** + * @param {string} emoji + * @param {string | null | undefined} shortcode + * @returns {string?} + */ +function encodeEmoji(emoji, shortcode) { + let discordPreferredEncoding + if (emoji.startsWith("mxc://")) { + // Custom emoji + let row = select("emoji", ["id", "name"], "WHERE mxc_url = ?").get(emoji) + if (!row && shortcode) { + // Use the name to try to find a known emoji with the same name. + const name = shortcode.replace(/^:|:$/g, "") + row = select("emoji", ["id", "name"], "WHERE name = ?").get(name) + } + if (!row) { + // We don't have this emoji and there's no realistic way to just-in-time upload a new emoji somewhere. + // Sucks! + return null + } + // Cool, we got an exact or a candidate emoji. + discordPreferredEncoding = encodeURIComponent(`${row.name}:${row.id}`) + } else { + // Default emoji + // https://github.com/discord/discord-api-docs/issues/2723#issuecomment-807022205 ???????????? + const encoded = encodeURIComponent(emoji) + const encodedTrimmed = encoded.replace(/%EF%B8%8F/g, "") + + const forceTrimmedList = [ + "%F0%9F%91%8D", // 👍 + "%E2%AD%90", // ⭐ + "%F0%9F%90%88", // 🐈 + ] + + discordPreferredEncoding = + ( forceTrimmedList.includes(encodedTrimmed) ? encodedTrimmed + : encodedTrimmed !== encoded && [...emoji].length === 2 ? encoded + : encodedTrimmed) + + console.log("add reaction from matrix:", emoji, encoded, encodedTrimmed, "chosen:", discordPreferredEncoding) + } + return discordPreferredEncoding +} + +module.exports.encodeEmoji = encodeEmoji