diff --git a/db/ooye-schema.sql b/db/ooye-schema.sql index 43642ee..51d33b2 100644 --- a/db/ooye-schema.sql +++ b/db/ooye-schema.sql @@ -70,4 +70,10 @@ CREATE TABLE IF NOT EXISTS "emoji" ( "mxc_url" TEXT NOT NULL, PRIMARY KEY("id") ) WITHOUT ROWID; +CREATE TABLE IF NOT EXISTS "reaction" ( + "hashed_event_id" INTEGER NOT NULL, + "message_id" TEXT NOT NULL, + "encoded_emoji" TEXT NOT NULL, + PRIMARY KEY ("hashed_event_id") +) WITHOUT ROWID; COMMIT; diff --git a/db/orm-utils.d.ts b/db/orm-utils.d.ts index 0ae0844..58eeb28 100644 --- a/db/orm-utils.d.ts +++ b/db/orm-utils.d.ts @@ -69,6 +69,12 @@ export type Models = { animated: number mxc_url: string } + + reaction: { + hashed_event_id: number + message_id: string + encoded_emoji: string + } } export type Prepared = { diff --git a/m2d/actions/add-reaction.js b/m2d/actions/add-reaction.js index d5d5cc7..a9e36c3 100644 --- a/m2d/actions/add-reaction.js +++ b/m2d/actions/add-reaction.js @@ -5,6 +5,8 @@ const Ty = require("../../types") const passthrough = require("../../passthrough") const {discord, sync, db, select} = passthrough +/** @type {import("../converters/utils")} */ +const utils = sync.require("../converters/utils") /** * @param {Ty.Event.Outer} event @@ -48,7 +50,9 @@ async function addReaction(event) { console.log("add reaction from matrix:", emoji, encoded, encodedTrimmed, "chosen:", discordPreferredEncoding) } - return discord.snow.channel.createReaction(channelID, messageID, discordPreferredEncoding) // acting as the discord bot itself + await discord.snow.channel.createReaction(channelID, messageID, discordPreferredEncoding) // acting as the discord bot itself + + db.prepare("REPLACE INTO reaction (hashed_event_id, message_id, encoded_emoji) VALUES (?, ?, ?)").run(utils.getEventIDHash(event.event_id), messageID, discordPreferredEncoding) } module.exports.addReaction = addReaction diff --git a/m2d/actions/redact.js b/m2d/actions/redact.js new file mode 100644 index 0000000..9b03abb --- /dev/null +++ b/m2d/actions/redact.js @@ -0,0 +1,39 @@ +// @ts-check + +const assert = require("assert").strict +const Ty = require("../../types") + +const passthrough = require("../../passthrough") +const {discord, sync, db, select, from} = passthrough +/** @type {import("../converters/utils")} */ +const utils = sync.require("../converters/utils") + +/** + * @param {Ty.Event.Outer_M_Room_Redaction} event + */ +async function deleteMessage(event) { + const row = from("event_message").join("message_channel", "message_id").select("channel_id", "message_id").and("WHERE event_id = ?").get(event.event_id) + if (!row) return + return discord.snow.channel.deleteMessage(row.channel_id, row.message_id, event.content.reason) +} + +/** + * @param {Ty.Event.Outer_M_Room_Redaction} event + */ +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) +} + +/** + * Try everything that could possibly be redacted. + * @param {Ty.Event.Outer_M_Room_Redaction} event + */ +async function handle(event) { + await deleteMessage(event) + await removeReaction(event) +} + +module.exports.handle = handle diff --git a/m2d/converters/utils.js b/m2d/converters/utils.js index 4f74c6b..e1579ee 100644 --- a/m2d/converters/utils.js +++ b/m2d/converters/utils.js @@ -38,7 +38,7 @@ function getPublicUrlForMxc(mxc) { /** * Event IDs are really big and have more entropy than we need. * If we want to store the event ID in the database, we can store a more compact version by hashing it with this. - * Choosing a 64-bit non-cryptographic hash as only a 32-bit hash will see birthday collisions unreasonably frequently: https://en.wikipedia.org/wiki/Birthday_attack#Mathematics + * I choose a 64-bit non-cryptographic hash as only a 32-bit hash will see birthday collisions unreasonably frequently: https://en.wikipedia.org/wiki/Birthday_attack#Mathematics * xxhash outputs an unsigned 64-bit integer. * Converting to a signed 64-bit integer with no bit loss so that it can be stored in an SQLite integer field as-is: https://www.sqlite.org/fileformat2.html#record_format * This should give very efficient storage with sufficient entropy. diff --git a/m2d/event-dispatcher.js b/m2d/event-dispatcher.js index d81e7cf..702f59b 100644 --- a/m2d/event-dispatcher.js +++ b/m2d/event-dispatcher.js @@ -12,6 +12,8 @@ const {discord, db, sync, as} = require("../passthrough") const sendEvent = sync.require("./actions/send-event") /** @type {import("./actions/add-reaction")} */ const addReaction = sync.require("./actions/add-reaction") +/** @type {import("./actions/redact")} */ +const redact = sync.require("./actions/redact") /** @type {import("./converters/utils")} */ const utils = sync.require("./converters/utils") /** @type {import("../matrix/api")}) */ @@ -101,6 +103,15 @@ async event => { } })) +sync.addTemporaryListener(as, "type:m.room.redaction", guard("m.room.redaction", +/** + * @param {Ty.Event.Outer_M_Room_Redaction} event it is a m.room.redaction because that's what this listener is filtering for + */ +async event => { + if (utils.eventSenderIsFromDiscord(event.sender)) return + await redact.handle(event) +})) + sync.addTemporaryListener(as, "type:m.room.avatar", guard("m.room.avatar", /** * @param {Ty.Event.StateOuter} event diff --git a/types.d.ts b/types.d.ts index d4e31ef..3161b14 100644 --- a/types.d.ts +++ b/types.d.ts @@ -173,6 +173,12 @@ export namespace Event { key: string // the unicode emoji, mxc uri, or reaction text } } + + export type Outer_M_Room_Redaction = Outer<{ + reason?: string + }> & { + redacts: string + } } export namespace R {