Compare commits

..

3 commits

9 changed files with 81 additions and 38 deletions

View file

@ -10,8 +10,9 @@ const api = sync.require("../../matrix/api")
const registerUser = sync.require("./register-user") const registerUser = sync.require("./register-user")
/** @type {import("../actions/create-room")} */ /** @type {import("../actions/create-room")} */
const createRoom = sync.require("../actions/create-room") const createRoom = sync.require("../actions/create-room")
/** @type {import("../../matrix/file")} */ /** @type {import("../converters/emoji-to-key")} */
const file = sync.require("../../matrix/file") const emojiToKey = sync.require("../converters/emoji-to-key")
/** /**
* @param {import("discord-api-types/v10").GatewayMessageReactionAddDispatchData} data * @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. if (!parentID) return // Nothing can be done if the parent message was never bridged.
assert.equal(typeof parentID, "string") assert.equal(typeof parentID, "string")
let key const key = await emojiToKey.emojiToKey(data.emoji)
if (data.emoji.id) { const shortcode = key.startsWith("mxc://") ? `:${data.emoji.name}:` : undefined
// 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 roomID = await createRoom.ensureRoom(data.channel_id) const roomID = await createRoom.ensureRoom(data.channel_id)
const senderMxid = await registerUser.ensureSimJoined(user, roomID) const senderMxid = await registerUser.ensureSimJoined(user, roomID)
@ -50,7 +35,8 @@ async function addReaction(data) {
rel_type: "m.annotation", rel_type: "m.annotation",
event_id: parentID, event_id: parentID,
key key
} },
shortcode
}, senderMxid) }, senderMxid)
return eventID return eventID
} }

View file

@ -7,12 +7,8 @@ const passthrough = require("../../passthrough")
const {discord, sync, db, select} = passthrough const {discord, sync, db, select} = passthrough
/** @type {import("../../matrix/api")} */ /** @type {import("../../matrix/api")} */
const api = sync.require("../../matrix/api") const api = sync.require("../../matrix/api")
/** @type {import("./register-user")} */ /** @type {import("../converters/emoji-to-key")} */
const registerUser = sync.require("./register-user") const emojiToKey = sync.require("../converters/emoji-to-key")
/** @type {import("../actions/create-room")} */
const createRoom = sync.require("../actions/create-room")
/** @type {import("../../matrix/file")} */
const file = sync.require("../../matrix/file")
/** /**
* @param {import("discord-api-types/v10").GatewayMessageReactionRemoveDispatchData} data * @param {import("discord-api-types/v10").GatewayMessageReactionRemoveDispatchData} data
@ -27,10 +23,11 @@ async function removeReaction(data) {
/** @type {Ty.Pagination<Ty.Event.Outer<Ty.Event.M_Reaction>>} */ /** @type {Ty.Pagination<Ty.Event.Outer<Ty.Event.M_Reaction>>} */
const relations = await api.getRelations(roomID, eventIDForMessage, "m.annotation") 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 if (!eventIDForReaction) return
await api.redactEvent(roomID, eventIDForReaction, mxid) await api.redactEvent(roomID, eventIDForReaction.event_id, mxid)
} }
module.exports.removeReaction = removeReaction module.exports.removeReaction = removeReaction

View file

@ -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<string>}
*/
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

View file

@ -155,6 +155,9 @@ const utils = {
} else if (message.t === "MESSAGE_REACTION_ADD") { } else if (message.t === "MESSAGE_REACTION_ADD") {
await eventDispatcher.onReactionAdd(client, message.d) await eventDispatcher.onReactionAdd(client, message.d)
} else if (message.t === "MESSAGE_REACTION_REMOVE") {
await eventDispatcher.onReactionRemove(client, message.d)
} }
} catch (e) { } catch (e) {
// Let OOYE try to handle errors too // Let OOYE try to handle errors too

View file

@ -10,6 +10,8 @@ const editMessage = sync.require("./actions/edit-message")
const deleteMessage = sync.require("./actions/delete-message") const deleteMessage = sync.require("./actions/delete-message")
/** @type {import("./actions/add-reaction")}) */ /** @type {import("./actions/add-reaction")}) */
const addReaction = sync.require("./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")}) */ /** @type {import("./actions/announce-thread")}) */
const announceThread = sync.require("./actions/announce-thread") const announceThread = sync.require("./actions/announce-thread")
/** @type {import("./actions/create-room")}) */ /** @type {import("./actions/create-room")}) */
@ -210,6 +212,15 @@ module.exports = {
await addReaction.addReaction(data) 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-client")} client
* @param {import("discord-api-types/v10").GatewayMessageDeleteDispatchData} data * @param {import("discord-api-types/v10").GatewayMessageDeleteDispatchData} data

View file

@ -21,16 +21,19 @@ async function addReaction(event) {
let discordPreferredEncoding let discordPreferredEncoding
if (emoji.startsWith("mxc://")) { if (emoji.startsWith("mxc://")) {
// Custom emoji // Custom emoji
const row = select("emoji", ["id", "name"], "WHERE mxc_url = ?").get(emoji) let row = select("emoji", ["id", "name"], "WHERE mxc_url = ?").get(emoji)
if (row) { if (!row && event.content.shortcode) {
// Great, we know exactly what this emoji is! // Use the name to try to find a known emoji with the same name.
discordPreferredEncoding = encodeURIComponent(`${row.name}:${row.id}`) const name = event.content.shortcode.replace(/^:|:$/g, "")
} else { 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 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! // Sucks!
return return
} }
// Cool, we got an exact or a candidate emoji.
discordPreferredEncoding = encodeURIComponent(`${row.name}:${row.id}`)
} else { } else {
// Default emoji // Default emoji
// https://github.com/discord/discord-api-docs/issues/2723#issuecomment-807022205 ???????????? // https://github.com/discord/discord-api-docs/issues/2723#issuecomment-807022205 ????????????

View file

@ -24,7 +24,8 @@ async function removeReaction(event) {
const hash = utils.getEventIDHash(event.redacts) 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) 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 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)
} }
/** /**

View file

@ -155,7 +155,10 @@ async function sendEvent(roomID, type, content, mxid, timestamp) {
} }
/** /**
* @returns {Promise<string>} room ID * @param {string} roomID
* @param {string} eventID
* @param {string?} [mxid]
* @returns {Promise<string>} event ID
*/ */
async function redactEvent(roomID, eventID, mxid) { async function redactEvent(roomID, eventID, mxid) {
/** @type {Ty.R.EventRedacted} */ /** @type {Ty.R.EventRedacted} */

3
types.d.ts vendored
View file

@ -171,7 +171,8 @@ export namespace Event {
rel_type: "m.annotation" rel_type: "m.annotation"
event_id: string // the event that was reacted to event_id: string // the event that was reacted to
key: string // the unicode emoji, mxc uri, or reaction text key: string // the unicode emoji, mxc uri, or reaction text
} },
"shortcode"?: string // starts and ends with colons
} }
export type Outer_M_Room_Redaction = Outer<{ export type Outer_M_Room_Redaction = Outer<{