From b1ca71f37c2d8a20e534aac22c925e17d176e566 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 16 Aug 2023 17:03:05 +1200 Subject: [PATCH] getting edits closer to working --- .vscode/settings.json | 2 + d2m/actions/register-user.js | 1 + d2m/converters/edit-to-changes.js | 47 ++++++- d2m/converters/edit-to-changes.test.js | 66 ++++++++++ d2m/converters/message-to-event.js | 11 ++ package-lock.json | 7 +- package.json | 1 + test/data.js | 166 ++++++++++++++++++------- test/test.js | 1 + types.d.ts | 8 ++ 10 files changed, 259 insertions(+), 51 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 d2m/converters/edit-to-changes.test.js diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index ef6045a..beb24bd 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -59,6 +59,7 @@ async function ensureSim(user) { /** * Ensure a sim is registered for the user and is joined to the room. * @param {import("discord-api-types/v10").APIUser} user + * @param {string} roomID * @returns mxid */ async function ensureSimJoined(user, roomID) { diff --git a/d2m/converters/edit-to-changes.js b/d2m/converters/edit-to-changes.js index 0dd084f..87e769b 100644 --- a/d2m/converters/edit-to-changes.js +++ b/d2m/converters/edit-to-changes.js @@ -9,7 +9,7 @@ const messageToEvent = sync.require("../converters/message-to-event") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") /** @type {import("../actions/register-user")} */ -const registerUser = sync.require("./register-user") +const registerUser = sync.require("../actions/register-user") /** @type {import("../actions/create-room")} */ const createRoom = sync.require("../actions/create-room") @@ -22,7 +22,7 @@ const createRoom = sync.require("../actions/create-room") async function editToChanges(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 roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().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) @@ -37,7 +37,7 @@ async function editToChanges(message, guild) { Rules: + The events must have the same type. + The events must have the same subtype. - Events will therefore be divided into three categories: + Events will therefore be divided into four categories: */ /** 1. Events that are matched, and should be edited by sending another m.replace event */ let eventsToReplace = [] @@ -45,12 +45,12 @@ async function editToChanges(message, guild) { 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 = [] + // 4. Events that are matched and have definitely not changed, so they don't need to be edited or replaced at all. This is represented as nothing. // 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)) { @@ -76,7 +76,7 @@ async function editToChanges(message, guild) { // 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. + // So we'll remove entries from eventsToReplace that *definitely* cannot have changed. (This is category 4 mentioned above.) 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") { @@ -90,7 +90,42 @@ async function editToChanges(message, guild) { return true }) + // Removing unnecessary properties before returning + eventsToRedact = eventsToRedact.map(e => e.event_id) + eventsToReplace = eventsToReplace.map(e => ({oldID: e.old.event_id, new: eventToReplacementEvent(e.old.event_id, e.new)})) + return {eventsToReplace, eventsToRedact, eventsToSend} } -module.exports.editMessage = editMessage +/** + * @template T + * @param {string} oldID + * @param {T} content + * @returns {import("../../types").Event.ReplacementContent} content + */ +function eventToReplacementEvent(oldID, content) { + const newContent = { + ...content, + "m.mentions": {}, + "m.new_content": { + ...content + }, + "m.relates_to": { + rel_type: "m.replace", + event_id: oldID + } + } + if (typeof newContent.body === "string") { + newContent.body = "* " + newContent.body + } + if (typeof newContent.formatted_body === "string") { + newContent.formatted_body = "* " + newContent.formatted_body + } + delete newContent["m.new_content"]["$type"] + // Client-Server API spec 11.37.3: Any m.relates_to property within m.new_content is ignored. + delete newContent["m.new_content"]["m.relates_to"] + return newContent +} + +module.exports.editToChanges = editToChanges +module.exports.eventToReplacementEvent = eventToReplacementEvent diff --git a/d2m/converters/edit-to-changes.test.js b/d2m/converters/edit-to-changes.test.js new file mode 100644 index 0000000..b3e6e0c --- /dev/null +++ b/d2m/converters/edit-to-changes.test.js @@ -0,0 +1,66 @@ +// @ts-check + +const {test} = require("supertape") +const {editToChanges} = require("./edit-to-changes") +const data = require("../../test/data") +const Ty = require("../../types") + +test("edit2changes: bot response", async t => { + const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.bot_response, data.guild.general) + t.deepEqual(eventsToRedact, []) + t.deepEqual(eventsToSend, []) + t.deepEqual(eventsToReplace, [{ + oldID: "$fdD9OZ55xg3EAsfvLZza5tMhtjUO91Wg3Otuo96TplY", + new: { + $type: "m.room.message", + msgtype: "m.text", + body: "* :ae_botrac4r: @cadence asked ``­``, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.", + format: "org.matrix.custom.html", + formatted_body: '* :ae_botrac4r: @cadence asked ­, I respond: Stop drinking paint. (No)

Hit :bn_re: to reroll.', + "m.mentions": { + // Client-Server API spec 11.37.7: Copy Discord's behaviour by not re-notifying anyone that an *edit occurred* + }, + // *** Replaced With: *** + "m.new_content": { + msgtype: "m.text", + body: ":ae_botrac4r: @cadence asked ``­``, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.", + format: "org.matrix.custom.html", + formatted_body: ':ae_botrac4r: @cadence asked ­, I respond: Stop drinking paint. (No)

Hit :bn_re: to reroll.', + "m.mentions": { + // Client-Server API spec 11.37.7: This should contain the mentions for the final version of the event + "user_ids": ["@cadence:cadence.moe"] + } + }, + "m.relates_to": { + rel_type: "m.replace", + event_id: "$fdD9OZ55xg3EAsfvLZza5tMhtjUO91Wg3Otuo96TplY" + } + } + }]) +}) + +test("edit2changes: edit of reply to skull webp attachment with content", async t => { + const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.edit_of_reply_to_skull_webp_attachment_with_content, data.guild.general) + t.deepEqual(eventsToRedact, []) + t.deepEqual(eventsToSend, []) + t.deepEqual(eventsToReplace, [{ + oldID: "$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M", + new: { + $type: "m.room.message", + // TODO: read "edits of replies" in the spec!!! + msgtype: "m.text", + body: "* Edit", + "m.mentions": {}, + "m.new_content": { + msgtype: "m.text", + body: "Edit", + "m.mentions": {} + }, + "m.relates_to": { + rel_type: "m.replace", + event_id: "$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M" + } + // TODO: read "edits of replies" in the spec!!! + } + }]) +}) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 49a387a..a2c4915 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -15,6 +15,7 @@ const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex)) function getDiscordParseCallbacks(message, useHTML) { return { + /** @param {{id: string, type: "discordUser"}} node */ user: node => { const mxid = db.prepare("SELECT mxid FROM sim WHERE discord_id = ?").pluck().get(node.id) const username = message.mentions.find(ment => ment.id === node.id)?.username || node.id @@ -24,6 +25,7 @@ function getDiscordParseCallbacks(message, useHTML) { return `@${username}:` } }, + /** @param {{id: string, type: "discordChannel"}} node */ channel: node => { const {room_id, name, nick} = db.prepare("SELECT room_id, name, nick FROM channel_room WHERE channel_id = ?").get(node.id) if (room_id && useHTML) { @@ -32,6 +34,15 @@ function getDiscordParseCallbacks(message, useHTML) { return `#${nick || name}` } }, + /** @param {{animated: boolean, name: string, id: string, type: "discordEmoji"}} node */ + emoji: node => { + if (useHTML) { + // TODO: upload the emoji and actually use the right mxc!! + return `:${node.name}:` + } else { + return `:${node.name}:` + } + }, role: node => "@&" + node.id, everyone: node => diff --git a/package-lock.json b/package-lock.json index e8b6aeb..6be3b42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "@types/node-fetch": "^2.6.3", "c8": "^8.0.1", "cross-env": "^7.0.3", + "discord-api-types": "^0.37.53", "supertape": "^8.3.0", "tap-dot": "github:cloudrac3r/tap-dot#223a4e67a6f7daf015506a12a7af74605f06c7f4" } @@ -1044,9 +1045,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.37.47", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.47.tgz", - "integrity": "sha512-rNif8IAv6duS2z47BMXq/V9kkrLfkAoiwpFY3sLxxbyKprk065zqf3HLTg4bEoxRSmi+Lhc7yqGDrG8C3j8GFA==" + "version": "0.37.53", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.53.tgz", + "integrity": "sha512-N6uUgv50OyP981Mfxrrt0uxcqiaNr0BDaQIoqfk+3zM2JpZtwU9v7ce1uaFAP53b2xSDvcbrk80Kneui6XJgGg==" }, "node_modules/discord-markdown": { "version": "2.4.1", diff --git a/package.json b/package.json index dd6f55d..2437aba 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@types/node-fetch": "^2.6.3", "c8": "^8.0.1", "cross-env": "^7.0.3", + "discord-api-types": "^0.37.53", "supertape": "^8.3.0", "tap-dot": "github:cloudrac3r/tap-dot#223a4e67a6f7daf015506a12a7af74605f06c7f4" }, diff --git a/test/data.js b/test/data.js index a5ca95e..e23d42f 100644 --- a/test/data.js +++ b/test/data.js @@ -88,7 +88,8 @@ module.exports = { rules_channel_id: null, name: "Psychonauts 3", max_stage_video_channel_users: 300, - system_channel_flags: 0|0 + system_channel_flags: 0|0, + safety_alerts_channel_id: null } }, member: { @@ -744,6 +745,128 @@ module.exports = { attachments: [], guild_id: "112760669178241024" }, + sticker: { + id: "1106366167788044450", + type: 0, + content: "can have attachments too", + channel_id: "122155380120748034", + author: { + id: "113340068197859328", + username: "Cookie 🍪", + global_name: null, + display_name: null, + avatar: "b48302623a12bc7c59a71328f72ccb39", + discriminator: "7766", + public_flags: 128, + avatar_decoration: null + }, + attachments: [{ + id: "1106366167486038016", + filename: "image.png", + size: 127373, + url: "https://cdn.discordapp.com/attachments/122155380120748034/1106366167486038016/image.png", + proxy_url: "https://media.discordapp.net/attachments/122155380120748034/1106366167486038016/image.png", + width: 333, + height: 287, + content_type: "image/png" + }], + embeds: [], + mentions: [], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2023-05-11T23:44:09.690000+00:00", + edited_timestamp: null, + flags: 0, + components: [], + sticker_items: [{ + id: "1106323941183717586", + format_type: 1, + name: "pomu puff" + }] + } + }, + message_update: { + bot_response: { + attachments: [], + author: { + avatar: "d14f47194b6ebe4da2e18a56fc6dacfd", + avatar_decoration: null, + bot: true, + discriminator: "9703", + global_name: null, + id: "771520384671416320", + public_flags: 0, + username: "Bojack Horseman" + }, + channel_id: "160197704226439168", + components: [], + content: "<:ae_botrac4r:551636841284108289> @cadence asked ``­``, I respond: Stop drinking paint. (No)\n\nHit <:bn_re:362741439211503616> to reroll.", + edited_timestamp: "2023-08-16T03:06:07.128980+00:00", + embeds: [], + flags: 0, + guild_id: "112760669178241024", + id: "1141206225632112650", + member: { + avatar: null, + communication_disabled_until: null, + deaf: false, + flags: 0, + joined_at: "2020-10-29T23:55:31.277000+00:00", + mute: false, + nick: "Olmec", + pending: false, + premium_since: null, + roles: [ + "112767366235959296", + "118924814567211009", + "392141548932038658", + "1123460940935991296", + "326409028601249793", + "114526764860047367", + "323966487763353610", + "1107404526870335629", + "1040735082610167858" + ] + }, + mention_everyone: false, + mention_roles: [], + mentions: [ + { + avatar: "8757ad3edee9541427edd7f817ae2f5c", + avatar_decoration: null, + bot: true, + discriminator: "8559", + global_name: null, + id: "353703396483661824", + member: { + avatar: null, + communication_disabled_until: null, + deaf: false, + flags: 0, + joined_at: "2017-11-30T04:27:20.749000+00:00", + mute: false, + nick: null, + pending: false, + premium_since: null, + roles: [ + "112767366235959296", + "118924814567211009", + "289671295359254529", + "114526764860047367", + "1040735082610167858" + ] + }, + public_flags: 0, + username: "botrac4r" + } + ], + pinned: false, + timestamp: "2023-08-16T03:06:06.777000+00:00", + tts: false, + type: 0 + }, edit_of_reply_to_skull_webp_attachment_with_content: { type: 19, tts: false, @@ -881,47 +1004,6 @@ module.exports = { } ], guild_id: "112760669178241024" - }, - sticker: { - id: "1106366167788044450", - type: 0, - content: "can have attachments too", - channel_id: "122155380120748034", - author: { - id: "113340068197859328", - username: "Cookie 🍪", - global_name: null, - display_name: null, - avatar: "b48302623a12bc7c59a71328f72ccb39", - discriminator: "7766", - public_flags: 128, - avatar_decoration: null - }, - attachments: [{ - id: "1106366167486038016", - filename: "image.png", - size: 127373, - url: "https://cdn.discordapp.com/attachments/122155380120748034/1106366167486038016/image.png", - proxy_url: "https://media.discordapp.net/attachments/122155380120748034/1106366167486038016/image.png", - width: 333, - height: 287, - content_type: "image/png" - }], - embeds: [], - mentions: [], - mention_roles: [], - pinned: false, - mention_everyone: false, - tts: false, - timestamp: "2023-05-11T23:44:09.690000+00:00", - edited_timestamp: null, - flags: 0, - components: [], - sticker_items: [{ - id: "1106323941183717586", - format_type: 1, - name: "pomu puff" - }] } } } diff --git a/test/test.js b/test/test.js index 5805d09..c6ee064 100644 --- a/test/test.js +++ b/test/test.js @@ -15,6 +15,7 @@ require("../matrix/kstate.test") require("../matrix/api.test") require("../matrix/read-registration.test") require("../d2m/converters/message-to-event.test") +require("../d2m/converters/edit-to-changes.test") require("../d2m/actions/create-room.test") require("../d2m/converters/user-to-mxid.test") require("../d2m/actions/register-user.test") diff --git a/types.d.ts b/types.d.ts index eeb4b75..76d3bd1 100644 --- a/types.d.ts +++ b/types.d.ts @@ -38,6 +38,14 @@ namespace Event { event_id: string } + export type ReplacementContent = T & { + "m.new_content": T + "m.relates_to": { + rel_type: string // "m.replace" + event_id: string + } + } + export type BaseStateEvent = { type: string room_id: string