From 96dd488e3935bc3a8a2eabca0546250dc6ac2fe3 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 28 Jul 2023 17:05:13 +1200 Subject: [PATCH] progress on edits --- d2m/actions/edit-message.js | 118 ++++++++++++++++++++++++++++++ d2m/actions/send-message.js | 2 +- m2d/actions/send-event.js | 2 +- matrix/api.js | 19 ++++- scripts/save-event-types-to-db.js | 30 ++++++++ 5 files changed, 166 insertions(+), 5 deletions(-) create mode 100644 d2m/actions/edit-message.js create mode 100644 scripts/save-event-types-to-db.js diff --git a/d2m/actions/edit-message.js b/d2m/actions/edit-message.js new file mode 100644 index 0000000..662e124 --- /dev/null +++ b/d2m/actions/edit-message.js @@ -0,0 +1,118 @@ +// @ts-check + +const assert = require("assert") + +const passthrough = require("../../passthrough") +const { discord, sync, db } = passthrough +/** @type {import("../converters/message-to-event")} */ +const messageToEvent = sync.require("../converters/message-to-event") +/** @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") + +/** + * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message + * @param {import("discord-api-types/v10").APIGuild} guild + */ +async function editMessage(message, guild) { + // Figure out what events we will be replacing + + const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").get(message.channel_id) + const senderMxid = await registerUser.ensureSimJoined(message.author, roomID) + /** @type {{event_id: string, event_type: string, event_subtype: string?, part: number}[]} */ + const oldEventRows = db.prepare("SELECT event_id, event_type, event_subtype, part FROM event_message WHERE message_id = ?").all(message.id) + + // Figure out what we will be replacing them with + + const newEvents = await messageToEvent.messageToEvent(message, guild, api) + + // Match the new events to the old events + + /* + Rules: + + The events must have the same type. + + The events must have the same subtype. + Events will therefore be divided into three categories: + */ + /** 1. Events that are matched, and should be edited by sending another m.replace event */ + let eventsToReplace = [] + /** 2. Events that are present in the old version only, and should be blanked or redacted */ + let eventsToRedact = [] + /** 3. Events that are present in the new version only, and should be sent as new, with references back to the context */ + let eventsToSend = [] + + // For each old event... + outer: while (newEvents.length) { + const newe = newEvents[0] + // Find a new event to pair it with... + let handled = false + for (let i = 0; i < oldEventRows.length; i++) { + const olde = oldEventRows[i] + if (olde.event_type === newe.$type && olde.event_subtype === (newe.msgtype || null)) { + // Found one! + // Set up the pairing + eventsToReplace.push({ + old: olde, + new: newe + }) + // These events have been handled now, so remove them from the source arrays + newEvents.shift() + oldEventRows.splice(i, 1) + // Go all the way back to the start of the next iteration of the outer loop + continue outer + } + } + // If we got this far, we could not pair it to an existing event, so it'll have to be a new one + eventsToSend.push(newe) + newEvents.shift() + } + // Anything remaining in oldEventRows is present in the old version only and should be redacted. + eventsToRedact = oldEventRows + + // Now, everything in eventsToSend and eventsToRedact is a real change, but everything in eventsToReplace might not have actually changed! + // (Consider a MESSAGE_UPDATE for a text+image message - Discord does not allow the image to be changed, but the text might have been.) + // So we'll remove entries from eventsToReplace that *definitely* cannot have changed. Everything remaining *may* have changed. + eventsToReplace = eventsToReplace.filter(ev => { + // Discord does not allow files, images, attachments, or videos to be edited. + if (ev.old.event_type === "m.room.message" && ev.old.event_subtype !== "m.text" && ev.old.event_subtype !== "m.emote") { + return false + } + // Discord does not allow stickers to be edited. + if (ev.old.event_type === "m.sticker") { + return false + } + // Anything else is fair game. + return true + }) + + // Action time! + + // 1. Replace all the things. + + + // 2. Redact all the things. + + // 3. Send all the things. + + // old code lies here + let eventPart = 0 // TODO: what to do about eventPart when editing? probably just need to make sure that exactly 1 value of '1' remains in the database? + for (const event of events) { + const eventType = event.$type + /** @type {Pick> & { $type?: string }} */ + const eventWithoutType = {...event} + delete eventWithoutType.$type + + const eventID = await api.sendEvent(roomID, eventType, event, senderMxid) + db.prepare("INSERT INTO event_message (event_id, message_id, channel_id, part, source) VALUES (?, ?, ?, ?, 1)").run(eventID, message.id, message.channel_id, eventPart) // source 1 = discord + + eventPart = 1 // TODO: use more intelligent algorithm to determine whether primary or supporting + eventIDs.push(eventID) + } + + return eventIDs +} + +module.exports.editMessage = editMessage diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index ff3c1de..258efcf 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -37,7 +37,7 @@ async function sendMessage(message, guild) { delete eventWithoutType.$type const eventID = await api.sendEvent(roomID, eventType, event, senderMxid) - db.prepare("INSERT INTO event_message (event_id, message_id, channel_id, part, source) VALUES (?, ?, ?, ?, 1)").run(eventID, message.id, message.channel_id, eventPart) // source 1 = discord + db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, channel_id, part, source) VALUES (?, ?, ?, ?, ?, ?, 1)").run(eventID, eventType, event.msgtype || null, message.id, message.channel_id, eventPart) // source 1 = discord eventPart = 1 // TODO: use more intelligent algorithm to determine whether primary or supporting eventIDs.push(eventID) diff --git a/m2d/actions/send-event.js b/m2d/actions/send-event.js index c05f08c..3f49fa4 100644 --- a/m2d/actions/send-event.js +++ b/m2d/actions/send-event.js @@ -25,7 +25,7 @@ async function sendEvent(event) { let eventPart = 0 // 0 is primary, 1 is supporting for (const message of messages) { const messageResponse = await channelWebhook.sendMessageWithWebhook(channelID, message) - db.prepare("INSERT INTO event_message (event_id, message_id, channel_id, part, source) VALUES (?, ?, ?, ?, 0)").run(event.event_id, messageResponse.id, channelID, eventPart) // source 0 = matrix + db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, channel_id, part, source) VALUES (?, ?, ?, ?, ?, ?, 0)").run(event.event_id, event.type, event.content.msgtype || null, messageResponse.id, channelID, eventPart) // source 0 = matrix eventPart = 1 // TODO: use more intelligent algorithm to determine whether primary or supporting? messageResponses.push(messageResponse) diff --git a/matrix/api.js b/matrix/api.js index 7f9d74e..9111909 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -15,12 +15,18 @@ const makeTxnId = sync.require("./txnid") /** * @param {string} p endpoint to access * @param {string} [mxid] optional: user to act as, for the ?user_id parameter + * @param {{[x: string]: any}} [otherParams] optional: any other query parameters to add * @returns {string} the new endpoint */ -function path(p, mxid) { +function path(p, mxid, otherParams = {}) { if (!mxid) return p const u = new URL(p, "http://localhost") u.searchParams.set("user_id", mxid) + for (const entry of Object.entries(otherParams)) { + if (entry[1] != undefined) { + u.searchParams.set(entry[0], entry[1]) + } + } return u.pathname + "?" + u.searchParams.toString() } @@ -109,10 +115,17 @@ async function sendState(roomID, type, stateKey, content, mxid) { return root.event_id } -async function sendEvent(roomID, type, content, mxid) { +/** + * @param {string} roomID + * @param {string} type + * @param {any} content + * @param {string} [mxid] + * @param {number} [timestamp] timestamp of the newly created event, in unix milliseconds + */ +async function sendEvent(roomID, type, content, mxid, timestamp) { console.log(`[api] event ${type} to ${roomID} as ${mxid || "default sim"}`) /** @type {Ty.R.EventSent} */ - const root = await mreq.mreq("PUT", path(`/client/v3/rooms/${roomID}/send/${type}/${makeTxnId.makeTxnId()}`, mxid), content) + const root = await mreq.mreq("PUT", path(`/client/v3/rooms/${roomID}/send/${type}/${makeTxnId.makeTxnId()}`, mxid, {ts: timestamp}), content) return root.event_id } diff --git a/scripts/save-event-types-to-db.js b/scripts/save-event-types-to-db.js new file mode 100644 index 0000000..83f5d2b --- /dev/null +++ b/scripts/save-event-types-to-db.js @@ -0,0 +1,30 @@ +// @ts-check + +const sqlite = require("better-sqlite3") +const HeatSync = require("heatsync") + +const passthrough = require("../passthrough") +const db = new sqlite("db/ooye.db") + +const sync = new HeatSync({watchFS: false}) + +Object.assign(passthrough, {sync, db}) + +const api = require("../matrix/api") + +/** @type {{event_id: string, room_id: string, event_type: string}[]} */ // @ts-ignore +const rows = db.prepare("SELECT event_id, room_id, event_type FROM event_message INNER JOIN channel_room USING (channel_id)").all() + +const preparedUpdate = db.prepare("UPDATE event_message SET event_type = ?, event_subtype = ? WHERE event_id = ?") + +;(async () => { + for (const row of rows) { + if (row.event_type == null) { + const event = await api.getEvent(row.room_id, row.event_id) + const type = event.type + const subtype = event.content.msgtype || null + preparedUpdate.run(type, subtype, row.event_id) + console.log(`Updated ${row.event_id} -> ${type} + ${subtype}`) + } + } +})()