diff --git a/d2m/actions/add-reaction.js b/d2m/actions/add-reaction.js index bbce42d..1c2380f 100644 --- a/d2m/actions/add-reaction.js +++ b/d2m/actions/add-reaction.js @@ -10,8 +10,9 @@ const api = sync.require("../../matrix/api") const registerUser = sync.require("./register-user") /** @type {import("../actions/create-room")} */ const createRoom = sync.require("../actions/create-room") -/** @type {import("../../matrix/file")} */ -const file = sync.require("../../matrix/file") +/** @type {import("../converters/emoji-to-key")} */ +const emojiToKey = sync.require("../converters/emoji-to-key") + /** * @param {import("discord-api-types/v10").GatewayMessageReactionAddDispatchData} data @@ -24,24 +25,8 @@ async function addReaction(data) { if (!parentID) return // Nothing can be done if the parent message was never bridged. assert.equal(typeof parentID, "string") - let key - if (data.emoji.id) { - // Custom emoji - const mxc = select("emoji", "mxc_url", "WHERE id = ?").pluck().get(data.emoji.id) - if (mxc) { - // The custom emoji is registered and we should send it - key = mxc - } else { - // The custom emoji is not registered. We will register it and then add it. - const mxc = await file.uploadDiscordFileToMxc(file.emoji(data.emoji.id, data.emoji.animated)) - db.prepare("INSERT OR IGNORE INTO emoji (id, name, animated, mxc_url) VALUES (?, ?, ?, ?)").run(data.emoji.id, data.emoji.name, +!!data.emoji.animated, mxc) - key = mxc - // TODO: what happens if the matrix user also tries adding this reaction? the bridge bot isn't able to use that emoji... - } - } else { - // Default emoji - key = data.emoji.name - } + const key = await emojiToKey.emojiToKey(data.emoji) + const shortcode = key.startsWith("mxc://") ? `:${data.emoji.name}:` : undefined const roomID = await createRoom.ensureRoom(data.channel_id) const senderMxid = await registerUser.ensureSimJoined(user, roomID) @@ -50,7 +35,8 @@ async function addReaction(data) { rel_type: "m.annotation", event_id: parentID, key - } + }, + shortcode }, senderMxid) return eventID } diff --git a/d2m/actions/remove-reaction.js b/d2m/actions/remove-reaction.js index 821b9d0..70ae5c3 100644 --- a/d2m/actions/remove-reaction.js +++ b/d2m/actions/remove-reaction.js @@ -7,12 +7,8 @@ const passthrough = require("../../passthrough") const {discord, sync, db, select} = passthrough /** @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") -/** @type {import("../../matrix/file")} */ -const file = sync.require("../../matrix/file") +/** @type {import("../converters/emoji-to-key")} */ +const emojiToKey = sync.require("../converters/emoji-to-key") /** * @param {import("discord-api-types/v10").GatewayMessageReactionRemoveDispatchData} data @@ -27,10 +23,11 @@ async function removeReaction(data) { /** @type {Ty.Pagination>} */ const relations = await api.getRelations(roomID, eventIDForMessage, "m.annotation") - const eventIDForReaction = relations.chunk.find(e => e.sender === mxid && e.content["m.relates_to"].key === data.emoji) // TODO: get the key from the emoji + 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, mxid) + await api.redactEvent(roomID, eventIDForReaction.event_id, mxid) } module.exports.removeReaction = removeReaction diff --git a/d2m/converters/emoji-to-key.js b/d2m/converters/emoji-to-key.js new file mode 100644 index 0000000..7d04f97 --- /dev/null +++ b/d2m/converters/emoji-to-key.js @@ -0,0 +1,38 @@ +// @ts-check + +const assert = require("assert").strict +const passthrough = require("../../passthrough") +const {discord, sync, db, select} = passthrough +/** @type {import("../../matrix/file")} */ +const file = sync.require("../../matrix/file") + +/** + * @param {import("discord-api-types/v10").APIEmoji} emoji + * @returns {Promise} + */ +async function emojiToKey(emoji) { + let key + if (emoji.id) { + // Custom emoji + const mxc = select("emoji", "mxc_url", "WHERE id = ?").pluck().get(emoji.id) + if (mxc) { + // The custom emoji is registered and we should send it + key = mxc + } else { + // The custom emoji is not registered. We will register it and then add it. + assert(emoji.name) // The docs say: "name may be null when custom emoji data is not available, for example, if it was deleted from the guild" + const mxc = await file.uploadDiscordFileToMxc(file.emoji(emoji.id, emoji.animated)) + db.prepare("INSERT OR IGNORE INTO emoji (id, name, animated, mxc_url) VALUES (?, ?, ?, ?)").run(emoji.id, emoji.name, +!!emoji.animated, mxc) + key = mxc + // TODO: what happens if the matrix user also tries adding this reaction? the bridge bot isn't able to use that emoji... + } + } else { + // Default emoji + const name = emoji.name + assert(name) + key = name + } + return key +} + +module.exports.emojiToKey = emojiToKey diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index b8f8eec..8c5a24a 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -155,6 +155,9 @@ const utils = { } else if (message.t === "MESSAGE_REACTION_ADD") { await eventDispatcher.onReactionAdd(client, message.d) + + } else if (message.t === "MESSAGE_REACTION_REMOVE") { + await eventDispatcher.onReactionRemove(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 32b8f49..a415b86 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -10,6 +10,8 @@ const editMessage = sync.require("./actions/edit-message") const deleteMessage = sync.require("./actions/delete-message") /** @type {import("./actions/add-reaction")}) */ const addReaction = sync.require("./actions/add-reaction") +/** @type {import("./actions/remove-reaction")}) */ +const removeReaction = sync.require("./actions/remove-reaction") /** @type {import("./actions/announce-thread")}) */ const announceThread = sync.require("./actions/announce-thread") /** @type {import("./actions/create-room")}) */ @@ -210,6 +212,15 @@ module.exports = { await addReaction.addReaction(data) }, + /** + * @param {import("./discord-client")} client + * @param {import("discord-api-types/v10").GatewayMessageReactionAddDispatchData} 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").GatewayMessageDeleteDispatchData} data diff --git a/m2d/actions/add-reaction.js b/m2d/actions/add-reaction.js index a9e36c3..7cfbc61 100644 --- a/m2d/actions/add-reaction.js +++ b/m2d/actions/add-reaction.js @@ -21,16 +21,19 @@ async function addReaction(event) { let discordPreferredEncoding if (emoji.startsWith("mxc://")) { // Custom emoji - const row = select("emoji", ["id", "name"], "WHERE mxc_url = ?").get(emoji) - if (row) { - // Great, we know exactly what this emoji is! - discordPreferredEncoding = encodeURIComponent(`${row.name}:${row.id}`) - } else { + 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. - // We can't try using a known emoji with the same name because we don't even know what the name is. We only have the mxc url. // 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 ???????????? diff --git a/m2d/actions/redact.js b/m2d/actions/redact.js index 9b03abb..e2d9ece 100644 --- a/m2d/actions/redact.js +++ b/m2d/actions/redact.js @@ -24,7 +24,8 @@ async function removeReaction(event) { const hash = utils.getEventIDHash(event.redacts) const row = from("reaction").join("message_channel", "message_id").select("channel_id", "message_id", "encoded_emoji").and("WHERE hashed_event_id = ?").get(hash) if (!row) return - return discord.snow.channel.deleteReactionSelf(row.channel_id, row.message_id, row.encoded_emoji) + await discord.snow.channel.deleteReactionSelf(row.channel_id, row.message_id, row.encoded_emoji) + db.prepare("DELETE FROM reaction WHERE hashed_event_id = ?").run(hash) } /** diff --git a/matrix/api.js b/matrix/api.js index c429216..032e702 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -155,7 +155,10 @@ async function sendEvent(roomID, type, content, mxid, timestamp) { } /** - * @returns {Promise} room ID + * @param {string} roomID + * @param {string} eventID + * @param {string?} [mxid] + * @returns {Promise} event ID */ async function redactEvent(roomID, eventID, mxid) { /** @type {Ty.R.EventRedacted} */ diff --git a/types.d.ts b/types.d.ts index db72eb3..3b4aacd 100644 --- a/types.d.ts +++ b/types.d.ts @@ -171,7 +171,8 @@ export namespace Event { rel_type: "m.annotation" event_id: string // the event that was reacted to key: string // the unicode emoji, mxc uri, or reaction text - } + }, + "shortcode"?: string // starts and ends with colons } export type Outer_M_Room_Redaction = Outer<{