From c7868e9dbb605d5c58b69542f5b88f3b86737757 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 5 May 2023 08:25:00 +1200 Subject: [PATCH 01/99] space and room creation --- d2m/actions/create-room.js | 113 ++++++++++++++++++++++++++++++------ d2m/actions/create-space.js | 46 +++++++++++++++ d2m/actions/send-message.js | 4 +- d2m/discord-packets.js | 3 + d2m/event-dispatcher.js | 15 +++-- index.js | 4 +- matrix/file.js | 63 ++++++++++++++++++++ matrix/mreq.js | 47 +++++++++++++++ matrix/read-registration.js | 8 +-- notes.md | 25 +++++++- package-lock.json | 9 +++ package.json | 1 + passthrough.js | 1 + stdin.js | 7 ++- types.d.ts | 18 ++++++ 15 files changed, 328 insertions(+), 36 deletions(-) create mode 100644 d2m/actions/create-space.js create mode 100644 matrix/file.js create mode 100644 matrix/mreq.js diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 7e00651..b4934dc 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -1,22 +1,99 @@ // @ts-check -const reg = require("../../matrix/read-registration.js") -const fetch = require("node-fetch") +const assert = require("assert").strict +const DiscordTypes = require("discord-api-types/v10") -fetch("https://matrix.cadence.moe/_matrix/client/v3/createRoom?user_id=@_ooye_example:cadence.moe", { - method: "POST", - body: JSON.stringify({ - invite: ["@cadence:cadence.moe"], - is_direct: false, - name: "New Bot User Room", - preset: "trusted_private_chat" - }), - headers: { - Authorization: `Bearer ${reg.as_token}` +const passthrough = require("../../passthrough") +const { discord, sync, db } = passthrough +/** @type {import("../../matrix/mreq")} */ +const mreq = sync.require("../../matrix/mreq") +/** @type {import("../../matrix/file")} */ +const file = sync.require("../../matrix/file") + +/** + * @param {import("discord-api-types/v10").APIGuildTextChannel} channel + */ +async function createRoom(channel) { + const guildID = channel.guild_id + assert.ok(guildID) + const guild = discord.guilds.get(guildID) + assert.ok(guild) + const spaceID = db.prepare("SELECT space_id FROM guild_space WHERE guild_id = ?").pluck().get(guildID) + assert.ok(typeof spaceID === "string") + + const avatarEventContent = {} + if (guild.icon) { + avatarEventContent.url = await file.uploadDiscordFileToMxc(file.guildIcon(guild)) } -}).then(res => res.text()).then(text => { - // {"room_id":"!aAVaqeAKwChjWbsywj:cadence.moe"} - console.log(text) -}).catch(err => { - console.log(err) -}) + + /** @type {import("../../types").R_RoomCreated} */ + const root = await mreq.mreq("POST", "/client/v3/createRoom", { + name: channel.name, + topic: channel.topic || undefined, + preset: "private_chat", + visibility: "private", + invite: ["@cadence:cadence.moe"], // TODO + initial_state: [ + { + type: "m.room.avatar", + state_key: "", + content: avatarEventContent + }, + { + type: "m.room.guest_access", + state_key: "", + content: { + guest_access: "can_join" + } + }, + { + type: "m.room.history_visibility", + state_key: "", + content: { + history_visibility: "invited" + } + }, + { + type: "m.space.parent", + state_key: spaceID, + content: { + via: ["cadence.moe"], // TODO: put the proper server here + canonical: true + } + }, + { + type: "m.room.join_rules", + content: { + join_rule: "restricted", + allow: [{ + type: "m.room.membership", + room_id: spaceID + }] + } + } + ] + }) + + db.prepare("INSERT INTO channel_room (channel_id, room_id) VALUES (?, ?)").run(channel.id, root.room_id) + + // Put the newly created child into the space + await mreq.mreq("PUT", `/client/v3/rooms/${spaceID}/state/m.space.child/${root.room_id}`, { + via: ["cadence.moe"] // TODO: use the proper server + }) +} + +async function createAllForGuild(guildID) { + const channelIDs = discord.guildChannelMap.get(guildID) + assert.ok(channelIDs) + for (const channelID of channelIDs) { + const channel = discord.channels.get(channelID) + assert.ok(channel) + const existing = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(channel.id) + if (channel.type === DiscordTypes.ChannelType.GuildText && !existing) { + await createRoom(channel) + } + } +} + +module.exports.createRoom = createRoom +module.exports.createAllForGuild = createAllForGuild diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js new file mode 100644 index 0000000..6d3c327 --- /dev/null +++ b/d2m/actions/create-space.js @@ -0,0 +1,46 @@ +// @ts-check + +const passthrough = require("../../passthrough") +const { sync, db } = passthrough +/** @type {import("../../matrix/mreq")} */ +const mreq = sync.require("../../matrix/mreq") + +/** + * @param {import("discord-api-types/v10").RESTGetAPIGuildResult} guild + */ +function createSpace(guild) { + return mreq.mreq("POST", "/client/v3/createRoom", { + name: guild.name, + preset: "private_chat", + visibility: "private", + power_level_content_override: { + events_default: 100, + invite: 50 + }, + invite: ["@cadence:cadence.moe"], // TODO + topic: guild.description || undefined, + creation_content: { + type: "m.space" + }, + initial_state: [ + { + type: "m.room.guest_access", + state_key: "", + content: { + guest_access: "can_join" + } + }, + { + type: "m.room.history_visibility", + content: { + history_visibility: "invited" + } + } + ] + }).then(/** @param {import("../../types").R_RoomCreated} root */ root => { + db.prepare("INSERT INTO guild_space (guild_id, space_id) VALUES (?, ?)").run(guild.id, root.room_id) + return root + }) +} + +module.exports.createSpace = createSpace diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index a4dd9a2..b736a50 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -10,7 +10,7 @@ const messageToEvent = require("../converters/message-to-event.js") */ function sendMessage(message) { const event = messageToEvent(message) - fetch(`https://matrix.cadence.moe/_matrix/client/v3/rooms/!VwVlIAjOjejUpDhlbA:cadence.moe/send/m.room.message/${makeTxnId()}?user_id=@_ooye_example:cadence.moe`, { + return fetch(`https://matrix.cadence.moe/_matrix/client/v3/rooms/!VwVlIAjOjejUpDhlbA:cadence.moe/send/m.room.message/${makeTxnId()}?user_id=@_ooye_example:cadence.moe`, { method: "PUT", body: JSON.stringify(event), headers: { @@ -24,4 +24,4 @@ function sendMessage(message) { }) } -module.exports = sendMessage +module.exports.sendMessage = sendMessage diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index fe97d20..0d16cdc 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -2,6 +2,7 @@ // Discord library internals type beat +const DiscordTypes = require("discord-api-types/v10") const passthrough = require("../passthrough") const { sync } = passthrough @@ -27,6 +28,8 @@ const utils = { const arr = [] client.guildChannelMap.set(message.d.id, arr) for (const channel of message.d.channels || []) { + // @ts-ignore + channel.guild_id = message.d.id arr.push(channel.id) client.channels.set(channel.id, channel) } diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 13626f4..e4de37e 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -1,8 +1,14 @@ // @ts-check -// Grab Discord events we care about for the bridge, check them, and pass them on +const {sync} = require("../passthrough") -const sendMessage = require("./actions/send-message") +/** @type {import("./actions/create-space")}) */ +const createSpace = sync.require("./actions/create-space") + +/** @type {import("./actions/send-message")}) */ +const sendMessage = sync.require("./actions/send-message") + +// Grab Discord events we care about for the bridge, check them, and pass them on module.exports = { /** @@ -10,10 +16,7 @@ module.exports = { * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message */ onMessageCreate(client, message) { - console.log(message) - console.log(message.guild_id) - console.log(message.member) - sendMessage(message) + sendMessage.sendMessage(message) }, /** diff --git a/index.js b/index.js index c6340f8..d1c721c 100644 --- a/index.js +++ b/index.js @@ -1,13 +1,15 @@ // @ts-check +const sqlite = require("better-sqlite3") const HeatSync = require("heatsync") const config = require("./config") const passthrough = require("./passthrough") +const db = new sqlite("db/ooye.db") const sync = new HeatSync() -Object.assign(passthrough, { config, sync }) +Object.assign(passthrough, { config, sync, db }) const DiscordClient = require("./d2m/discord-client") diff --git a/matrix/file.js b/matrix/file.js new file mode 100644 index 0000000..f5e81ce --- /dev/null +++ b/matrix/file.js @@ -0,0 +1,63 @@ +// @ts-check + +const fetch = require("node-fetch") + +const passthrough = require("../passthrough") +const { sync, db } = passthrough +/** @type {import("./mreq")} */ +const mreq = sync.require("./mreq") + +const DISCORD_IMAGES_BASE = "https://cdn.discordapp.com" +const IMAGE_SIZE = 1024 + +/** @type {Map>} */ +const inflight = new Map() + +/** + * @param {string} path + */ +async function uploadDiscordFileToMxc(path) { + const url = DISCORD_IMAGES_BASE + path + + // Are we uploading this file RIGHT NOW? Return the same inflight promise with the same resolution + let existing = inflight.get(url) + if (typeof existing === "string") { + return existing + } + + // Has this file already been uploaded in the past? Grab the existing copy from the database. + existing = db.prepare("SELECT mxc_url FROM file WHERE discord_url = ?").pluck().get(url) + if (typeof existing === "string") { + return existing + } + + // Download from Discord + const promise = fetch(url, {}).then(/** @param {import("node-fetch").Response} res */ async res => { + /** @ts-ignore @type {import("stream").Readable} body */ + const body = res.body + + // Upload to Matrix + /** @type {import("../types").R_FileUploaded} */ + const root = await mreq.mreq("POST", "/media/v3/upload", body, { + headers: { + "Content-Type": res.headers.get("content-type") + } + }) + + // Store relationship in database + db.prepare("INSERT INTO file (discord_url, mxc_url) VALUES (?, ?)").run(url, root.content_uri) + inflight.delete(url) + + return root.content_uri + }) + inflight.set(url, promise) + + return promise +} + +function guildIcon(guild) { + return `/icons/${guild.id}/${guild.icon}?size=${IMAGE_SIZE}` +} + +module.exports.guildIcon = guildIcon +module.exports.uploadDiscordFileToMxc = uploadDiscordFileToMxc diff --git a/matrix/mreq.js b/matrix/mreq.js new file mode 100644 index 0000000..ad7fa46 --- /dev/null +++ b/matrix/mreq.js @@ -0,0 +1,47 @@ +// @ts-check + +const fetch = require("node-fetch") +const mixin = require("mixin-deep") + +const passthrough = require("../passthrough") +const { sync } = passthrough +/** @type {import("./read-registration")} */ +const reg = sync.require("./read-registration.js") + +const baseUrl = "https://matrix.cadence.moe/_matrix" + +class MatrixServerError { + constructor(data) { + this.data = data + /** @type {string} */ + this.errcode = data.errcode + /** @type {string} */ + this.error = data.error + } +} + +/** + * @param {string} method + * @param {string} url + * @param {any} [body] + * @param {any} [extra] + */ +function mreq(method, url, body, extra = {}) { + const opts = mixin({ + method, + body: (body == undefined || Object.is(body.constructor, Object)) ? JSON.stringify(body) : body, + headers: { + Authorization: `Bearer ${reg.as_token}` + } + }, extra) + console.log(baseUrl + url, opts) + return fetch(baseUrl + url, opts).then(res => { + return res.json().then(root => { + if (!res.ok || root.errcode) throw new MatrixServerError(root) + return root + }) + }) +} + +module.exports.MatrixServerError = MatrixServerError +module.exports.mreq = mreq diff --git a/matrix/read-registration.js b/matrix/read-registration.js index ee17f28..cc3fed8 100644 --- a/matrix/read-registration.js +++ b/matrix/read-registration.js @@ -3,11 +3,5 @@ const fs = require("fs") const yaml = require("js-yaml") -/** - * @typedef AppServiceRegistrationConfig - * @property {string} id - * @property {string} as_token - * @property {string} hs_token - */ - +/** @type {import("../types").AppServiceRegistrationConfig} */ module.exports = yaml.load(fs.readFileSync("registration.yaml", "utf8")) diff --git a/notes.md b/notes.md index eed5990..e63d9e5 100644 --- a/notes.md +++ b/notes.md @@ -36,6 +36,13 @@ Public channels in that server should then use the following settings, so that t - Find & join access: Space members (so users must have been invited to the space already, even if they find out the room ID to join) - Who can read history: Anyone (so that people can see messages during the preview before joining) +Step by step process: + +1. Create a space room for the guild. Store the guild-space ID relationship in the database. Configure the space room to act like a space. + - `{"name":"NAME","preset":"private_chat","visibility":"private","power_level_content_override":{"events_default":100,"invite":50},"topic":"TOPIC","creation_content":{"type":"m.space"},"initial_state":[{"type":"m.room.guest_access","state_key":"","content":{"guest_access":"can_join"}},{"type":"m.room.history_visibility","content":{"history_visibility":"invited"}}]}` +2. Create channel rooms for the channels. Store the channel-room ID relationship in the database. (Probably no need to store parent-child relationships in the database?) +3. Send state events to put the channel rooms in the space. + ### Private channels Discord **channels** that disallow view permission to @everyone should instead have the following **room** settings in Matrix: @@ -58,6 +65,13 @@ The context-sensitive /invite command will invite Matrix users to the correspond 1. Transform content. 2. Send to matrix. +## Webhook message sent + +- Consider using the _ooye_bot account to send all webhook messages to prevent extraneous joins? + - Downside: the profile information from the most recently sent message would stick around in the member list. This is toleable. +- Otherwise, could use an account per webhook ID, but if webhook IDs are often deleted and re-created, this could still end up leaving too many accounts in the room. +- The original bridge uses an account per webhook display name, which does the most sense in terms of canonical accounts, but leaves too many accounts in the room. + ## Message deleted 1. Look up equivalents on matrix. @@ -91,4 +105,13 @@ The context-sensitive /invite command will invite Matrix users to the correspond 1. Create the corresponding room. 2. Add to database. 3. Update room details to match. -4. Add to space. +4. Make sure the permissions are correct according to the rules above! +5. Add to space. + +## Emojis updated + +1. Upload any newly added images to msc. +2. Create or replace state event for the bridged pack. (Can just use key "ooye" and display name "Discord", or something, for this pack.) +3. The emojis may now be sent by Matrix users! + +TOSPEC: m2d emoji uploads?? diff --git a/package-lock.json b/package-lock.json index a653203..31df4c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "js-yaml": "^4.1.0", "matrix-appservice": "^2.0.0", "matrix-js-sdk": "^24.1.0", + "mixin-deep": "^2.0.1", "node-fetch": "^2.6.7", "snowtransfer": "^0.7.0", "supertape": "^8.3.0" @@ -1696,6 +1697,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mixin-deep": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-2.0.1.tgz", + "integrity": "sha512-imbHQNRglyaplMmjBLL3V5R6Bfq5oM+ivds3SKgc6oRtzErEnBUUc5No11Z2pilkUvl42gJvi285xTNswcKCMA==", + "engines": { + "node": ">=6" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", diff --git a/package.json b/package.json index 7a7de4a..6755f9f 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "js-yaml": "^4.1.0", "matrix-appservice": "^2.0.0", "matrix-js-sdk": "^24.1.0", + "mixin-deep": "^2.0.1", "node-fetch": "^2.6.7", "snowtransfer": "^0.7.0", "supertape": "^8.3.0" diff --git a/passthrough.js b/passthrough.js index b6bc56f..8ef75db 100644 --- a/passthrough.js +++ b/passthrough.js @@ -6,6 +6,7 @@ * @property {typeof import("./config")} config * @property {import("./d2m/discord-client")} discord * @property {import("heatsync")} sync + * @property {import("better-sqlite3/lib/database")} db */ /** @type {Passthrough} */ // @ts-ignore diff --git a/stdin.js b/stdin.js index be38b06..fb95809 100644 --- a/stdin.js +++ b/stdin.js @@ -4,7 +4,12 @@ const repl = require("repl") const util = require("util") const passthrough = require("./passthrough") -const { discord, config, sync } = passthrough +const { discord, config, sync, db } = passthrough + +const createSpace = sync.require("./d2m/actions/create-space.js") +const createRoom = sync.require("./d2m/actions/create-room.js") +const mreq = sync.require("./matrix/mreq.js") +const guildID = "112760669178241024" const extraContext = {} diff --git a/types.d.ts b/types.d.ts index 180a559..d571359 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1,6 +1,24 @@ +export type AppServiceRegistrationConfig = { + id: string + as_token: string + hs_token: string + url: string + sender_localpart: string + protocols: [string] + rate_limited: boolean +} + export type M_Room_Message_content = { msgtype: "m.text" body: string formatted_body?: "org.matrix.custom.html" format?: string } + +export type R_RoomCreated = { + room_id: string +} + +export type R_FileUploaded = { + content_uri: string +} From f09eeccef32d1ea8d0fe58a3ac3c905b063cd855 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 5 May 2023 17:29:08 +1200 Subject: [PATCH 02/99] add tests, implement kstate and state diffing --- d2m/actions/create-room.js | 186 ++- d2m/actions/send-message.js | 4 +- d2m/converters/message-to-event.js | 4 +- matrix/file.js | 5 +- matrix/mreq.js | 16 +- matrix/read-registration.js | 5 +- package-lock.json | 2397 +++++++++++++++++++++++++++- package.json | 3 + test/data.js | 85 + test/test.js | 15 + 10 files changed, 2656 insertions(+), 64 deletions(-) create mode 100644 test/data.js create mode 100644 test/test.js diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index b4934dc..5af26f5 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -1,6 +1,8 @@ // @ts-check const assert = require("assert").strict +const {test} = require("supertape") +const testData = require("../../test/data") const DiscordTypes = require("discord-api-types/v10") const passthrough = require("../../passthrough") @@ -10,22 +12,134 @@ const mreq = sync.require("../../matrix/mreq") /** @type {import("../../matrix/file")} */ const file = sync.require("../../matrix/file") +function kstateToState(kstate) { + return Object.entries(kstate).map(([k, content]) => { + console.log(k) + const [type, state_key] = k.split("/") + assert.ok(typeof type === "string") + assert.ok(typeof state_key === "string") + return {type, state_key, content} + }) +} + +test("kstate2state: general", t => { + t.deepEqual(kstateToState({ + "m.room.name/": {name: "test name"}, + "m.room.member/@cadence:cadence.moe": {membership: "join"} + }), [ + { + type: "m.room.name", + state_key: "", + content: { + name: "test name" + } + }, + { + type: "m.room.member", + state_key: "@cadence:cadence.moe", + content: { + membership: "join" + } + } + ]) +}) + +function diffKState(actual, target) { + const diff = {} + // go through each key that it should have + for (const key of Object.keys(target)) { + if (key in actual) { + // diff + try { + assert.deepEqual(actual[key], target[key]) + } catch (e) { + // they differ. reassign the target + diff[key] = target[key] + } + } else { + // not present, needs to be added + diff[key] = target[key] + } + } + return diff +} + +test("diffKState: detects edits", t => { + t.deepEqual( + diffKState({ + "m.room.name/": {name: "test name"}, + "same/": {a: 2} + }, { + "m.room.name/": {name: "edited name"}, + "same/": {a: 2} + }), + { + "m.room.name/": {name: "edited name"} + } + ) +}) + +test("diffKState: detects new properties", t => { + t.deepEqual( + diffKState({ + "m.room.name/": {name: "test name"}, + }, { + "m.room.name/": {name: "test name"}, + "new/": {a: 2} + }), + { + "new/": {a: 2} + } + ) +}) + /** * @param {import("discord-api-types/v10").APIGuildTextChannel} channel + * @param {import("discord-api-types/v10").APIGuild} guild */ -async function createRoom(channel) { - const guildID = channel.guild_id - assert.ok(guildID) - const guild = discord.guilds.get(guildID) - assert.ok(guild) - const spaceID = db.prepare("SELECT space_id FROM guild_space WHERE guild_id = ?").pluck().get(guildID) +async function channelToKState(channel, guild) { + const spaceID = db.prepare("SELECT space_id FROM guild_space WHERE guild_id = ?").pluck().get(guild.id) assert.ok(typeof spaceID === "string") const avatarEventContent = {} if (guild.icon) { - avatarEventContent.url = await file.uploadDiscordFileToMxc(file.guildIcon(guild)) + avatarEventContent.discord_path = file.guildIcon(guild) + avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) } + const kstate = { + "m.room.name/": {name: channel.name}, + "m.room.topic/": {topic: channel.topic || undefined}, + "m.room.avatar/": avatarEventContent, + "m.room.guest_access/": {guest_access: "can_join"}, + "m.room.history_visibility/": {history_visibility: "invited"}, + [`m.space.parent/${spaceID}`]: { // TODO: put the proper server here + via: ["cadence.moe"], + canonical: true + }, + "m.room.join_rules/": { + join_rule: "restricted", + allow: [{ + type: "m.room.membership", + room_id: spaceID + }] + } + } + + return {spaceID, kstate} +} + +test("channel2room: general", async t => { + t.deepEqual(await channelToKState(testData.channel.general, testData.guild.general).then(x => x.kstate), {expected: true, ...testData.room.general}) +}) + +/** + * @param {import("discord-api-types/v10").APIGuildTextChannel} channel + * @param guild + * @param {string} spaceID + * @param {any} kstate + */ +async function createRoom(channel, guild, spaceID, kstate) { /** @type {import("../../types").R_RoomCreated} */ const root = await mreq.mreq("POST", "/client/v3/createRoom", { name: channel.name, @@ -33,45 +147,7 @@ async function createRoom(channel) { preset: "private_chat", visibility: "private", invite: ["@cadence:cadence.moe"], // TODO - initial_state: [ - { - type: "m.room.avatar", - state_key: "", - content: avatarEventContent - }, - { - type: "m.room.guest_access", - state_key: "", - content: { - guest_access: "can_join" - } - }, - { - type: "m.room.history_visibility", - state_key: "", - content: { - history_visibility: "invited" - } - }, - { - type: "m.space.parent", - state_key: spaceID, - content: { - via: ["cadence.moe"], // TODO: put the proper server here - canonical: true - } - }, - { - type: "m.room.join_rules", - content: { - join_rule: "restricted", - allow: [{ - type: "m.room.membership", - room_id: spaceID - }] - } - } - ] + initial_state: kstateToState(kstate) }) db.prepare("INSERT INTO channel_room (channel_id, room_id) VALUES (?, ?)").run(channel.id, root.room_id) @@ -82,6 +158,24 @@ async function createRoom(channel) { }) } +/** + * @param {import("discord-api-types/v10").APIGuildTextChannel} channel + */ +async function syncRoom(channel) { + const guildID = channel.guild_id + assert(guildID) + const guild = discord.guilds.get(guildID) + assert(guild) + + const {spaceID, kstate} = await channelToKState(channel, guild) + + /** @type {string?} */ + const existing = db.prepare("SELECT room_id from channel_room WHERE channel_id = ?").pluck().get(channel.id) + if (!existing) { + createRoom(channel, guild, spaceID, kstate) + } +} + async function createAllForGuild(guildID) { const channelIDs = discord.guildChannelMap.get(guildID) assert.ok(channelIDs) diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index b736a50..1f71a66 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -2,14 +2,14 @@ const reg = require("../../matrix/read-registration.js") const makeTxnId = require("../../matrix/txnid.js") -const fetch = require("node-fetch") +const fetch = require("node-fetch").default const messageToEvent = require("../converters/message-to-event.js") /** * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message */ function sendMessage(message) { - const event = messageToEvent(message) + const event = messageToEvent.messageToEvent(message) return fetch(`https://matrix.cadence.moe/_matrix/client/v3/rooms/!VwVlIAjOjejUpDhlbA:cadence.moe/send/m.room.message/${makeTxnId()}?user_id=@_ooye_example:cadence.moe`, { method: "PUT", body: JSON.stringify(event), diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index c9d158e..6f9bc37 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -6,7 +6,7 @@ const markdown = require("discord-markdown") * @param {import("discord-api-types/v10").APIMessage} message * @returns {import("../../types").M_Room_Message_content} */ -module.exports = function messageToEvent(message) { +function messageToEvent(message) { const body = message.content const html = markdown.toHTML(body, { /* discordCallback: { @@ -24,3 +24,5 @@ module.exports = function messageToEvent(message) { formatted_body: html } } + +module.exports.messageToEvent = messageToEvent \ No newline at end of file diff --git a/matrix/file.js b/matrix/file.js index f5e81ce..d242f52 100644 --- a/matrix/file.js +++ b/matrix/file.js @@ -1,6 +1,6 @@ // @ts-check -const fetch = require("node-fetch") +const fetch = require("node-fetch").default const passthrough = require("../passthrough") const { sync, db } = passthrough @@ -33,7 +33,6 @@ async function uploadDiscordFileToMxc(path) { // Download from Discord const promise = fetch(url, {}).then(/** @param {import("node-fetch").Response} res */ async res => { - /** @ts-ignore @type {import("stream").Readable} body */ const body = res.body // Upload to Matrix @@ -56,7 +55,7 @@ async function uploadDiscordFileToMxc(path) { } function guildIcon(guild) { - return `/icons/${guild.id}/${guild.icon}?size=${IMAGE_SIZE}` + return `/icons/${guild.id}/${guild.icon}.png?size=${IMAGE_SIZE}` } module.exports.guildIcon = guildIcon diff --git a/matrix/mreq.js b/matrix/mreq.js index ad7fa46..b3e12aa 100644 --- a/matrix/mreq.js +++ b/matrix/mreq.js @@ -1,6 +1,6 @@ // @ts-check -const fetch = require("node-fetch") +const fetch = require("node-fetch").default const mixin = require("mixin-deep") const passthrough = require("../passthrough") @@ -26,7 +26,7 @@ class MatrixServerError { * @param {any} [body] * @param {any} [extra] */ -function mreq(method, url, body, extra = {}) { +async function mreq(method, url, body, extra = {}) { const opts = mixin({ method, body: (body == undefined || Object.is(body.constructor, Object)) ? JSON.stringify(body) : body, @@ -34,13 +34,13 @@ function mreq(method, url, body, extra = {}) { Authorization: `Bearer ${reg.as_token}` } }, extra) + console.log(baseUrl + url, opts) - return fetch(baseUrl + url, opts).then(res => { - return res.json().then(root => { - if (!res.ok || root.errcode) throw new MatrixServerError(root) - return root - }) - }) + const res = await fetch(baseUrl + url, opts) + const root = await res.json() + + if (!res.ok || root.errcode) throw new MatrixServerError(root) + return root } module.exports.MatrixServerError = MatrixServerError diff --git a/matrix/read-registration.js b/matrix/read-registration.js index cc3fed8..a1d920d 100644 --- a/matrix/read-registration.js +++ b/matrix/read-registration.js @@ -3,5 +3,6 @@ const fs = require("fs") const yaml = require("js-yaml") -/** @type {import("../types").AppServiceRegistrationConfig} */ -module.exports = yaml.load(fs.readFileSync("registration.yaml", "utf8")) +/** @ts-ignore @type {import("../types").AppServiceRegistrationConfig} */ +const reg = yaml.load(fs.readFileSync("registration.yaml", "utf8")) +module.exports = reg \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 31df4c7..654d022 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,7 +1,7 @@ { "name": "out-of-your-element", "version": "1.0.0", - "lockfileVersion": 3, + "lockfileVersion": 2, "requires": true, "packages": { "": { @@ -22,7 +22,8 @@ "supertape": "^8.3.0" }, "devDependencies": { - "@types/node": "^18.16.0" + "@types/node": "^18.16.0", + "tap-dot": "github:alandelaney-whs/tap-dot#32d909760fc177c83a6738fecf0c8c7eb3a7b2bf" } }, "node_modules/@babel/runtime": { @@ -656,6 +657,12 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, "node_modules/csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", @@ -834,6 +841,15 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1127,6 +1143,27 @@ "node": ">= 0.4.0" } }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -1951,6 +1988,12 @@ "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==" }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2022,6 +2065,12 @@ "rc": "cli.js" } }, + "node_modules/re-emitter": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/re-emitter/-/re-emitter-1.1.4.tgz", + "integrity": "sha512-C0SIXdXDSus2yqqvV7qifnb4NoWP7mEBXJq3axci301mXHCZb8Djwm4hrEZo4UeXRaEnfjH98uQ8EBppk2oNWA==", + "dev": true + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -2276,6 +2325,18 @@ "node": ">=0.10.0" } }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, "node_modules/stacktracey": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", @@ -2424,6 +2485,131 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tap-dot": { + "version": "2.0.0", + "resolved": "git+ssh://git@github.com/alandelaney-whs/tap-dot.git#32d909760fc177c83a6738fecf0c8c7eb3a7b2bf", + "integrity": "sha512-r3EdqKvdl8qy4OxB0oy3YuibtCGy0hbNKUITLfMlblCVnTgWZEDmlhxsV0Dfn5B3xwcZQTokjvaygKDUGIe+WA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^1.1.1", + "tap-out": "github:alandelaney-whs/tap-out", + "through2": "^2.0.0" + }, + "bin": { + "tap-dot": "bin/dot" + } + }, + "node_modules/tap-dot/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tap-dot/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tap-dot/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tap-dot/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tap-dot/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/tap-out": { + "name": "tap-in", + "version": "3.2.1", + "resolved": "git+ssh://git@github.com/alandelaney-whs/tap-out.git#c93556c36b8c7013d38ff12f9ce156eb06f734cb", + "dev": true, + "license": "MIT", + "dependencies": { + "re-emitter": "1.1.4", + "readable-stream": "2.3.7", + "split": "1.0.1", + "trim": "1.0.1" + }, + "bin": { + "tap-in": "bin/tap-in.js" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/tap-out/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/tap-out/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/tap-out/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/tap-out/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", @@ -2450,6 +2636,58 @@ "node": ">=6" } }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -2463,6 +2701,13 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/trim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim/-/trim-1.0.1.tgz", + "integrity": "sha512-3JVP2YVqITUisXblCDq/Bi4P9457G/sdEamInkyvCsjbTcXLXIiG7XCb4kGMFWh6JGXesS3TKxOPtrncN/xe8w==", + "deprecated": "Use String.prototype.trim() instead", + "dev": true + }, "node_modules/try-catch": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/try-catch/-/try-catch-3.0.1.tgz", @@ -2608,6 +2853,15 @@ "resolved": "https://registry.npmjs.org/wraptile/-/wraptile-3.0.0.tgz", "integrity": "sha512-23LJhkIw940uTcDFyJZmNyO0z8lEINOTGCr4vR5YCG3urkdXwduRIhivBm9wKaVynLHYvxoHHYbKsDiafCLp6w==" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -2621,5 +2875,2144 @@ "node": ">=12" } } + }, + "dependencies": { + "@babel/runtime": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", + "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "@cloudcmd/stub": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@cloudcmd/stub/-/stub-4.0.1.tgz", + "integrity": "sha512-7x7tVxJZOdQowHv/VKwHLo9aoNNoVRc6PdKYqyKcDHX+xrF78jSXnqEWrOplnD/gF+tCnyFafu1Is+lFfWCILw==", + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^27.0.6", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + }, + "diff-sequences": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==" + }, + "jest-diff": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", + "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" + } + }, + "jest-get-type": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==" + }, + "pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "requires": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + } + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "@jest/schemas": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", + "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "requires": { + "@sinclair/typebox": "^0.25.16" + } + }, + "@matrix-org/matrix-sdk-crypto-js": { + "version": "0.1.0-alpha.7", + "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.7.tgz", + "integrity": "sha512-sQEG9cSfNji5NYBf5h7j5IxYVO0dwtAKoetaVyR+LhIXz/Su7zyEE3EwlAWAeJOFdAV/vZ5LTNyh39xADuNlTg==" + }, + "@putout/cli-keypress": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@putout/cli-keypress/-/cli-keypress-1.0.0.tgz", + "integrity": "sha512-w+lRVGZodRM4K214R4jvyOsmCUGA3OnaYDOJ2rpXj6a+O6n91zLlkb7JYsw6I0QCNmXjpNLJSoLgzGWTue6YIg==", + "requires": { + "ci-info": "^3.1.1", + "fullstore": "^3.0.0" + } + }, + "@putout/cli-validate-args": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@putout/cli-validate-args/-/cli-validate-args-1.1.1.tgz", + "integrity": "sha512-AczBS98YyvsDVxvvYjHGyIygFu3i/EJ0xsruU6MlytTuUiCFQIE/QQPDy1bcN5J2Y75BzSYncaYnVrEGcBjeeQ==", + "requires": { + "fastest-levenshtein": "^1.0.12", + "just-kebab-case": "^1.1.0" + } + }, + "@sinclair/typebox": { + "version": "0.25.24", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", + "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==" + }, + "@supertape/engine-loader": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@supertape/engine-loader/-/engine-loader-1.1.3.tgz", + "integrity": "sha512-5ilgEng0WBvMQjNJWQ/bnAA6HKgbLKxTya2C0RxFH0LYSN5faBVtgxjLDvTQ+5L+ZxjK/7ooQDDaRS1Mo0ga5Q==", + "requires": { + "try-catch": "^3.0.0" + } + }, + "@supertape/formatter-fail": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@supertape/formatter-fail/-/formatter-fail-3.0.2.tgz", + "integrity": "sha512-mSBnNprfLFmGvZkP+ODGroPLFCIN5BWE/06XaD5ghiTVWqek7eH8IDqvKyEduvuQu1O5tvQiaTwQsyxvikF+2w==", + "requires": { + "@supertape/formatter-tap": "^3.0.3", + "fullstore": "^3.0.0" + } + }, + "@supertape/formatter-json-lines": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@supertape/formatter-json-lines/-/formatter-json-lines-2.0.1.tgz", + "integrity": "sha512-9LWOCu4yOF9orf4QJseS8lP3hXkYn24qn57VqYt/3r2aRJv4vWTPfaL1ot5JRHCZs0qXrV1sqPmN6E05rRLDYA==", + "requires": { + "fullstore": "^3.0.0" + } + }, + "@supertape/formatter-progress-bar": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@supertape/formatter-progress-bar/-/formatter-progress-bar-3.0.0.tgz", + "integrity": "sha512-rVFAQ21eApq3TQV8taFLNcCxcGZvvOPxQC63swdmHFCp+07Dt3tvC/aFxF35NLobc3rySasGSEuPucpyoPrjfg==", + "requires": { + "chalk": "^4.1.0", + "ci-info": "^3.1.1", + "cli-progress": "^3.8.2", + "fullstore": "^3.0.0", + "once": "^1.4.0" + } + }, + "@supertape/formatter-short": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@supertape/formatter-short/-/formatter-short-2.0.1.tgz", + "integrity": "sha512-zxFrZfCccFV+bf6A7MCEqT/Xsf0Elc3qa0P3jShfdEfrpblEcpSo0T/Wd9jFwc7uHA3ABgxgcHy7LNIpyrFTCg==" + }, + "@supertape/formatter-tap": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@supertape/formatter-tap/-/formatter-tap-3.0.3.tgz", + "integrity": "sha512-U5OuMotfYhGo9cZ8IgdAXRTH5Yy8yfLDZzYo1upTPTwlJJquKwtvuz7ptiB7BN3OFr5YakkDYlFxOYPcLo7urg==" + }, + "@supertape/operator-stub": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@supertape/operator-stub/-/operator-stub-3.0.0.tgz", + "integrity": "sha512-LZ6E4nSMDMbLOhvEZyeXo8wS5EBiAAffWrohb7yaVHDVTHr+xkczzPxinkvcOBhNuAtC0kVARdMbHg+HULmozA==", + "requires": { + "@cloudcmd/stub": "^4.0.0" + } + }, + "@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" + }, + "@types/node": { + "version": "18.16.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.0.tgz", + "integrity": "sha512-BsAaKhB+7X+H4GnSjGhJG9Qi8Tw+inU9nJDwmD5CgOmBLEI6ArdhikpLX7DjbjDRDTbqZzU2LSQNZg8WGPiSZQ==", + "dev": true + }, + "@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + }, + "@types/react": { + "version": "18.0.38", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.38.tgz", + "integrity": "sha512-ExsidLLSzYj4cvaQjGnQCk4HFfVT9+EZ9XZsQ8Hsrcn8QNgXtpZ3m9vSIC2MWtx7jHictK6wYhQgGh6ic58oOw==", + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + }, + "@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "another-json": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/another-json/-/another-json-0.2.0.tgz", + "integrity": "sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg==" + }, + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "as-table": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", + "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", + "requires": { + "printable-characters": "^1.0.42" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" + }, + "backtracker": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/backtracker/-/backtracker-3.3.1.tgz", + "integrity": "sha512-bQTxQ/JL9nm8/mNFP/bkiOJN0w9OOK6LQDqa+Jt9YnnFGQzAplYwi2TDmzuEwHoAtuUso5StoyKvZazkPO4q4g==" + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base-x": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", + "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "requires": { + "safe-buffer": "5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "better-sqlite3": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-8.3.0.tgz", + "integrity": "sha512-JTmvBZL/JLTc+3Msbvq6gK6elbU9/wVMqiudplHrVJpr7sVMR9KJrNhZAbW+RhXKlpMcuEhYkdcHa3TXKNXQ1w==", + "requires": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.0" + } + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "requires": { + "base-x": "^4.0.0" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "centra": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/centra/-/centra-2.6.0.tgz", + "integrity": "sha512-dgh+YleemrT8u85QL11Z6tYhegAs3MMxsaWAq/oXeAmYJ7VxL3SI9TZtnfaEvNDMAPolj25FXIb3S+HCI4wQaQ==" + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==" + }, + "cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "requires": { + "string-width": "^4.2.3" + } + }, + "cloudstorm": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.7.0.tgz", + "integrity": "sha512-k+1u1kTdtlz3L6lnflAKMhkkZPoBl/2Du2czNvad2pYNOMBs8e0XZpSuCazC50Q29tzi08latn4SxtLbkws50A==", + "requires": { + "snowtransfer": "0.7.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "data-uri-to-buffer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", + "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==" + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + } + }, + "deep-equal": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz", + "integrity": "sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw==", + "requires": { + "call-bind": "^1.0.2", + "es-get-iterator": "^1.1.2", + "get-intrinsic": "^1.1.3", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" + }, + "diff-sequences": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", + "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==" + }, + "discord-api-types": { + "version": "0.37.39", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.39.tgz", + "integrity": "sha512-hkhQsQyzsTJITp311WXvHZh9j4RAMfIk2hPmsWeOTN50QTpg6zqmJNfel9D/8lYNvsU01wzw9281Yke8NhYyHg==" + }, + "discord-markdown": { + "version": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#24508e701e91d5a00fa5e773ced874d9ee8c889b", + "from": "discord-markdown@git+https://git.sr.ht/~cadence/nodejs-discord-markdown#24508e701e91d5a00fa5e773ced874d9ee8c889b", + "requires": { + "simple-markdown": "^0.7.2" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" + }, + "express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + } + } + }, + "fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==" + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "requires": { + "is-callable": "^1.1.3" + } + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "fullstore": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fullstore/-/fullstore-3.0.0.tgz", + "integrity": "sha512-EEIdG+HWpyygWRwSLIZy+x4u0xtghjHNfhQb0mI5825Mmjq6oFESFUY0hoZigEgd3KH8GX+ZOCK9wgmOiS7VBQ==" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" + }, + "get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "get-source": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", + "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", + "requires": { + "data-uri-to-buffer": "^2.0.0", + "source-map": "^0.6.1" + } + }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true + } + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "heatsync": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/heatsync/-/heatsync-2.4.0.tgz", + "integrity": "sha512-3avAZvdWohjVNhx/P1lHGEUriGP8VlbdFKrMsiBVbXzOGuEEKnC9840Qu4SyUWxgs0V1D3RIpNS3898NFgQkng==", + "requires": { + "backtracker": "3.3.1" + } + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "requires": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + } + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" + }, + "is-core-module": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", + "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==" + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==" + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + } + }, + "is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==" + }, + "is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, + "jest-diff": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", + "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + } + }, + "jest-get-type": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", + "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "just-kebab-case": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/just-kebab-case/-/just-kebab-case-1.1.0.tgz", + "integrity": "sha512-QkuwuBMQ9BQHMUEkAtIA4INLrkmnnveqlFB1oFi09gbU0wBdZo6tTnyxNWMR84zHxBuwK7GLAwqN8nrvVxOLTA==" + }, + "loglevel": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", + "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "matrix-appservice": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/matrix-appservice/-/matrix-appservice-2.0.0.tgz", + "integrity": "sha512-HCIuJ5i0YuO8b0dMyGe5dqlsE4f3RzHU0MuMg/2gGAZ4HL3r7aSWOFbyIWStSSUrk1qCa9Eml0i4EnEi0pOtdA==", + "requires": { + "body-parser": "^1.19.0", + "express": "^4.18.1", + "js-yaml": "^4.1.0", + "morgan": "^1.10.0" + } + }, + "matrix-events-sdk": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz", + "integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==" + }, + "matrix-js-sdk": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-24.1.0.tgz", + "integrity": "sha512-xEx2ZoNsS56dwgqLJ3rIv2SUpFxdQLrLKmJCpMatMUKCAg+NGuZfpQ3QXblIbGaqFNQZCH7fC7S48AeTMZp1Jw==", + "requires": { + "@babel/runtime": "^7.12.5", + "@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.5", + "another-json": "^0.2.0", + "bs58": "^5.0.0", + "content-type": "^1.0.4", + "loglevel": "^1.7.1", + "matrix-events-sdk": "0.0.1", + "matrix-widget-api": "^1.3.1", + "p-retry": "4", + "sdp-transform": "^2.14.1", + "unhomoglyph": "^1.0.6", + "uuid": "9" + }, + "dependencies": { + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + } + } + }, + "matrix-widget-api": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.3.1.tgz", + "integrity": "sha512-+rN6vGvnXm+fn0uq9r2KWSL/aPtehD6ObC50jYmUcEfgo8CUpf9eUurmjbRlwZkWq3XHXFuKQBUCI9UzqWg37Q==", + "requires": { + "@types/events": "^3.0.0", + "events": "^3.2.0" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "mixin-deep": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-2.0.1.tgz", + "integrity": "sha512-imbHQNRglyaplMmjBLL3V5R6Bfq5oM+ivds3SKgc6oRtzErEnBUUc5No11Z2pilkUvl42gJvi285xTNswcKCMA==" + }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "requires": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "requires": { + "ee-first": "1.1.1" + } + } + } + }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "node-abi": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.40.0.tgz", + "integrity": "sha512-zNy02qivjjRosswoYmPi8hIKJRr8MpQyeKT6qlcq/OnOgA3Rhoae+IYOqsM9V5+JnHWmxKnWOT2GxvtqdtOCXA==", + "requires": { + "semver": "^7.3.5" + } + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + }, + "object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "requires": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "requires": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + } + }, + "pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "requires": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + } + } + }, + "printable-characters": { + "version": "1.0.42", + "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "re-emitter": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/re-emitter/-/re-emitter-1.1.4.tgz", + "integrity": "sha512-C0SIXdXDSus2yqqvV7qifnb4NoWP7mEBXJq3axci301mXHCZb8Djwm4hrEZo4UeXRaEnfjH98uQ8EBppk2oNWA==", + "dev": true + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, + "regexp.prototype.flags": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" + } + }, + "resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "requires": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sdp-transform": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz", + "integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==" + }, + "semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + }, + "simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "requires": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "simple-markdown": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/simple-markdown/-/simple-markdown-0.7.3.tgz", + "integrity": "sha512-uGXIc13NGpqfPeFJIt/7SHHxd6HekEJYtsdoCM06mEBPL9fQH/pSD7LRM6PZ7CKchpSvxKL4tvwMamqAaNDAyg==", + "requires": { + "@types/react": ">=16.0.0" + } + }, + "snowtransfer": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.7.0.tgz", + "integrity": "sha512-vc7B46tO4QeK99z/pN8ISd8QvO9QB3Oo4qP7nYYhriIMOtjYkHMi8t6kUBPIJLbeX+h0NpfwxaGJfXNLm1ZQ5A==", + "requires": { + "centra": "^2.6.0", + "discord-api-types": "^0.37.31", + "form-data": "^4.0.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "requires": { + "through": "2" + } + }, + "stacktracey": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", + "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", + "requires": { + "as-table": "^1.0.36", + "get-source": "^2.0.12" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "requires": { + "internal-slot": "^1.0.4" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" + }, + "supertape": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/supertape/-/supertape-8.3.0.tgz", + "integrity": "sha512-dcMylmkr1Mctr5UBCrlvZynuBRuLvlkWJLGXdL/PcI41BERnObO+kV0PeZhH5n6lwVnvK2xfvZyN32WIAPf/tw==", + "requires": { + "@cloudcmd/stub": "^4.0.0", + "@putout/cli-keypress": "^1.0.0", + "@putout/cli-validate-args": "^1.0.1", + "@supertape/engine-loader": "^1.0.0", + "@supertape/formatter-fail": "^3.0.0", + "@supertape/formatter-json-lines": "^2.0.0", + "@supertape/formatter-progress-bar": "^3.0.0", + "@supertape/formatter-short": "^2.0.0", + "@supertape/formatter-tap": "^3.0.0", + "@supertape/operator-stub": "^3.0.0", + "cli-progress": "^3.8.2", + "deep-equal": "^2.0.3", + "fullstore": "^3.0.0", + "glob": "^8.0.3", + "jest-diff": "^29.0.1", + "once": "^1.4.0", + "resolve": "^1.17.0", + "stacktracey": "^2.1.7", + "strip-ansi": "^7.0.0", + "try-to-catch": "^3.0.0", + "wraptile": "^3.0.0", + "yargs-parser": "^21.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, + "tap-dot": { + "version": "git+ssh://git@github.com/alandelaney-whs/tap-dot.git#32d909760fc177c83a6738fecf0c8c7eb3a7b2bf", + "integrity": "sha512-r3EdqKvdl8qy4OxB0oy3YuibtCGy0hbNKUITLfMlblCVnTgWZEDmlhxsV0Dfn5B3xwcZQTokjvaygKDUGIe+WA==", + "dev": true, + "from": "tap-dot@github:alandelaney-whs/tap-dot#32d909760fc177c83a6738fecf0c8c7eb3a7b2bf", + "requires": { + "chalk": "^1.1.1", + "tap-out": "github:alandelaney-whs/tap-out", + "through2": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true + } + } + }, + "tap-out": { + "version": "git+ssh://git@github.com/alandelaney-whs/tap-out.git#c93556c36b8c7013d38ff12f9ce156eb06f734cb", + "dev": true, + "from": "tap-out@github:alandelaney-whs/tap-out", + "requires": { + "re-emitter": "1.1.4", + "readable-stream": "2.3.7", + "split": "1.0.1", + "trim": "1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "trim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim/-/trim-1.0.1.tgz", + "integrity": "sha512-3JVP2YVqITUisXblCDq/Bi4P9457G/sdEamInkyvCsjbTcXLXIiG7XCb4kGMFWh6JGXesS3TKxOPtrncN/xe8w==", + "dev": true + }, + "try-catch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/try-catch/-/try-catch-3.0.1.tgz", + "integrity": "sha512-91yfXw1rr/P6oLpHSyHDOHm0vloVvUoo9FVdw8YwY05QjJQG9OT0LUxe2VRAzmHG+0CUOmI3nhxDUMLxDN/NEQ==" + }, + "try-to-catch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-3.0.1.tgz", + "integrity": "sha512-hOY83V84Hx/1sCzDSaJA+Xz2IIQOHRvjxzt+F0OjbQGPZ6yLPLArMA0gw/484MlfUkQbCpKYMLX3VDCAjWKfzQ==" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "unhomoglyph": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/unhomoglyph/-/unhomoglyph-1.0.6.tgz", + "integrity": "sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "requires": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + } + }, + "which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "wraptile": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/wraptile/-/wraptile-3.0.0.tgz", + "integrity": "sha512-23LJhkIw940uTcDFyJZmNyO0z8lEINOTGCr4vR5YCG3urkdXwduRIhivBm9wKaVynLHYvxoHHYbKsDiafCLp6w==" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + } } } diff --git a/package.json b/package.json index 6755f9f..f8917e7 100644 --- a/package.json +++ b/package.json @@ -29,5 +29,8 @@ }, "devDependencies": { "@types/node": "^18.16.0" + }, + "scripts": { + "test": "supertape --format short test/test.js" } } diff --git a/test/data.js b/test/data.js new file mode 100644 index 0000000..42af177 --- /dev/null +++ b/test/data.js @@ -0,0 +1,85 @@ +// @ts-check + +const DiscordTypes = require("discord-api-types/v10") + +module.exports = { + channel: { + general: { + type: 0, + topic: 'https://docs.google.com/document/d/blah/edit | I spread, pipe, and whip because it is my will. :headstone:', + rate_limit_per_user: 0, + position: 0, + permission_overwrites: [], + parent_id: null, + nsfw: false, + name: 'collective-unconscious' , + last_pin_timestamp: '2023-04-06T09:51:57+00:00', + last_message_id: '1103832925784514580', + id: '112760669178241024', + default_thread_rate_limit_per_user: 0, + guild_id: '112760669178241024' + } + }, + room: { + general: { + "m.room.name/": {name: "collective-unconscious"}, + "m.room.topic/": {topic: "https://docs.google.com/document/d/blah/edit | I spread, pipe, and whip because it is my will. :headstone:"}, + "m.room.guest_access/": {guest_access: "can_join"}, + "m.room.history_visibility/": {history_visibility: "invited"}, + "m.space.parent/!jjWAGMeQdNrVZSSfvz:cadence.moe": { + via: ["cadence.moe"], // TODO: put the proper server here + canonical: true + }, + "m.room.join_rules/": { + join_rule: "restricted", + allow: [{ + type: "m.room.membership", + room_id: "!jjWAGMeQdNrVZSSfvz:cadence.moe" + }] + }, + "m.room.avatar/": { + discord_path: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024", + url: "mxc://cadence.moe/sZtPwbfOIsvfSoWCWPrGnzql" + } + } + }, + guild: { + general: { + owner_id: '112760500130975744', + premium_tier: 3, + stickers: [], + max_members: 500000, + splash: '86a34ed02524b972918bef810087f8e7', + explicit_content_filter: 0, + afk_channel_id: null, + nsfw_level: 0, + description: null, + preferred_locale: 'en-US', + system_channel_id: '112760669178241024', + mfa_level: 0, + /** @type {300} */ + afk_timeout: 300, + id: '112760669178241024', + icon: 'a_f83622e09ead74f0c5c527fe241f8f8c', + emojis: [], + premium_subscription_count: 14, + roles: [], + discovery_splash: null, + default_message_notifications: 1, + region: 'deprecated', + max_video_channel_users: 25, + verification_level: 0, + application_id: null, + premium_progress_bar_enabled: false, + banner: 'a_a666ae551605a2d8cda0afd591c0af3a', + features: [], + vanity_url_code: null, + hub_type: null, + public_updates_channel_id: null, + rules_channel_id: null, + name: 'Psychonauts 3', + max_stage_video_channel_users: 300, + system_channel_flags: 0|0 + } + } +} \ No newline at end of file diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..cb6fe5d --- /dev/null +++ b/test/test.js @@ -0,0 +1,15 @@ +// @ts-check + +const sqlite = require("better-sqlite3") +const HeatSync = require("heatsync") + +const config = require("../config") +const passthrough = require("../passthrough") +const db = new sqlite("db/ooye.db") + +// @ts-ignore +const sync = new HeatSync({persistent: false}) + +Object.assign(passthrough, { config, sync, db }) + +require("../d2m/actions/create-room") \ No newline at end of file From 3fbe7eed6e83732e2cc0c0d66ef133deb2f6f821 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 6 May 2023 01:25:15 +1200 Subject: [PATCH 03/99] Finish room diffing and syncing. All tests pass --- d2m/actions/create-room.js | 170 ++++--- d2m/actions/create-room.test.js | 83 ++++ d2m/actions/create-space.js | 2 +- d2m/actions/register-user.js | 2 +- index.js | 3 - matrix/file.js | 2 +- matrix/mreq.js | 5 +- package-lock.json | 857 ++++++++++++++++---------------- package.json | 10 +- stdin.js | 2 +- test/data.js | 4 +- test/test.js | 2 +- types.d.ts | 45 +- 13 files changed, 658 insertions(+), 529 deletions(-) create mode 100644 d2m/actions/create-room.test.js diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 5af26f5..214d026 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -1,8 +1,6 @@ // @ts-check const assert = require("assert").strict -const {test} = require("supertape") -const testData = require("../../test/data") const DiscordTypes = require("discord-api-types/v10") const passthrough = require("../../passthrough") @@ -12,37 +10,62 @@ const mreq = sync.require("../../matrix/mreq") /** @type {import("../../matrix/file")} */ const file = sync.require("../../matrix/file") +function kstateStripConditionals(kstate) { + for (const [k, content] of Object.entries(kstate)) { + if ("$if" in content) { + if (content.$if) delete content.$if + else delete kstate[k] + } + } + return kstate +} + function kstateToState(kstate) { - return Object.entries(kstate).map(([k, content]) => { - console.log(k) + const events = [] + for (const [k, content] of Object.entries(kstate)) { + // conditional for whether a key is even part of the kstate (doing this declaratively on json is hard, so represent it as a property instead.) + if ("$if" in content && !content.$if) continue + delete content.$if + const [type, state_key] = k.split("/") assert.ok(typeof type === "string") assert.ok(typeof state_key === "string") - return {type, state_key, content} - }) + events.push({type, state_key, content}) + } + return events } -test("kstate2state: general", t => { - t.deepEqual(kstateToState({ - "m.room.name/": {name: "test name"}, - "m.room.member/@cadence:cadence.moe": {membership: "join"} - }), [ - { - type: "m.room.name", - state_key: "", - content: { - name: "test name" - } - }, - { - type: "m.room.member", - state_key: "@cadence:cadence.moe", - content: { - membership: "join" - } - } - ]) -}) +/** + * @param {import("../../types").Event.BaseStateEvent[]} events + * @returns {any} + */ +function stateToKState(events) { + const kstate = {} + for (const event of events) { + kstate[event.type + "/" + event.state_key] = event.content + } + return kstate +} + +/** + * @param {string} roomID + */ +async function roomToKState(roomID) { + /** @type {import("../../types").Event.BaseStateEvent[]} */ + const root = await mreq.mreq("GET", `/client/v3/rooms/${roomID}/state`) + return stateToKState(root) +} + +/** + * @params {string} roomID + * @params {any} kstate + */ +function applyKStateDiffToRoom(roomID, kstate) { + const events = kstateToState(kstate) + return Promise.all(events.map(({type, state_key, content}) => + mreq.mreq("PUT", `/client/v3/rooms/${roomID}/state/${type}/${state_key}`, content) + )) +} function diffKState(actual, target) { const diff = {} @@ -60,39 +83,11 @@ function diffKState(actual, target) { // not present, needs to be added diff[key] = target[key] } + // keys that are missing in "actual" will not be deleted on "target" (no action) } return diff } -test("diffKState: detects edits", t => { - t.deepEqual( - diffKState({ - "m.room.name/": {name: "test name"}, - "same/": {a: 2} - }, { - "m.room.name/": {name: "edited name"}, - "same/": {a: 2} - }), - { - "m.room.name/": {name: "edited name"} - } - ) -}) - -test("diffKState: detects new properties", t => { - t.deepEqual( - diffKState({ - "m.room.name/": {name: "test name"}, - }, { - "m.room.name/": {name: "test name"}, - "new/": {a: 2} - }), - { - "new/": {a: 2} - } - ) -}) - /** * @param {import("discord-api-types/v10").APIGuildTextChannel} channel * @param {import("discord-api-types/v10").APIGuild} guild @@ -107,14 +102,14 @@ async function channelToKState(channel, guild) { avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) } - const kstate = { + const channelKState = { "m.room.name/": {name: channel.name}, - "m.room.topic/": {topic: channel.topic || undefined}, + "m.room.topic/": {$if: channel.topic, topic: channel.topic}, "m.room.avatar/": avatarEventContent, "m.room.guest_access/": {guest_access: "can_join"}, "m.room.history_visibility/": {history_visibility: "invited"}, - [`m.space.parent/${spaceID}`]: { // TODO: put the proper server here - via: ["cadence.moe"], + [`m.space.parent/${spaceID}`]: { + via: ["cadence.moe"], // TODO: put the proper server here canonical: true }, "m.room.join_rules/": { @@ -126,13 +121,9 @@ async function channelToKState(channel, guild) { } } - return {spaceID, kstate} + return {spaceID, channelKState} } -test("channel2room: general", async t => { - t.deepEqual(await channelToKState(testData.channel.general, testData.guild.general).then(x => x.kstate), {expected: true, ...testData.room.general}) -}) - /** * @param {import("discord-api-types/v10").APIGuildTextChannel} channel * @param guild @@ -140,7 +131,7 @@ test("channel2room: general", async t => { * @param {any} kstate */ async function createRoom(channel, guild, spaceID, kstate) { - /** @type {import("../../types").R_RoomCreated} */ + /** @type {import("../../types").R.RoomCreated} */ const root = await mreq.mreq("POST", "/client/v3/createRoom", { name: channel.name, topic: channel.topic || undefined, @@ -159,20 +150,46 @@ async function createRoom(channel, guild, spaceID, kstate) { } /** - * @param {import("discord-api-types/v10").APIGuildTextChannel} channel + * @param {import("discord-api-types/v10").APIGuildChannel} channel */ -async function syncRoom(channel) { +function channelToGuild(channel) { const guildID = channel.guild_id assert(guildID) const guild = discord.guilds.get(guildID) assert(guild) + return guild +} - const {spaceID, kstate} = await channelToKState(channel, guild) +/** + * @param {string} channelID + */ +async function syncRoom(channelID) { + /** @ts-ignore @type {import("discord-api-types/v10").APIGuildChannel} */ + const channel = discord.channels.get(channelID) + assert.ok(channel) + const guild = channelToGuild(channel) + + const {spaceID, channelKState} = await channelToKState(channel, guild) /** @type {string?} */ const existing = db.prepare("SELECT room_id from channel_room WHERE channel_id = ?").pluck().get(channel.id) if (!existing) { - createRoom(channel, guild, spaceID, kstate) + return createRoom(channel, guild, spaceID, channelKState) + } else { + // sync channel state to room + const roomKState = await roomToKState(existing) + const roomDiff = diffKState(roomKState, channelKState) + const roomApply = applyKStateDiffToRoom(existing, roomDiff) + + // sync room as space member + const spaceKState = await roomToKState(spaceID) + const spaceDiff = diffKState(spaceKState, { + [`m.space.child/${existing}`]: { + via: ["cadence.moe"] // TODO: use the proper server + } + }) + const spaceApply = applyKStateDiffToRoom(spaceID, spaceDiff) + return Promise.all([roomApply, spaceApply]) } } @@ -180,14 +197,15 @@ async function createAllForGuild(guildID) { const channelIDs = discord.guildChannelMap.get(guildID) assert.ok(channelIDs) for (const channelID of channelIDs) { - const channel = discord.channels.get(channelID) - assert.ok(channel) - const existing = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(channel.id) - if (channel.type === DiscordTypes.ChannelType.GuildText && !existing) { - await createRoom(channel) - } + await syncRoom(channelID).then(r => console.log(`synced ${channelID}:`, r)) } } module.exports.createRoom = createRoom +module.exports.syncRoom = syncRoom module.exports.createAllForGuild = createAllForGuild +module.exports.kstateToState = kstateToState +module.exports.stateToKState = stateToKState +module.exports.diffKState = diffKState +module.exports.channelToKState = channelToKState +module.exports.kstateStripConditionals = kstateStripConditionals diff --git a/d2m/actions/create-room.test.js b/d2m/actions/create-room.test.js new file mode 100644 index 0000000..5ce52e8 --- /dev/null +++ b/d2m/actions/create-room.test.js @@ -0,0 +1,83 @@ +const {kstateToState, stateToKState, diffKState, channelToKState, kstateStripConditionals} = require("./create-room") +const {test} = require("supertape") +const testData = require("../../test/data") + +test("kstate2state: general", t => { + t.deepEqual(kstateToState({ + "m.room.name/": {name: "test name"}, + "m.room.member/@cadence:cadence.moe": {membership: "join"} + }), [ + { + type: "m.room.name", + state_key: "", + content: { + name: "test name" + } + }, + { + type: "m.room.member", + state_key: "@cadence:cadence.moe", + content: { + membership: "join" + } + } + ]) +}) + +test("state2kstate: general", t => { + t.deepEqual(stateToKState([ + { + type: "m.room.name", + state_key: "", + content: { + name: "test name" + } + }, + { + type: "m.room.member", + state_key: "@cadence:cadence.moe", + content: { + membership: "join" + } + } + ]), { + "m.room.name/": {name: "test name"}, + "m.room.member/@cadence:cadence.moe": {membership: "join"} + }) +}) + +test("diffKState: detects edits", t => { + t.deepEqual( + diffKState({ + "m.room.name/": {name: "test name"}, + "same/": {a: 2} + }, { + "m.room.name/": {name: "edited name"}, + "same/": {a: 2} + }), + { + "m.room.name/": {name: "edited name"} + } + ) +}) + +test("diffKState: detects new properties", t => { + t.deepEqual( + diffKState({ + "m.room.name/": {name: "test name"}, + }, { + "m.room.name/": {name: "test name"}, + "new/": {a: 2} + }), + { + "new/": {a: 2} + } + ) +}) + +test("channel2room: general", async t => { + t.deepEqual( + kstateStripConditionals(await channelToKState(testData.channel.general, testData.guild.general).then(x => x.channelKState)), + testData.room.general + ) +}) diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index 6d3c327..b3d7e95 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -37,7 +37,7 @@ function createSpace(guild) { } } ] - }).then(/** @param {import("../../types").R_RoomCreated} root */ root => { + }).then(/** @param {import("../../types").R.RoomCreated} root */ root => { db.prepare("INSERT INTO guild_space (guild_id, space_id) VALUES (?, ?)").run(guild.id, root.room_id) return root }) diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index 14dda55..325a3d0 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -1,7 +1,7 @@ // @ts-check const reg = require("../../matrix/read-registration.js") -const fetch = require("node-fetch") +const fetch = require("node-fetch").default fetch("https://matrix.cadence.moe/_matrix/client/v3/register", { method: "POST", diff --git a/index.js b/index.js index d1c721c..807b49f 100644 --- a/index.js +++ b/index.js @@ -22,6 +22,3 @@ passthrough.discord = discord require("./stdin") })() - -// process.on("unhandledRejection", console.error) -// process.on("uncaughtException", console.error) diff --git a/matrix/file.js b/matrix/file.js index d242f52..137a096 100644 --- a/matrix/file.js +++ b/matrix/file.js @@ -36,7 +36,7 @@ async function uploadDiscordFileToMxc(path) { const body = res.body // Upload to Matrix - /** @type {import("../types").R_FileUploaded} */ + /** @type {import("../types").R.FileUploaded} */ const root = await mreq.mreq("POST", "/media/v3/upload", body, { headers: { "Content-Type": res.headers.get("content-type") diff --git a/matrix/mreq.js b/matrix/mreq.js index b3e12aa..1345f78 100644 --- a/matrix/mreq.js +++ b/matrix/mreq.js @@ -10,8 +10,9 @@ const reg = sync.require("./read-registration.js") const baseUrl = "https://matrix.cadence.moe/_matrix" -class MatrixServerError { +class MatrixServerError extends Error { constructor(data) { + super(data.error || data.errcode) this.data = data /** @type {string} */ this.errcode = data.errcode @@ -35,7 +36,7 @@ async function mreq(method, url, body, extra = {}) { } }, extra) - console.log(baseUrl + url, opts) + // console.log(baseUrl + url, opts) const res = await fetch(baseUrl + url, opts) const root = await res.json() diff --git a/package-lock.json b/package-lock.json index 654d022..b845abf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,12 +18,13 @@ "matrix-js-sdk": "^24.1.0", "mixin-deep": "^2.0.1", "node-fetch": "^2.6.7", - "snowtransfer": "^0.7.0", - "supertape": "^8.3.0" + "snowtransfer": "^0.7.0" }, "devDependencies": { "@types/node": "^18.16.0", - "tap-dot": "github:alandelaney-whs/tap-dot#32d909760fc177c83a6738fecf0c8c7eb3a7b2bf" + "@types/node-fetch": "^2.6.3", + "supertape": "^8.3.0", + "tap-dot": "github:cloudrac3r/tap-dot#223a4e67a6f7daf015506a12a7af74605f06c7f4" } }, "node_modules/@babel/runtime": { @@ -41,6 +42,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@cloudcmd/stub/-/stub-4.0.1.tgz", "integrity": "sha512-7x7tVxJZOdQowHv/VKwHLo9aoNNoVRc6PdKYqyKcDHX+xrF78jSXnqEWrOplnD/gF+tCnyFafu1Is+lFfWCILw==", + "dev": true, "dependencies": { "chalk": "^4.0.0", "jest-diff": "^27.0.6", @@ -54,6 +56,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "engines": { "node": ">=8" } @@ -62,6 +65,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, "engines": { "node": ">=10" }, @@ -73,6 +77,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", + "dev": true, "engines": { "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } @@ -81,6 +86,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "dev": true, "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^27.5.1", @@ -95,6 +101,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "dev": true, "engines": { "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } @@ -103,6 +110,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -115,12 +123,14 @@ "node_modules/@cloudcmd/stub/node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true }, "node_modules/@cloudcmd/stub/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -132,6 +142,7 @@ "version": "29.4.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "dev": true, "dependencies": { "@sinclair/typebox": "^0.25.16" }, @@ -151,6 +162,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@putout/cli-keypress/-/cli-keypress-1.0.0.tgz", "integrity": "sha512-w+lRVGZodRM4K214R4jvyOsmCUGA3OnaYDOJ2rpXj6a+O6n91zLlkb7JYsw6I0QCNmXjpNLJSoLgzGWTue6YIg==", + "dev": true, "dependencies": { "ci-info": "^3.1.1", "fullstore": "^3.0.0" @@ -163,6 +175,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@putout/cli-validate-args/-/cli-validate-args-1.1.1.tgz", "integrity": "sha512-AczBS98YyvsDVxvvYjHGyIygFu3i/EJ0xsruU6MlytTuUiCFQIE/QQPDy1bcN5J2Y75BzSYncaYnVrEGcBjeeQ==", + "dev": true, "dependencies": { "fastest-levenshtein": "^1.0.12", "just-kebab-case": "^1.1.0" @@ -174,12 +187,14 @@ "node_modules/@sinclair/typebox": { "version": "0.25.24", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", - "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==" + "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "dev": true }, "node_modules/@supertape/engine-loader": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@supertape/engine-loader/-/engine-loader-1.1.3.tgz", "integrity": "sha512-5ilgEng0WBvMQjNJWQ/bnAA6HKgbLKxTya2C0RxFH0LYSN5faBVtgxjLDvTQ+5L+ZxjK/7ooQDDaRS1Mo0ga5Q==", + "dev": true, "dependencies": { "try-catch": "^3.0.0" }, @@ -191,6 +206,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/@supertape/formatter-fail/-/formatter-fail-3.0.2.tgz", "integrity": "sha512-mSBnNprfLFmGvZkP+ODGroPLFCIN5BWE/06XaD5ghiTVWqek7eH8IDqvKyEduvuQu1O5tvQiaTwQsyxvikF+2w==", + "dev": true, "dependencies": { "@supertape/formatter-tap": "^3.0.3", "fullstore": "^3.0.0" @@ -203,6 +219,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/@supertape/formatter-json-lines/-/formatter-json-lines-2.0.1.tgz", "integrity": "sha512-9LWOCu4yOF9orf4QJseS8lP3hXkYn24qn57VqYt/3r2aRJv4vWTPfaL1ot5JRHCZs0qXrV1sqPmN6E05rRLDYA==", + "dev": true, "dependencies": { "fullstore": "^3.0.0" }, @@ -214,6 +231,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@supertape/formatter-progress-bar/-/formatter-progress-bar-3.0.0.tgz", "integrity": "sha512-rVFAQ21eApq3TQV8taFLNcCxcGZvvOPxQC63swdmHFCp+07Dt3tvC/aFxF35NLobc3rySasGSEuPucpyoPrjfg==", + "dev": true, "dependencies": { "chalk": "^4.1.0", "ci-info": "^3.1.1", @@ -229,6 +247,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/@supertape/formatter-short/-/formatter-short-2.0.1.tgz", "integrity": "sha512-zxFrZfCccFV+bf6A7MCEqT/Xsf0Elc3qa0P3jShfdEfrpblEcpSo0T/Wd9jFwc7uHA3ABgxgcHy7LNIpyrFTCg==", + "dev": true, "engines": { "node": ">=16" } @@ -237,6 +256,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/@supertape/formatter-tap/-/formatter-tap-3.0.3.tgz", "integrity": "sha512-U5OuMotfYhGo9cZ8IgdAXRTH5Yy8yfLDZzYo1upTPTwlJJquKwtvuz7ptiB7BN3OFr5YakkDYlFxOYPcLo7urg==", + "dev": true, "engines": { "node": ">=16" } @@ -245,6 +265,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@supertape/operator-stub/-/operator-stub-3.0.0.tgz", "integrity": "sha512-LZ6E4nSMDMbLOhvEZyeXo8wS5EBiAAffWrohb7yaVHDVTHr+xkczzPxinkvcOBhNuAtC0kVARdMbHg+HULmozA==", + "dev": true, "dependencies": { "@cloudcmd/stub": "^4.0.0" }, @@ -263,6 +284,30 @@ "integrity": "sha512-BsAaKhB+7X+H4GnSjGhJG9Qi8Tw+inU9nJDwmD5CgOmBLEI6ArdhikpLX7DjbjDRDTbqZzU2LSQNZg8WGPiSZQ==", "dev": true }, + "node_modules/@types/node-fetch": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.3.tgz", + "integrity": "sha512-ETTL1mOEdq/sxUtgtOhKjyB2Irra4cjxksvcMUR5Zr4n+PxVhsCD9WS46oPbHL3et9Zde7CNRr+WUNlcHvsX+w==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", @@ -288,6 +333,18 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -309,6 +366,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, "engines": { "node": ">=12" }, @@ -320,6 +378,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -344,6 +403,7 @@ "version": "1.0.55", "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", + "dev": true, "dependencies": { "printable-characters": "^1.0.42" } @@ -357,6 +417,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -372,7 +433,8 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/base-x": { "version": "4.0.0", @@ -482,6 +544,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -546,6 +609,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -566,6 +630,7 @@ "version": "3.8.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, "funding": [ { "type": "github", @@ -580,6 +645,7 @@ "version": "3.12.0", "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "dev": true, "dependencies": { "string-width": "^4.2.3" }, @@ -602,6 +668,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -612,7 +679,14 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true }, "node_modules/combined-stream": { "version": "1.0.8", @@ -657,12 +731,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, "node_modules/csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", @@ -671,7 +739,8 @@ "node_modules/data-uri-to-buffer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", - "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==" + "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", + "dev": true }, "node_modules/decompress-response": { "version": "6.0.0", @@ -691,6 +760,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz", "integrity": "sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "es-get-iterator": "^1.1.2", @@ -726,6 +796,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, "dependencies": { "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" @@ -774,6 +845,7 @@ "version": "29.4.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -799,7 +871,8 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "node_modules/encodeurl": { "version": "1.0.2", @@ -821,6 +894,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -841,15 +915,6 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -858,6 +923,15 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -969,6 +1043,7 @@ "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, "engines": { "node": ">= 4.9.1" } @@ -1012,6 +1087,7 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, "dependencies": { "is-callable": "^1.1.3" } @@ -1053,12 +1129,14 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "node_modules/fullstore": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/fullstore/-/fullstore-3.0.0.tgz", "integrity": "sha512-EEIdG+HWpyygWRwSLIZy+x4u0xtghjHNfhQb0mI5825Mmjq6oFESFUY0hoZigEgd3KH8GX+ZOCK9wgmOiS7VBQ==", + "dev": true, "engines": { "node": ">=4" } @@ -1072,6 +1150,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1093,6 +1172,7 @@ "version": "2.0.12", "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", + "dev": true, "dependencies": { "data-uri-to-buffer": "^2.0.0", "source-map": "^0.6.1" @@ -1107,6 +1187,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -1125,6 +1206,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -1143,31 +1225,11 @@ "node": ">= 0.4.0" } }, - "node_modules/has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", - "dev": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-ansi/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1176,6 +1238,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -1184,6 +1247,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, "dependencies": { "get-intrinsic": "^1.1.1" }, @@ -1206,6 +1270,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, "dependencies": { "has-symbols": "^1.0.2" }, @@ -1273,6 +1338,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -1292,6 +1358,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dev": true, "dependencies": { "get-intrinsic": "^1.2.0", "has": "^1.0.3", @@ -1313,6 +1380,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -1328,6 +1396,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.0", @@ -1341,6 +1410,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, "dependencies": { "has-bigints": "^1.0.1" }, @@ -1352,6 +1422,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -1367,6 +1438,7 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -1378,6 +1450,7 @@ "version": "2.12.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "dev": true, "dependencies": { "has": "^1.0.3" }, @@ -1389,6 +1462,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -1403,6 +1477,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "engines": { "node": ">=8" } @@ -1411,6 +1486,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1419,6 +1495,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -1433,6 +1510,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -1448,6 +1526,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1456,6 +1535,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2" }, @@ -1467,6 +1547,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -1481,6 +1562,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, "dependencies": { "has-symbols": "^1.0.2" }, @@ -1495,6 +1577,7 @@ "version": "1.1.10", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", @@ -1513,6 +1596,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1521,6 +1605,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" @@ -1532,12 +1617,14 @@ "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true }, "node_modules/jest-diff": { "version": "29.5.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "dev": true, "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.4.3", @@ -1552,6 +1639,7 @@ "version": "29.4.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -1570,7 +1658,8 @@ "node_modules/just-kebab-case": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/just-kebab-case/-/just-kebab-case-1.1.0.tgz", - "integrity": "sha512-QkuwuBMQ9BQHMUEkAtIA4INLrkmnnveqlFB1oFi09gbU0wBdZo6tTnyxNWMR84zHxBuwK7GLAwqN8nrvVxOLTA==" + "integrity": "sha512-QkuwuBMQ9BQHMUEkAtIA4INLrkmnnveqlFB1oFi09gbU0wBdZo6tTnyxNWMR84zHxBuwK7GLAwqN8nrvVxOLTA==", + "dev": true }, "node_modules/loglevel": { "version": "1.8.1", @@ -1719,6 +1808,7 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -1841,6 +1931,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" @@ -1856,6 +1947,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, "engines": { "node": ">= 0.4" } @@ -1864,6 +1956,7 @@ "version": "4.1.4", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -1927,7 +2020,8 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "node_modules/path-to-regexp": { "version": "0.1.7", @@ -1963,6 +2057,7 @@ "version": "29.5.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, "dependencies": { "@jest/schemas": "^29.4.3", "ansi-styles": "^5.0.0", @@ -1976,6 +2071,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, "engines": { "node": ">=10" }, @@ -1986,14 +2082,18 @@ "node_modules/printable-characters": { "version": "1.0.42", "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", - "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==" - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", "dev": true }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2074,7 +2174,8 @@ "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true }, "node_modules/readable-stream": { "version": "3.6.2", @@ -2098,6 +2199,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -2114,6 +2216,7 @@ "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, "dependencies": { "is-core-module": "^2.11.0", "path-parse": "^1.0.7", @@ -2321,6 +2424,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -2341,6 +2445,7 @@ "version": "2.1.8", "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", + "dev": true, "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" @@ -2358,6 +2463,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, "dependencies": { "internal-slot": "^1.0.4" }, @@ -2377,6 +2483,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2390,6 +2497,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "engines": { "node": ">=8" } @@ -2398,6 +2506,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2409,6 +2518,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -2431,6 +2541,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/supertape/-/supertape-8.3.0.tgz", "integrity": "sha512-dcMylmkr1Mctr5UBCrlvZynuBRuLvlkWJLGXdL/PcI41BERnObO+kV0PeZhH5n6lwVnvK2xfvZyN32WIAPf/tw==", + "dev": true, "dependencies": { "@cloudcmd/stub": "^4.0.0", "@putout/cli-keypress": "^1.0.0", @@ -2467,6 +2578,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -2478,6 +2590,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -2487,85 +2600,28 @@ }, "node_modules/tap-dot": { "version": "2.0.0", - "resolved": "git+ssh://git@github.com/alandelaney-whs/tap-dot.git#32d909760fc177c83a6738fecf0c8c7eb3a7b2bf", - "integrity": "sha512-r3EdqKvdl8qy4OxB0oy3YuibtCGy0hbNKUITLfMlblCVnTgWZEDmlhxsV0Dfn5B3xwcZQTokjvaygKDUGIe+WA==", + "resolved": "git+ssh://git@github.com/cloudrac3r/tap-dot.git#223a4e67a6f7daf015506a12a7af74605f06c7f4", + "integrity": "sha512-tHte0Cqt0Unnfz3zbhtk8ByNoh9KA7xXKWIC6/UUNJcyueR9DBlTx1YCH6TH7rIKaz8aBNqCV9HCCpAWilOOAQ==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^1.1.1", - "tap-out": "github:alandelaney-whs/tap-out", - "through2": "^2.0.0" + "colorette": "^1.0.5", + "tap-out": "github:cloudrac3r/tap-out#1b4ec6084aedb9f44ccaa0c7185ff9bfd83da771" }, "bin": { "tap-dot": "bin/dot" } }, - "node_modules/tap-dot/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tap-dot/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tap-dot/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "dev": true, - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tap-dot/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tap-dot/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/tap-out": { - "name": "tap-in", "version": "3.2.1", - "resolved": "git+ssh://git@github.com/alandelaney-whs/tap-out.git#c93556c36b8c7013d38ff12f9ce156eb06f734cb", + "resolved": "git+ssh://git@github.com/cloudrac3r/tap-out.git#1b4ec6084aedb9f44ccaa0c7185ff9bfd83da771", + "integrity": "sha512-55eUSaX5AeEOqJMRlj9XSqUlLV/yYPOPeC3kOFqjmorq6/jlH5kIeqpgLNW5PlPEAuggzYREYYXqrN8E37ZPfQ==", "dev": true, "license": "MIT", "dependencies": { "re-emitter": "1.1.4", - "readable-stream": "2.3.7", - "split": "1.0.1", - "trim": "1.0.1" + "readable-stream": "^4.3.0", + "split": "^1.0.1" }, "bin": { "tap-in": "bin/tap-in.js" @@ -2574,40 +2630,43 @@ "node": ">=8.0.0" } }, - "node_modules/tap-out/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, - "node_modules/tap-out/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "node_modules/tap-out/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } }, - "node_modules/tap-out/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/tap-out/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "node_modules/tap-out/node_modules/readable-stream": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.3.0.tgz", + "integrity": "sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==", "dev": true, "dependencies": { - "safe-buffer": "~5.1.0" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/tar-fs": { @@ -2642,52 +2701,6 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, - "node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/through2/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, - "node_modules/through2/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/through2/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/through2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -2701,17 +2714,11 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, - "node_modules/trim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim/-/trim-1.0.1.tgz", - "integrity": "sha512-3JVP2YVqITUisXblCDq/Bi4P9457G/sdEamInkyvCsjbTcXLXIiG7XCb4kGMFWh6JGXesS3TKxOPtrncN/xe8w==", - "deprecated": "Use String.prototype.trim() instead", - "dev": true - }, "node_modules/try-catch": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/try-catch/-/try-catch-3.0.1.tgz", "integrity": "sha512-91yfXw1rr/P6oLpHSyHDOHm0vloVvUoo9FVdw8YwY05QjJQG9OT0LUxe2VRAzmHG+0CUOmI3nhxDUMLxDN/NEQ==", + "dev": true, "engines": { "node": ">=6" } @@ -2720,6 +2727,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-3.0.1.tgz", "integrity": "sha512-hOY83V84Hx/1sCzDSaJA+Xz2IIQOHRvjxzt+F0OjbQGPZ6yLPLArMA0gw/484MlfUkQbCpKYMLX3VDCAjWKfzQ==", + "dev": true, "engines": { "node": ">=6" } @@ -2799,6 +2807,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, "dependencies": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", @@ -2814,6 +2823,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, "dependencies": { "is-map": "^2.0.1", "is-set": "^2.0.1", @@ -2828,6 +2838,7 @@ "version": "1.1.9", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", @@ -2851,16 +2862,8 @@ "node_modules/wraptile": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/wraptile/-/wraptile-3.0.0.tgz", - "integrity": "sha512-23LJhkIw940uTcDFyJZmNyO0z8lEINOTGCr4vR5YCG3urkdXwduRIhivBm9wKaVynLHYvxoHHYbKsDiafCLp6w==" - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "engines": { - "node": ">=0.4" - } + "integrity": "sha512-23LJhkIw940uTcDFyJZmNyO0z8lEINOTGCr4vR5YCG3urkdXwduRIhivBm9wKaVynLHYvxoHHYbKsDiafCLp6w==", + "dev": true }, "node_modules/yallist": { "version": "4.0.0", @@ -2871,6 +2874,7 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, "engines": { "node": ">=12" } @@ -2889,6 +2893,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@cloudcmd/stub/-/stub-4.0.1.tgz", "integrity": "sha512-7x7tVxJZOdQowHv/VKwHLo9aoNNoVRc6PdKYqyKcDHX+xrF78jSXnqEWrOplnD/gF+tCnyFafu1Is+lFfWCILw==", + "dev": true, "requires": { "chalk": "^4.0.0", "jest-diff": "^27.0.6", @@ -2898,22 +2903,26 @@ "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true }, "ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true }, "diff-sequences": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==" + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", + "dev": true }, "jest-diff": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", + "dev": true, "requires": { "chalk": "^4.0.0", "diff-sequences": "^27.5.1", @@ -2924,12 +2933,14 @@ "jest-get-type": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==" + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", + "dev": true }, "pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, "requires": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -2939,12 +2950,14 @@ "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "requires": { "ansi-regex": "^5.0.1" } @@ -2955,6 +2968,7 @@ "version": "29.4.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "dev": true, "requires": { "@sinclair/typebox": "^0.25.16" } @@ -2968,6 +2982,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@putout/cli-keypress/-/cli-keypress-1.0.0.tgz", "integrity": "sha512-w+lRVGZodRM4K214R4jvyOsmCUGA3OnaYDOJ2rpXj6a+O6n91zLlkb7JYsw6I0QCNmXjpNLJSoLgzGWTue6YIg==", + "dev": true, "requires": { "ci-info": "^3.1.1", "fullstore": "^3.0.0" @@ -2977,6 +2992,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@putout/cli-validate-args/-/cli-validate-args-1.1.1.tgz", "integrity": "sha512-AczBS98YyvsDVxvvYjHGyIygFu3i/EJ0xsruU6MlytTuUiCFQIE/QQPDy1bcN5J2Y75BzSYncaYnVrEGcBjeeQ==", + "dev": true, "requires": { "fastest-levenshtein": "^1.0.12", "just-kebab-case": "^1.1.0" @@ -2985,12 +3001,14 @@ "@sinclair/typebox": { "version": "0.25.24", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", - "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==" + "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "dev": true }, "@supertape/engine-loader": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@supertape/engine-loader/-/engine-loader-1.1.3.tgz", "integrity": "sha512-5ilgEng0WBvMQjNJWQ/bnAA6HKgbLKxTya2C0RxFH0LYSN5faBVtgxjLDvTQ+5L+ZxjK/7ooQDDaRS1Mo0ga5Q==", + "dev": true, "requires": { "try-catch": "^3.0.0" } @@ -2999,6 +3017,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/@supertape/formatter-fail/-/formatter-fail-3.0.2.tgz", "integrity": "sha512-mSBnNprfLFmGvZkP+ODGroPLFCIN5BWE/06XaD5ghiTVWqek7eH8IDqvKyEduvuQu1O5tvQiaTwQsyxvikF+2w==", + "dev": true, "requires": { "@supertape/formatter-tap": "^3.0.3", "fullstore": "^3.0.0" @@ -3008,6 +3027,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/@supertape/formatter-json-lines/-/formatter-json-lines-2.0.1.tgz", "integrity": "sha512-9LWOCu4yOF9orf4QJseS8lP3hXkYn24qn57VqYt/3r2aRJv4vWTPfaL1ot5JRHCZs0qXrV1sqPmN6E05rRLDYA==", + "dev": true, "requires": { "fullstore": "^3.0.0" } @@ -3016,6 +3036,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@supertape/formatter-progress-bar/-/formatter-progress-bar-3.0.0.tgz", "integrity": "sha512-rVFAQ21eApq3TQV8taFLNcCxcGZvvOPxQC63swdmHFCp+07Dt3tvC/aFxF35NLobc3rySasGSEuPucpyoPrjfg==", + "dev": true, "requires": { "chalk": "^4.1.0", "ci-info": "^3.1.1", @@ -3027,17 +3048,20 @@ "@supertape/formatter-short": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@supertape/formatter-short/-/formatter-short-2.0.1.tgz", - "integrity": "sha512-zxFrZfCccFV+bf6A7MCEqT/Xsf0Elc3qa0P3jShfdEfrpblEcpSo0T/Wd9jFwc7uHA3ABgxgcHy7LNIpyrFTCg==" + "integrity": "sha512-zxFrZfCccFV+bf6A7MCEqT/Xsf0Elc3qa0P3jShfdEfrpblEcpSo0T/Wd9jFwc7uHA3ABgxgcHy7LNIpyrFTCg==", + "dev": true }, "@supertape/formatter-tap": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@supertape/formatter-tap/-/formatter-tap-3.0.3.tgz", - "integrity": "sha512-U5OuMotfYhGo9cZ8IgdAXRTH5Yy8yfLDZzYo1upTPTwlJJquKwtvuz7ptiB7BN3OFr5YakkDYlFxOYPcLo7urg==" + "integrity": "sha512-U5OuMotfYhGo9cZ8IgdAXRTH5Yy8yfLDZzYo1upTPTwlJJquKwtvuz7ptiB7BN3OFr5YakkDYlFxOYPcLo7urg==", + "dev": true }, "@supertape/operator-stub": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@supertape/operator-stub/-/operator-stub-3.0.0.tgz", "integrity": "sha512-LZ6E4nSMDMbLOhvEZyeXo8wS5EBiAAffWrohb7yaVHDVTHr+xkczzPxinkvcOBhNuAtC0kVARdMbHg+HULmozA==", + "dev": true, "requires": { "@cloudcmd/stub": "^4.0.0" } @@ -3053,6 +3077,29 @@ "integrity": "sha512-BsAaKhB+7X+H4GnSjGhJG9Qi8Tw+inU9nJDwmD5CgOmBLEI6ArdhikpLX7DjbjDRDTbqZzU2LSQNZg8WGPiSZQ==", "dev": true }, + "@types/node-fetch": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.3.tgz", + "integrity": "sha512-ETTL1mOEdq/sxUtgtOhKjyB2Irra4cjxksvcMUR5Zr4n+PxVhsCD9WS46oPbHL3et9Zde7CNRr+WUNlcHvsX+w==", + "dev": true, + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + }, + "dependencies": { + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, "@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", @@ -3078,6 +3125,15 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "requires": { + "event-target-shim": "^5.0.0" + } + }, "accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -3095,12 +3151,14 @@ "ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -3119,6 +3177,7 @@ "version": "1.0.55", "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", + "dev": true, "requires": { "printable-characters": "^1.0.42" } @@ -3131,7 +3190,8 @@ "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true }, "backtracker": { "version": "3.3.1", @@ -3141,7 +3201,8 @@ "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "base-x": { "version": "4.0.0", @@ -3233,6 +3294,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "requires": { "balanced-match": "^1.0.0" } @@ -3277,6 +3339,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3290,12 +3353,14 @@ "ci-info": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==" + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true }, "cli-progress": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "dev": true, "requires": { "string-width": "^4.2.3" } @@ -3312,6 +3377,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -3319,7 +3385,14 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true }, "combined-stream": { "version": "1.0.8", @@ -3352,12 +3425,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, - "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, "csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", @@ -3366,7 +3433,8 @@ "data-uri-to-buffer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", - "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==" + "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", + "dev": true }, "decompress-response": { "version": "6.0.0", @@ -3380,6 +3448,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz", "integrity": "sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw==", + "dev": true, "requires": { "call-bind": "^1.0.2", "es-get-iterator": "^1.1.2", @@ -3409,6 +3478,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, "requires": { "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" @@ -3437,7 +3507,8 @@ "diff-sequences": { "version": "29.4.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", - "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==" + "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "dev": true }, "discord-api-types": { "version": "0.37.39", @@ -3459,7 +3530,8 @@ "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "encodeurl": { "version": "1.0.2", @@ -3478,6 +3550,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, "requires": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -3495,17 +3568,17 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true + }, "events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -3602,7 +3675,8 @@ "fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==" + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true }, "file-uri-to-path": { "version": "1.0.0", @@ -3642,6 +3716,7 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, "requires": { "is-callable": "^1.1.3" } @@ -3674,12 +3749,14 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "fullstore": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/fullstore/-/fullstore-3.0.0.tgz", - "integrity": "sha512-EEIdG+HWpyygWRwSLIZy+x4u0xtghjHNfhQb0mI5825Mmjq6oFESFUY0hoZigEgd3KH8GX+ZOCK9wgmOiS7VBQ==" + "integrity": "sha512-EEIdG+HWpyygWRwSLIZy+x4u0xtghjHNfhQb0mI5825Mmjq6oFESFUY0hoZigEgd3KH8GX+ZOCK9wgmOiS7VBQ==", + "dev": true }, "function-bind": { "version": "1.1.1", @@ -3689,7 +3766,8 @@ "functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true }, "get-intrinsic": { "version": "1.2.0", @@ -3705,6 +3783,7 @@ "version": "2.0.12", "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", + "dev": true, "requires": { "data-uri-to-buffer": "^2.0.0", "source-map": "^0.6.1" @@ -3719,6 +3798,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3731,6 +3811,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, "requires": { "get-intrinsic": "^1.1.3" } @@ -3743,37 +3824,23 @@ "function-bind": "^1.1.1" } }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true - } - } - }, "has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true }, "has-property-descriptors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, "requires": { "get-intrinsic": "^1.1.1" } @@ -3787,6 +3854,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, "requires": { "has-symbols": "^1.0.2" } @@ -3828,6 +3896,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -3847,6 +3916,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dev": true, "requires": { "get-intrinsic": "^1.2.0", "has": "^1.0.3", @@ -3862,6 +3932,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -3871,6 +3942,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, "requires": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.0", @@ -3881,6 +3953,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, "requires": { "has-bigints": "^1.0.1" } @@ -3889,6 +3962,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -3897,12 +3971,14 @@ "is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true }, "is-core-module": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "dev": true, "requires": { "has": "^1.0.3" } @@ -3911,6 +3987,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, "requires": { "has-tostringtag": "^1.0.0" } @@ -3918,17 +3995,20 @@ "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true }, "is-map": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==" + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true }, "is-number-object": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, "requires": { "has-tostringtag": "^1.0.0" } @@ -3937,6 +4017,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -3945,12 +4026,14 @@ "is-set": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==" + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true }, "is-shared-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, "requires": { "call-bind": "^1.0.2" } @@ -3959,6 +4042,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, "requires": { "has-tostringtag": "^1.0.0" } @@ -3967,6 +4051,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, "requires": { "has-symbols": "^1.0.2" } @@ -3975,6 +4060,7 @@ "version": "1.1.10", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "dev": true, "requires": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", @@ -3986,12 +4072,14 @@ "is-weakmap": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==" + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true }, "is-weakset": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, "requires": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" @@ -4000,12 +4088,14 @@ "isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true }, "jest-diff": { "version": "29.5.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "dev": true, "requires": { "chalk": "^4.0.0", "diff-sequences": "^29.4.3", @@ -4016,7 +4106,8 @@ "jest-get-type": { "version": "29.4.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", - "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==" + "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "dev": true }, "js-yaml": { "version": "4.1.0", @@ -4029,7 +4120,8 @@ "just-kebab-case": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/just-kebab-case/-/just-kebab-case-1.1.0.tgz", - "integrity": "sha512-QkuwuBMQ9BQHMUEkAtIA4INLrkmnnveqlFB1oFi09gbU0wBdZo6tTnyxNWMR84zHxBuwK7GLAwqN8nrvVxOLTA==" + "integrity": "sha512-QkuwuBMQ9BQHMUEkAtIA4INLrkmnnveqlFB1oFi09gbU0wBdZo6tTnyxNWMR84zHxBuwK7GLAwqN8nrvVxOLTA==", + "dev": true }, "loglevel": { "version": "1.8.1", @@ -4137,6 +4229,7 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, "requires": { "brace-expansion": "^2.0.1" } @@ -4226,6 +4319,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" @@ -4234,12 +4328,14 @@ "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true }, "object.assign": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -4285,7 +4381,8 @@ "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "path-to-regexp": { "version": "0.1.7", @@ -4315,6 +4412,7 @@ "version": "29.5.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, "requires": { "@jest/schemas": "^29.4.3", "ansi-styles": "^5.0.0", @@ -4324,19 +4422,21 @@ "ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true } } }, "printable-characters": { "version": "1.0.42", "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", - "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==" + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", + "dev": true }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "dev": true }, "proxy-addr": { @@ -4401,7 +4501,8 @@ "react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true }, "readable-stream": { "version": "3.6.2", @@ -4422,6 +4523,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -4432,6 +4534,7 @@ "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, "requires": { "is-core-module": "^2.11.0", "path-parse": "^1.0.7", @@ -4570,7 +4673,8 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true }, "split": { "version": "1.0.1", @@ -4585,6 +4689,7 @@ "version": "2.1.8", "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", + "dev": true, "requires": { "as-table": "^1.0.36", "get-source": "^2.0.12" @@ -4599,6 +4704,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, "requires": { "internal-slot": "^1.0.4" } @@ -4615,6 +4721,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -4624,12 +4731,14 @@ "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "requires": { "ansi-regex": "^5.0.1" } @@ -4640,6 +4749,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "dev": true, "requires": { "ansi-regex": "^6.0.1" } @@ -4653,6 +4763,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/supertape/-/supertape-8.3.0.tgz", "integrity": "sha512-dcMylmkr1Mctr5UBCrlvZynuBRuLvlkWJLGXdL/PcI41BERnObO+kV0PeZhH5n6lwVnvK2xfvZyN32WIAPf/tw==", + "dev": true, "requires": { "@cloudcmd/stub": "^4.0.0", "@putout/cli-keypress": "^1.0.0", @@ -4682,6 +4793,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -4689,106 +4801,50 @@ "supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true }, "tap-dot": { - "version": "git+ssh://git@github.com/alandelaney-whs/tap-dot.git#32d909760fc177c83a6738fecf0c8c7eb3a7b2bf", - "integrity": "sha512-r3EdqKvdl8qy4OxB0oy3YuibtCGy0hbNKUITLfMlblCVnTgWZEDmlhxsV0Dfn5B3xwcZQTokjvaygKDUGIe+WA==", + "version": "git+ssh://git@github.com/cloudrac3r/tap-dot.git#223a4e67a6f7daf015506a12a7af74605f06c7f4", + "integrity": "sha512-tHte0Cqt0Unnfz3zbhtk8ByNoh9KA7xXKWIC6/UUNJcyueR9DBlTx1YCH6TH7rIKaz8aBNqCV9HCCpAWilOOAQ==", "dev": true, - "from": "tap-dot@github:alandelaney-whs/tap-dot#32d909760fc177c83a6738fecf0c8c7eb3a7b2bf", + "from": "tap-dot@github:cloudrac3r/tap-dot#223a4e67a6f7daf015506a12a7af74605f06c7f4", "requires": { - "chalk": "^1.1.1", - "tap-out": "github:alandelaney-whs/tap-out", - "through2": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "dev": true - } + "colorette": "^1.0.5", + "tap-out": "github:cloudrac3r/tap-out#1b4ec6084aedb9f44ccaa0c7185ff9bfd83da771" } }, "tap-out": { - "version": "git+ssh://git@github.com/alandelaney-whs/tap-out.git#c93556c36b8c7013d38ff12f9ce156eb06f734cb", + "version": "git+ssh://git@github.com/cloudrac3r/tap-out.git#1b4ec6084aedb9f44ccaa0c7185ff9bfd83da771", + "integrity": "sha512-55eUSaX5AeEOqJMRlj9XSqUlLV/yYPOPeC3kOFqjmorq6/jlH5kIeqpgLNW5PlPEAuggzYREYYXqrN8E37ZPfQ==", "dev": true, - "from": "tap-out@github:alandelaney-whs/tap-out", + "from": "tap-out@github:cloudrac3r/tap-out#1b4ec6084aedb9f44ccaa0c7185ff9bfd83da771", "requires": { "re-emitter": "1.1.4", - "readable-stream": "2.3.7", - "split": "1.0.1", - "trim": "1.0.1" + "readable-stream": "^4.3.0", + "split": "^1.0.1" }, "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "dev": true, "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "readable-stream": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.3.0.tgz", + "integrity": "sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==", "dev": true, "requires": { - "safe-buffer": "~5.1.0" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10" } } } @@ -4822,54 +4878,6 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, - "readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, "toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -4880,21 +4888,17 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, - "trim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim/-/trim-1.0.1.tgz", - "integrity": "sha512-3JVP2YVqITUisXblCDq/Bi4P9457G/sdEamInkyvCsjbTcXLXIiG7XCb4kGMFWh6JGXesS3TKxOPtrncN/xe8w==", - "dev": true - }, "try-catch": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/try-catch/-/try-catch-3.0.1.tgz", - "integrity": "sha512-91yfXw1rr/P6oLpHSyHDOHm0vloVvUoo9FVdw8YwY05QjJQG9OT0LUxe2VRAzmHG+0CUOmI3nhxDUMLxDN/NEQ==" + "integrity": "sha512-91yfXw1rr/P6oLpHSyHDOHm0vloVvUoo9FVdw8YwY05QjJQG9OT0LUxe2VRAzmHG+0CUOmI3nhxDUMLxDN/NEQ==", + "dev": true }, "try-to-catch": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-3.0.1.tgz", - "integrity": "sha512-hOY83V84Hx/1sCzDSaJA+Xz2IIQOHRvjxzt+F0OjbQGPZ6yLPLArMA0gw/484MlfUkQbCpKYMLX3VDCAjWKfzQ==" + "integrity": "sha512-hOY83V84Hx/1sCzDSaJA+Xz2IIQOHRvjxzt+F0OjbQGPZ6yLPLArMA0gw/484MlfUkQbCpKYMLX3VDCAjWKfzQ==", + "dev": true }, "tunnel-agent": { "version": "0.6.0", @@ -4956,6 +4960,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, "requires": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", @@ -4968,6 +4973,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, "requires": { "is-map": "^2.0.1", "is-set": "^2.0.1", @@ -4979,6 +4985,7 @@ "version": "1.1.9", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "dev": true, "requires": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", @@ -4996,12 +5003,7 @@ "wraptile": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/wraptile/-/wraptile-3.0.0.tgz", - "integrity": "sha512-23LJhkIw940uTcDFyJZmNyO0z8lEINOTGCr4vR5YCG3urkdXwduRIhivBm9wKaVynLHYvxoHHYbKsDiafCLp6w==" - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "integrity": "sha512-23LJhkIw940uTcDFyJZmNyO0z8lEINOTGCr4vR5YCG3urkdXwduRIhivBm9wKaVynLHYvxoHHYbKsDiafCLp6w==", "dev": true }, "yallist": { @@ -5012,7 +5014,8 @@ "yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true } } } diff --git a/package.json b/package.json index f8917e7..e3ca70f 100644 --- a/package.json +++ b/package.json @@ -24,13 +24,15 @@ "matrix-js-sdk": "^24.1.0", "mixin-deep": "^2.0.1", "node-fetch": "^2.6.7", - "snowtransfer": "^0.7.0", - "supertape": "^8.3.0" + "snowtransfer": "^0.7.0" }, "devDependencies": { - "@types/node": "^18.16.0" + "@types/node": "^18.16.0", + "@types/node-fetch": "^2.6.3", + "supertape": "^8.3.0", + "tap-dot": "github:cloudrac3r/tap-dot#223a4e67a6f7daf015506a12a7af74605f06c7f4" }, "scripts": { - "test": "supertape --format short test/test.js" + "test": "FORCE_COLOR=true supertape --format tap test/test.js | tap-dot" } } diff --git a/stdin.js b/stdin.js index fb95809..a57c044 100644 --- a/stdin.js +++ b/stdin.js @@ -43,7 +43,7 @@ async function customEval(input, _context, _filename, callback) { const output = util.inspect(result, false, depth, true) return callback(null, output) } catch (e) { - return callback(null, util.inspect(e, true, 100, true)) + return callback(null, util.inspect(e, false, 100, true)) } } diff --git a/test/data.js b/test/data.js index 42af177..e6a49c8 100644 --- a/test/data.js +++ b/test/data.js @@ -39,7 +39,7 @@ module.exports = { }, "m.room.avatar/": { discord_path: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024", - url: "mxc://cadence.moe/sZtPwbfOIsvfSoWCWPrGnzql" + url: "mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF" } } }, @@ -82,4 +82,4 @@ module.exports = { system_channel_flags: 0|0 } } -} \ No newline at end of file +} diff --git a/test/test.js b/test/test.js index cb6fe5d..de399f3 100644 --- a/test/test.js +++ b/test/test.js @@ -12,4 +12,4 @@ const sync = new HeatSync({persistent: false}) Object.assign(passthrough, { config, sync, db }) -require("../d2m/actions/create-room") \ No newline at end of file +require("../d2m/actions/create-room.test") diff --git a/types.d.ts b/types.d.ts index d571359..1f00836 100644 --- a/types.d.ts +++ b/types.d.ts @@ -8,17 +8,42 @@ export type AppServiceRegistrationConfig = { rate_limited: boolean } -export type M_Room_Message_content = { - msgtype: "m.text" - body: string - formatted_body?: "org.matrix.custom.html" - format?: string +namespace Event { + export type BaseStateEvent = { + type: string + room_id: string + sender: string + content: any + state_key: string + origin_server_ts: number + unsigned: any + event_id: string + user_id: string + age: number + replaces_state: string + prev_content?: any + } + + export type M_Room_Message = { + msgtype: "m.text" + body: string + formatted_body?: "org.matrix.custom.html" + format?: string + } + + export type M_Room_Member = { + membership: string + display_name?: string + avatar_url?: string + } } -export type R_RoomCreated = { - room_id: string -} +namespace R { + export type RoomCreated = { + room_id: string + } -export type R_FileUploaded = { - content_uri: string + export type FileUploaded = { + content_uri: string + } } From 48c2ef76f5244b6df053092d2bf1b4e7a736c16f Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 8 May 2023 08:27:42 +1200 Subject: [PATCH 04/99] preparations for creating users --- d2m/actions/register-user.js | 54 +++++++++++++++++++++++++----------- types.d.ts | 8 ++++++ 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index 325a3d0..a23147d 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -1,20 +1,42 @@ // @ts-check -const reg = require("../../matrix/read-registration.js") -const fetch = require("node-fetch").default +const assert = require("assert") -fetch("https://matrix.cadence.moe/_matrix/client/v3/register", { - method: "POST", - body: JSON.stringify({ +const passthrough = require("../../passthrough") +const { discord, sync, db } = passthrough +/** @type {import("../../matrix/mreq")} */ +const mreq = sync.require("../../matrix/mreq") +/** @type {import("../../matrix/file")} */ +const file = sync.require("../../matrix/file") + +async function registerUser(username) { + assert.ok(username.startsWith("_ooye_")) + /** @type {import("../../types").R.Registered} */ + const res = await mreq.mreq("POST", "/client/v3/register", { type: "m.login.application_service", - username: "_ooye_example" - }), - headers: { - Authorization: `Bearer ${reg.as_token}` - } -}).then(res => res.text()).then(text => { - // {"user_id":"@_ooye_example:cadence.moe","home_server":"cadence.moe","access_token":"XXX","device_id":"XXX"} - console.log(text) -}).catch(err => { - console.log(err) -}) + username + }) + return res +} + +/** + * A sim is an account that is being simulated by the bridge to copy events from the other side. + * @param {import("discord-api-types/v10").APIUser} user + */ +async function createSim(user) { + assert.notEqual(user.discriminator, "0000", "user is not a webhook") + fetch("https://matrix.cadence.moe/_matrix/client/v3/register", { + method: "POST", + body: JSON.stringify({ + type: "m.login.application_service", + username: "_ooye_example" + }), + headers: { + Authorization: `Bearer ${reg.as_token}` + } + }).then(res => res.text()).then(text => { + + console.log(text) + }).catch(err => { + console.log(err) + }) diff --git a/types.d.ts b/types.d.ts index 1f00836..8d15d6b 100644 --- a/types.d.ts +++ b/types.d.ts @@ -46,4 +46,12 @@ namespace R { export type FileUploaded = { content_uri: string } + + export type Registered = { + /** "@localpart:domain.tld" */ + user_id: string + home_server: string + access_token: string + device_id: string + } } From 7ee04d085ff69fab41cb7abc30131e7a9f8de53a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 8 May 2023 17:22:20 +1200 Subject: [PATCH 05/99] username sanitisation for registration --- .vscode/tasks.json | 24 + d2m/actions/register-user.js | 31 +- d2m/converters/user-to-mxid.js | 74 + d2m/converters/user-to-mxid.test.js | 33 + matrix/api.js | 20 + package-lock.json | 2559 +++------------------------ package.json | 8 +- test/test.js | 3 +- 8 files changed, 402 insertions(+), 2350 deletions(-) create mode 100644 .vscode/tasks.json create mode 100644 d2m/converters/user-to-mxid.js create mode 100644 d2m/converters/user-to-mxid.test.js create mode 100644 matrix/api.js diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..bb95546 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "test", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [], + "label": "npm: test", + "detail": "cross-env FORCE_COLOR=true supertape --format tap test/test.js | tap-dot", + "presentation": { + "echo": false, + "reveal": "always", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": true + } + } + ] +} \ No newline at end of file diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index a23147d..8f31f23 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -4,39 +4,16 @@ const assert = require("assert") const passthrough = require("../../passthrough") const { discord, sync, db } = passthrough -/** @type {import("../../matrix/mreq")} */ -const mreq = sync.require("../../matrix/mreq") +/** @type {import("../../matrix/api")} */ +const api = sync.require("../../matrix/api") /** @type {import("../../matrix/file")} */ const file = sync.require("../../matrix/file") -async function registerUser(username) { - assert.ok(username.startsWith("_ooye_")) - /** @type {import("../../types").R.Registered} */ - const res = await mreq.mreq("POST", "/client/v3/register", { - type: "m.login.application_service", - username - }) - return res -} - /** * A sim is an account that is being simulated by the bridge to copy events from the other side. * @param {import("discord-api-types/v10").APIUser} user */ async function createSim(user) { assert.notEqual(user.discriminator, "0000", "user is not a webhook") - fetch("https://matrix.cadence.moe/_matrix/client/v3/register", { - method: "POST", - body: JSON.stringify({ - type: "m.login.application_service", - username: "_ooye_example" - }), - headers: { - Authorization: `Bearer ${reg.as_token}` - } - }).then(res => res.text()).then(text => { - - console.log(text) - }).catch(err => { - console.log(err) - }) + api.register("_ooye_example") +} diff --git a/d2m/converters/user-to-mxid.js b/d2m/converters/user-to-mxid.js new file mode 100644 index 0000000..15b997f --- /dev/null +++ b/d2m/converters/user-to-mxid.js @@ -0,0 +1,74 @@ +// @ts-check + +const assert = require("assert") + +const passthrough = require("../../passthrough") +const { sync, db } = passthrough + +/** + * Downcased and stripped username. Can only include a basic set of characters. + * https://spec.matrix.org/v1.6/appendices/#user-identifiers + * @param {import("discord-api-types/v10").APIUser} user + * @returns {string} localpart + */ +function downcaseUsername(user) { + // First, try to convert the username to the set of allowed characters + let downcased = user.username.toLowerCase() + // spaces to underscores... + .replace(/ /g, "_") + // remove disallowed characters... + .replace(/[^a-z0-9._=/-]*/g, "") + // remove leading and trailing dashes and underscores... + .replace(/(?:^[_-]*|[_-]*$)/g, "") + // The new length must be at least 2 characters (in other words, it should have some content) + if (downcased.length < 2) { + downcased = user.id + } + return downcased +} + +/** @param {string[]} preferences */ +function* generateLocalpartAlternatives(preferences) { + const best = preferences[0] + assert.ok(best) + // First, suggest the preferences... + for (const localpart of preferences) { + yield localpart + } + // ...then fall back to generating number suffixes... + let i = 2 + while (true) { + yield best + (i++) + } +} + +/** + * @param {import("discord-api-types/v10").APIUser} user + * @returns {string} + */ +function userToSimName(user) { + assert.notEqual(user.discriminator, "0000", "cannot create user for a webhook") + + // 1. Is sim user already registered? + const existing = db.prepare("SELECT sim_name FROM sim WHERE discord_id = ?").pluck().get(user.id) + if (existing) return existing + + // 2. Register based on username (could be new or old format) + const downcased = downcaseUsername(user) + const preferences = [downcased] + if (user.discriminator.length === 4) { // Old style tag? If user.username is unavailable, try the full tag next + preferences.push(downcased + user.discriminator) + } + + // Check for conflicts with already registered sims + /** @type {string[]} */ + const matches = db.prepare("SELECT sim_name FROM sim WHERE sim_name LIKE ? ESCAPE '@'").pluck().all(downcased + "%") + // Keep generating until we get a suggestion that doesn't conflict + for (const suggestion of generateLocalpartAlternatives(preferences)) { + if (!matches.includes(suggestion)) return suggestion + } + + throw new Error(`Ran out of suggestions when generating sim name. downcased: "${downcased}"`) +} + +module.exports.userToSimName = userToSimName \ No newline at end of file diff --git a/d2m/converters/user-to-mxid.test.js b/d2m/converters/user-to-mxid.test.js new file mode 100644 index 0000000..7cda6d7 --- /dev/null +++ b/d2m/converters/user-to-mxid.test.js @@ -0,0 +1,33 @@ +const {test} = require("supertape") +const tryToCatch = require("try-to-catch") +const assert = require("assert") +const {userToSimName} = require("./user-to-mxid") + +test("user2name: cannot create user for a webhook", async t => { + const [error] = await tryToCatch(() => userToSimName({discriminator: "0000"})) + t.ok(error instanceof assert.AssertionError, error.message) +}) + +test("user2name: works on normal name", t => { + t.equal(userToSimName({username: "Harry Styles!", discriminator: "0001"}), "harry_styles") +}) + +test("user2name: works on emojis", t => { + t.equal(userToSimName({username: "Cookie 🍪", discriminator: "0001"}), "cookie") +}) + +test("user2name: works on crazy name", t => { + t.equal(userToSimName({username: "*** D3 &W (89) _7//-", discriminator: "0001"}), "d3_w_89__7//") +}) + +test("user2name: adds discriminator if name is unavailable (old tag format)", t => { + t.equal(userToSimName({username: "BOT$", discriminator: "1234"}), "bot1234") +}) + +test("user2name: adds number suffix if name is unavailable (new username format)", t => { + t.equal(userToSimName({username: "bot", discriminator: "0"}), "bot2") +}) + +test("user2name: uses ID if name becomes too short", t => { + t.equal(userToSimName({username: "f***", discriminator: "0001", id: "9"}), "9") +}) diff --git a/matrix/api.js b/matrix/api.js new file mode 100644 index 0000000..4335585 --- /dev/null +++ b/matrix/api.js @@ -0,0 +1,20 @@ +// @ts-check + +const passthrough = require("../passthrough") +const { discord, sync, db } = passthrough +/** @type {import("./mreq")} */ +const mreq = sync.require("./mreq") +/** @type {import("./file")} */ +const file = sync.require("./file") + +/** + * @returns {Promise} + */ +function register(username) { + return mreq.mreq("POST", "/client/v3/register", { + type: "m.login.application_service", + username + }) +} + +module.exports.register = register \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b845abf..9556331 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,7 +1,7 @@ { "name": "out-of-your-element", "version": "1.0.0", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { @@ -12,25 +12,27 @@ "better-sqlite3": "^8.3.0", "cloudstorm": "^0.7.0", "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#24508e701e91d5a00fa5e773ced874d9ee8c889b", - "heatsync": "^2.4.0", + "heatsync": "^2.4.1", "js-yaml": "^4.1.0", "matrix-appservice": "^2.0.0", "matrix-js-sdk": "^24.1.0", "mixin-deep": "^2.0.1", "node-fetch": "^2.6.7", - "snowtransfer": "^0.7.0" + "snowtransfer": "^0.7.0", + "try-to-catch": "^3.0.1" }, "devDependencies": { "@types/node": "^18.16.0", "@types/node-fetch": "^2.6.3", + "cross-env": "^7.0.3", "supertape": "^8.3.0", "tap-dot": "github:cloudrac3r/tap-dot#223a4e67a6f7daf015506a12a7af74605f06c7f4" } }, "node_modules/@babel/runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", - "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", + "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", "dependencies": { "regenerator-runtime": "^0.13.11" }, @@ -151,9 +153,9 @@ } }, "node_modules/@matrix-org/matrix-sdk-crypto-js": { - "version": "0.1.0-alpha.7", - "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.7.tgz", - "integrity": "sha512-sQEG9cSfNji5NYBf5h7j5IxYVO0dwtAKoetaVyR+LhIXz/Su7zyEE3EwlAWAeJOFdAV/vZ5LTNyh39xADuNlTg==", + "version": "0.1.0-alpha.8", + "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.8.tgz", + "integrity": "sha512-hdmbbGXKrN6JNo3wdBaR5Zs3lXlzllT3U43ViNTlabB3nKkOZQnEAN/Isv+4EQSgz1+8897veI9Q8sqlQX22oA==", "engines": { "node": ">= 10" } @@ -279,9 +281,9 @@ "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" }, "node_modules/@types/node": { - "version": "18.16.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.0.tgz", - "integrity": "sha512-BsAaKhB+7X+H4GnSjGhJG9Qi8Tw+inU9nJDwmD5CgOmBLEI6ArdhikpLX7DjbjDRDTbqZzU2LSQNZg8WGPiSZQ==", + "version": "18.16.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.5.tgz", + "integrity": "sha512-seOA34WMo9KB+UA78qaJoCO20RJzZGVXQ5Sh6FWu0g/hfT44nKXnej3/tCQl7FL97idFpBhisLYCTB50S0EirA==", "dev": true }, "node_modules/@types/node-fetch": { @@ -294,29 +296,15 @@ "form-data": "^3.0.0" } }, - "node_modules/@types/node-fetch/node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "node_modules/@types/react": { - "version": "18.0.38", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.38.tgz", - "integrity": "sha512-ExsidLLSzYj4cvaQjGnQCk4HFfVT9+EZ9XZsQ8Hsrcn8QNgXtpZ3m9vSIC2MWtx7jHictK6wYhQgGh6ic58oOw==", + "version": "18.2.6", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.6.tgz", + "integrity": "sha512-wRZClXn//zxCFW+ye/D2qY65UsYP1Fpex2YXorHc8awoNamkMZSvBxwxdYVInsHOZZd2Ppq8isnSzJL5Mpf8OA==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -394,6 +382,19 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -426,9 +427,9 @@ } }, "node_modules/backtracker": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/backtracker/-/backtracker-3.3.1.tgz", - "integrity": "sha512-bQTxQ/JL9nm8/mNFP/bkiOJN0w9OOK6LQDqa+Jt9YnnFGQzAplYwi2TDmzuEwHoAtuUso5StoyKvZazkPO4q4g==" + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/backtracker/-/backtracker-3.3.2.tgz", + "integrity": "sha512-bXosLBp95xGE1kcWRnbG+e+Sw1xCKTT1GMdvaqEk9cGfBQoFPEvdI78fepKIJWFejV4NCl7OLUt8SNvYom/D/w==" }, "node_modules/balanced-match": { "version": "1.0.2", @@ -504,6 +505,42 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/body-parser": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", @@ -527,19 +564,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -558,9 +582,10 @@ } }, "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, "funding": [ { "type": "github", @@ -577,7 +602,7 @@ ], "dependencies": { "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "ieee754": "^1.2.1" } }, "node_modules/bytes": { @@ -731,6 +756,38 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", @@ -742,6 +799,14 @@ "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", "dev": true }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -757,16 +822,17 @@ } }, "node_modules/deep-equal": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz", - "integrity": "sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.1.tgz", + "integrity": "sha512-lKdkdV6EOGoVn65XaOsPdH4rMxTZOnmFyuIkMjM1i5HHCbfjC97dawgTAy0deYNfuqUqW+Q5VrVaQYtUpSd6yQ==", "dev": true, "dependencies": { + "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.2", - "es-get-iterator": "^1.1.2", - "get-intrinsic": "^1.1.3", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.0", "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.1", + "is-array-buffer": "^3.0.2", "is-date-object": "^1.0.5", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", @@ -774,7 +840,7 @@ "object-is": "^1.1.5", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", + "regexp.prototype.flags": "^1.5.0", "side-channel": "^1.0.4", "which-boxed-primitive": "^1.0.2", "which-collection": "^1.0.1", @@ -851,9 +917,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.37.39", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.39.tgz", - "integrity": "sha512-hkhQsQyzsTJITp311WXvHZh9j4RAMfIk2hPmsWeOTN50QTpg6zqmJNfel9D/8lYNvsU01wzw9281Yke8NhYyHg==" + "version": "0.37.41", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.41.tgz", + "integrity": "sha512-FaPGBK9hx3zqSRX1x3KQWj+OElAJKmcyyfcdCy+U4AKv+gYuIkRySM7zd1So2sE4gc1DikkghkSBgBgKh6pe4Q==" }, "node_modules/discord-markdown": { "version": "2.4.1", @@ -1012,19 +1078,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/express/node_modules/raw-body": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", @@ -1070,19 +1123,6 @@ "node": ">= 0.8" } }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -1093,9 +1133,10 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -1282,11 +1323,11 @@ } }, "node_modules/heatsync": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/heatsync/-/heatsync-2.4.0.tgz", - "integrity": "sha512-3avAZvdWohjVNhx/P1lHGEUriGP8VlbdFKrMsiBVbXzOGuEEKnC9840Qu4SyUWxgs0V1D3RIpNS3898NFgQkng==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/heatsync/-/heatsync-2.4.1.tgz", + "integrity": "sha512-cRzLwnKnJ5O4dQWXiJyFp4myKY8lGfK+49/SbPsvnr3pf2PNG1Xh8pPono303cjJeFpaPSTs609mQH1xhPVyzA==", "dependencies": { - "backtracker": "3.3.1" + "backtracker": "3.3.2" } }, "node_modules/http-errors": { @@ -1620,6 +1661,12 @@ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, "node_modules/jest-diff": { "version": "29.5.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", @@ -1725,18 +1772,10 @@ "node": ">=16.0.0" } }, - "node_modules/matrix-js-sdk/node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/matrix-widget-api": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.3.1.tgz", - "integrity": "sha512-+rN6vGvnXm+fn0uq9r2KWSL/aPtehD6ObC50jYmUcEfgo8CUpf9eUurmjbRlwZkWq3XHXFuKQBUCI9UzqWg37Q==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.4.0.tgz", + "integrity": "sha512-dw0dRylGQzDUoiaY/g5xx1tBbS7aoov31PRtFMAvG58/4uerYllV9Gfou7w+I1aglwB6hihTREzKltVjARWV6A==", "dependencies": { "@types/events": "^3.0.0", "events": "^3.2.0" @@ -1852,19 +1891,6 @@ "node": ">= 0.8.0" } }, - "node_modules/morgan/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/morgan/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/morgan/node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -1876,6 +1902,11 @@ "node": ">= 0.8" } }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", @@ -1901,9 +1932,9 @@ } }, "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", + "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -2017,6 +2048,15 @@ "node": ">= 0.8" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -2178,16 +2218,18 @@ "dev": true }, "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.3.0.tgz", + "integrity": "sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==", + "dev": true, "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10" }, "engines": { - "node": ">= 6" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/regenerator-runtime": { @@ -2306,19 +2348,6 @@ "node": ">= 0.8.0" } }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2343,6 +2372,27 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -2420,6 +2470,19 @@ "node": ">=12.0.0" } }, + "node_modules/snowtransfer/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2601,7 +2664,7 @@ "node_modules/tap-dot": { "version": "2.0.0", "resolved": "git+ssh://git@github.com/cloudrac3r/tap-dot.git#223a4e67a6f7daf015506a12a7af74605f06c7f4", - "integrity": "sha512-tHte0Cqt0Unnfz3zbhtk8ByNoh9KA7xXKWIC6/UUNJcyueR9DBlTx1YCH6TH7rIKaz8aBNqCV9HCCpAWilOOAQ==", + "integrity": "sha512-nhpVoX/s4IJJdm7OymbZ1rdZNlqt3l/yQ9Z9if06jcgRNto6QAZOrLIvdCILYQ6GE0mu+cyVA8s24amdwbvHiQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2615,7 +2678,7 @@ "node_modules/tap-out": { "version": "3.2.1", "resolved": "git+ssh://git@github.com/cloudrac3r/tap-out.git#1b4ec6084aedb9f44ccaa0c7185ff9bfd83da771", - "integrity": "sha512-55eUSaX5AeEOqJMRlj9XSqUlLV/yYPOPeC3kOFqjmorq6/jlH5kIeqpgLNW5PlPEAuggzYREYYXqrN8E37ZPfQ==", + "integrity": "sha512-hyMMeN6jagEyeEOq7Xyg3GNIAR3iUDDocaoK5QRPjnEGbFZOYJ39Dkn7BsFUXyGVl+s4b3zPkDcTS38+6KTXCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2630,45 +2693,6 @@ "node": ">=8.0.0" } }, - "node_modules/tap-out/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/tap-out/node_modules/readable-stream": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.3.0.tgz", - "integrity": "sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==", - "dev": true, - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, "node_modules/tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", @@ -2695,6 +2719,19 @@ "node": ">=6" } }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -2727,7 +2764,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-3.0.1.tgz", "integrity": "sha512-hOY83V84Hx/1sCzDSaJA+Xz2IIQOHRvjxzt+F0OjbQGPZ6yLPLArMA0gw/484MlfUkQbCpKYMLX3VDCAjWKfzQ==", - "dev": true, "engines": { "node": ">=6" } @@ -2781,6 +2817,14 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -2803,6 +2847,21 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/which-boxed-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", @@ -2879,2143 +2938,5 @@ "node": ">=12" } } - }, - "dependencies": { - "@babel/runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", - "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", - "requires": { - "regenerator-runtime": "^0.13.11" - } - }, - "@cloudcmd/stub": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@cloudcmd/stub/-/stub-4.0.1.tgz", - "integrity": "sha512-7x7tVxJZOdQowHv/VKwHLo9aoNNoVRc6PdKYqyKcDHX+xrF78jSXnqEWrOplnD/gF+tCnyFafu1Is+lFfWCILw==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "jest-diff": "^27.0.6", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - }, - "diff-sequences": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", - "dev": true - }, - "jest-diff": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", - "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - } - }, - "jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "dev": true - }, - "pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - } - }, - "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, - "@jest/schemas": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", - "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", - "dev": true, - "requires": { - "@sinclair/typebox": "^0.25.16" - } - }, - "@matrix-org/matrix-sdk-crypto-js": { - "version": "0.1.0-alpha.7", - "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.7.tgz", - "integrity": "sha512-sQEG9cSfNji5NYBf5h7j5IxYVO0dwtAKoetaVyR+LhIXz/Su7zyEE3EwlAWAeJOFdAV/vZ5LTNyh39xADuNlTg==" - }, - "@putout/cli-keypress": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@putout/cli-keypress/-/cli-keypress-1.0.0.tgz", - "integrity": "sha512-w+lRVGZodRM4K214R4jvyOsmCUGA3OnaYDOJ2rpXj6a+O6n91zLlkb7JYsw6I0QCNmXjpNLJSoLgzGWTue6YIg==", - "dev": true, - "requires": { - "ci-info": "^3.1.1", - "fullstore": "^3.0.0" - } - }, - "@putout/cli-validate-args": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@putout/cli-validate-args/-/cli-validate-args-1.1.1.tgz", - "integrity": "sha512-AczBS98YyvsDVxvvYjHGyIygFu3i/EJ0xsruU6MlytTuUiCFQIE/QQPDy1bcN5J2Y75BzSYncaYnVrEGcBjeeQ==", - "dev": true, - "requires": { - "fastest-levenshtein": "^1.0.12", - "just-kebab-case": "^1.1.0" - } - }, - "@sinclair/typebox": { - "version": "0.25.24", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", - "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", - "dev": true - }, - "@supertape/engine-loader": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@supertape/engine-loader/-/engine-loader-1.1.3.tgz", - "integrity": "sha512-5ilgEng0WBvMQjNJWQ/bnAA6HKgbLKxTya2C0RxFH0LYSN5faBVtgxjLDvTQ+5L+ZxjK/7ooQDDaRS1Mo0ga5Q==", - "dev": true, - "requires": { - "try-catch": "^3.0.0" - } - }, - "@supertape/formatter-fail": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@supertape/formatter-fail/-/formatter-fail-3.0.2.tgz", - "integrity": "sha512-mSBnNprfLFmGvZkP+ODGroPLFCIN5BWE/06XaD5ghiTVWqek7eH8IDqvKyEduvuQu1O5tvQiaTwQsyxvikF+2w==", - "dev": true, - "requires": { - "@supertape/formatter-tap": "^3.0.3", - "fullstore": "^3.0.0" - } - }, - "@supertape/formatter-json-lines": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@supertape/formatter-json-lines/-/formatter-json-lines-2.0.1.tgz", - "integrity": "sha512-9LWOCu4yOF9orf4QJseS8lP3hXkYn24qn57VqYt/3r2aRJv4vWTPfaL1ot5JRHCZs0qXrV1sqPmN6E05rRLDYA==", - "dev": true, - "requires": { - "fullstore": "^3.0.0" - } - }, - "@supertape/formatter-progress-bar": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@supertape/formatter-progress-bar/-/formatter-progress-bar-3.0.0.tgz", - "integrity": "sha512-rVFAQ21eApq3TQV8taFLNcCxcGZvvOPxQC63swdmHFCp+07Dt3tvC/aFxF35NLobc3rySasGSEuPucpyoPrjfg==", - "dev": true, - "requires": { - "chalk": "^4.1.0", - "ci-info": "^3.1.1", - "cli-progress": "^3.8.2", - "fullstore": "^3.0.0", - "once": "^1.4.0" - } - }, - "@supertape/formatter-short": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@supertape/formatter-short/-/formatter-short-2.0.1.tgz", - "integrity": "sha512-zxFrZfCccFV+bf6A7MCEqT/Xsf0Elc3qa0P3jShfdEfrpblEcpSo0T/Wd9jFwc7uHA3ABgxgcHy7LNIpyrFTCg==", - "dev": true - }, - "@supertape/formatter-tap": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@supertape/formatter-tap/-/formatter-tap-3.0.3.tgz", - "integrity": "sha512-U5OuMotfYhGo9cZ8IgdAXRTH5Yy8yfLDZzYo1upTPTwlJJquKwtvuz7ptiB7BN3OFr5YakkDYlFxOYPcLo7urg==", - "dev": true - }, - "@supertape/operator-stub": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@supertape/operator-stub/-/operator-stub-3.0.0.tgz", - "integrity": "sha512-LZ6E4nSMDMbLOhvEZyeXo8wS5EBiAAffWrohb7yaVHDVTHr+xkczzPxinkvcOBhNuAtC0kVARdMbHg+HULmozA==", - "dev": true, - "requires": { - "@cloudcmd/stub": "^4.0.0" - } - }, - "@types/events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" - }, - "@types/node": { - "version": "18.16.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.0.tgz", - "integrity": "sha512-BsAaKhB+7X+H4GnSjGhJG9Qi8Tw+inU9nJDwmD5CgOmBLEI6ArdhikpLX7DjbjDRDTbqZzU2LSQNZg8WGPiSZQ==", - "dev": true - }, - "@types/node-fetch": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.3.tgz", - "integrity": "sha512-ETTL1mOEdq/sxUtgtOhKjyB2Irra4cjxksvcMUR5Zr4n+PxVhsCD9WS46oPbHL3et9Zde7CNRr+WUNlcHvsX+w==", - "dev": true, - "requires": { - "@types/node": "*", - "form-data": "^3.0.0" - }, - "dependencies": { - "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } - } - }, - "@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" - }, - "@types/react": { - "version": "18.0.38", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.38.tgz", - "integrity": "sha512-ExsidLLSzYj4cvaQjGnQCk4HFfVT9+EZ9XZsQ8Hsrcn8QNgXtpZ3m9vSIC2MWtx7jHictK6wYhQgGh6ic58oOw==", - "requires": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" - }, - "@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" - }, - "abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, - "requires": { - "event-target-shim": "^5.0.0" - } - }, - "accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - } - }, - "another-json": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/another-json/-/another-json-0.2.0.tgz", - "integrity": "sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg==" - }, - "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "as-table": { - "version": "1.0.55", - "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", - "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", - "dev": true, - "requires": { - "printable-characters": "^1.0.42" - } - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true - }, - "backtracker": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/backtracker/-/backtracker-3.3.1.tgz", - "integrity": "sha512-bQTxQ/JL9nm8/mNFP/bkiOJN0w9OOK6LQDqa+Jt9YnnFGQzAplYwi2TDmzuEwHoAtuUso5StoyKvZazkPO4q4g==" - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "base-x": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", - "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "requires": { - "safe-buffer": "5.1.2" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } - } - }, - "better-sqlite3": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-8.3.0.tgz", - "integrity": "sha512-JTmvBZL/JLTc+3Msbvq6gK6elbU9/wVMqiudplHrVJpr7sVMR9KJrNhZAbW+RhXKlpMcuEhYkdcHa3TXKNXQ1w==", - "requires": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.0" - } - }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "requires": { - "file-uri-to-path": "1.0.0" - } - }, - "bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "bs58": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", - "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", - "requires": { - "base-x": "^4.0.0" - } - }, - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "centra": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/centra/-/centra-2.6.0.tgz", - "integrity": "sha512-dgh+YleemrT8u85QL11Z6tYhegAs3MMxsaWAq/oXeAmYJ7VxL3SI9TZtnfaEvNDMAPolj25FXIb3S+HCI4wQaQ==" - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, - "ci-info": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", - "dev": true - }, - "cli-progress": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", - "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", - "dev": true, - "requires": { - "string-width": "^4.2.3" - } - }, - "cloudstorm": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.7.0.tgz", - "integrity": "sha512-k+1u1kTdtlz3L6lnflAKMhkkZPoBl/2Du2czNvad2pYNOMBs8e0XZpSuCazC50Q29tzi08latn4SxtLbkws50A==", - "requires": { - "snowtransfer": "0.7.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "colorette": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", - "dev": true - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "requires": { - "safe-buffer": "5.2.1" - } - }, - "content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" - }, - "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" - }, - "data-uri-to-buffer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", - "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", - "dev": true - }, - "decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "requires": { - "mimic-response": "^3.1.0" - } - }, - "deep-equal": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz", - "integrity": "sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-get-iterator": "^1.1.2", - "get-intrinsic": "^1.1.3", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.1", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" - }, - "define-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", - "dev": true, - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" - }, - "detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" - }, - "diff-sequences": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", - "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", - "dev": true - }, - "discord-api-types": { - "version": "0.37.39", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.39.tgz", - "integrity": "sha512-hkhQsQyzsTJITp311WXvHZh9j4RAMfIk2hPmsWeOTN50QTpg6zqmJNfel9D/8lYNvsU01wzw9281Yke8NhYyHg==" - }, - "discord-markdown": { - "version": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#24508e701e91d5a00fa5e773ced874d9ee8c889b", - "from": "discord-markdown@git+https://git.sr.ht/~cadence/nodejs-discord-markdown#24508e701e91d5a00fa5e773ced874d9ee8c889b", - "requires": { - "simple-markdown": "^0.7.2" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "requires": { - "once": "^1.4.0" - } - }, - "es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - } - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" - }, - "event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true - }, - "events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" - }, - "expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" - }, - "express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", - "requires": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - } - } - }, - "fastest-levenshtein": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "dev": true - }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" - }, - "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, - "requires": { - "is-callable": "^1.1.3" - } - }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" - }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "fullstore": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fullstore/-/fullstore-3.0.0.tgz", - "integrity": "sha512-EEIdG+HWpyygWRwSLIZy+x4u0xtghjHNfhQb0mI5825Mmjq6oFESFUY0hoZigEgd3KH8GX+ZOCK9wgmOiS7VBQ==", - "dev": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true - }, - "get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - } - }, - "get-source": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", - "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", - "dev": true, - "requires": { - "data-uri-to-buffer": "^2.0.0", - "source-map": "^0.6.1" - } - }, - "github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" - }, - "glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - } - }, - "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.3" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.1" - } - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, - "requires": { - "has-symbols": "^1.0.2" - } - }, - "heatsync": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/heatsync/-/heatsync-2.4.0.tgz", - "integrity": "sha512-3avAZvdWohjVNhx/P1lHGEUriGP8VlbdFKrMsiBVbXzOGuEEKnC9840Qu4SyUWxgs0V1D3RIpNS3898NFgQkng==", - "requires": { - "backtracker": "3.3.1" - } - }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "internal-slot": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", - "dev": true, - "requires": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - } - }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" - }, - "is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" - } - }, - "is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "requires": { - "has-bigints": "^1.0.1" - } - }, - "is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true - }, - "is-core-module": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", - "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", - "dev": true - }, - "is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-set": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", - "dev": true - }, - "is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "requires": { - "has-symbols": "^1.0.2" - } - }, - "is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", - "dev": true, - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - } - }, - "is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", - "dev": true - }, - "is-weakset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", - "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - } - }, - "isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, - "jest-diff": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", - "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^29.4.3", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" - } - }, - "jest-get-type": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", - "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "requires": { - "argparse": "^2.0.1" - } - }, - "just-kebab-case": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/just-kebab-case/-/just-kebab-case-1.1.0.tgz", - "integrity": "sha512-QkuwuBMQ9BQHMUEkAtIA4INLrkmnnveqlFB1oFi09gbU0wBdZo6tTnyxNWMR84zHxBuwK7GLAwqN8nrvVxOLTA==", - "dev": true - }, - "loglevel": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", - "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==" - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, - "matrix-appservice": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/matrix-appservice/-/matrix-appservice-2.0.0.tgz", - "integrity": "sha512-HCIuJ5i0YuO8b0dMyGe5dqlsE4f3RzHU0MuMg/2gGAZ4HL3r7aSWOFbyIWStSSUrk1qCa9Eml0i4EnEi0pOtdA==", - "requires": { - "body-parser": "^1.19.0", - "express": "^4.18.1", - "js-yaml": "^4.1.0", - "morgan": "^1.10.0" - } - }, - "matrix-events-sdk": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz", - "integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==" - }, - "matrix-js-sdk": { - "version": "24.1.0", - "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-24.1.0.tgz", - "integrity": "sha512-xEx2ZoNsS56dwgqLJ3rIv2SUpFxdQLrLKmJCpMatMUKCAg+NGuZfpQ3QXblIbGaqFNQZCH7fC7S48AeTMZp1Jw==", - "requires": { - "@babel/runtime": "^7.12.5", - "@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.5", - "another-json": "^0.2.0", - "bs58": "^5.0.0", - "content-type": "^1.0.4", - "loglevel": "^1.7.1", - "matrix-events-sdk": "0.0.1", - "matrix-widget-api": "^1.3.1", - "p-retry": "4", - "sdp-transform": "^2.14.1", - "unhomoglyph": "^1.0.6", - "uuid": "9" - }, - "dependencies": { - "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" - } - } - }, - "matrix-widget-api": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.3.1.tgz", - "integrity": "sha512-+rN6vGvnXm+fn0uq9r2KWSL/aPtehD6ObC50jYmUcEfgo8CUpf9eUurmjbRlwZkWq3XHXFuKQBUCI9UzqWg37Q==", - "requires": { - "@types/events": "^3.0.0", - "events": "^3.2.0" - } - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, - "mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" - }, - "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - }, - "minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" - }, - "mixin-deep": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-2.0.1.tgz", - "integrity": "sha512-imbHQNRglyaplMmjBLL3V5R6Bfq5oM+ivds3SKgc6oRtzErEnBUUc5No11Z2pilkUvl42gJvi285xTNswcKCMA==" - }, - "mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" - }, - "morgan": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", - "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", - "requires": { - "basic-auth": "~2.0.1", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-finished": "~2.3.0", - "on-headers": "~1.0.2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "requires": { - "ee-first": "1.1.1" - } - } - } - }, - "napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" - }, - "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" - }, - "node-abi": { - "version": "3.40.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.40.0.tgz", - "integrity": "sha512-zNy02qivjjRosswoYmPi8hIKJRr8MpQyeKT6qlcq/OnOgA3Rhoae+IYOqsM9V5+JnHWmxKnWOT2GxvtqdtOCXA==", - "requires": { - "semver": "^7.3.5" - } - }, - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" - }, - "object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - } - }, - "on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "requires": { - "wrappy": "1" - } - }, - "p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", - "requires": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - } - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "prebuild-install": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", - "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", - "requires": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - } - }, - "pretty-format": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", - "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.4.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, - "printable-characters": { - "version": "1.0.42", - "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", - "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", - "dev": true - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true - }, - "proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "requires": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "requires": { - "side-channel": "^1.0.4" - } - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" - }, - "raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "re-emitter": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/re-emitter/-/re-emitter-1.1.4.tgz", - "integrity": "sha512-C0SIXdXDSus2yqqvV7qifnb4NoWP7mEBXJq3axci301mXHCZb8Djwm4hrEZo4UeXRaEnfjH98uQ8EBppk2oNWA==", - "dev": true - }, - "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" - }, - "regexp.prototype.flags": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", - "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "functions-have-names": "^1.2.3" - } - }, - "resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", - "dev": true, - "requires": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==" - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sdp-transform": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz", - "integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==" - }, - "semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "requires": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - }, - "dependencies": { - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } - } - }, - "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - } - }, - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" - }, - "simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "requires": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "simple-markdown": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/simple-markdown/-/simple-markdown-0.7.3.tgz", - "integrity": "sha512-uGXIc13NGpqfPeFJIt/7SHHxd6HekEJYtsdoCM06mEBPL9fQH/pSD7LRM6PZ7CKchpSvxKL4tvwMamqAaNDAyg==", - "requires": { - "@types/react": ">=16.0.0" - } - }, - "snowtransfer": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.7.0.tgz", - "integrity": "sha512-vc7B46tO4QeK99z/pN8ISd8QvO9QB3Oo4qP7nYYhriIMOtjYkHMi8t6kUBPIJLbeX+h0NpfwxaGJfXNLm1ZQ5A==", - "requires": { - "centra": "^2.6.0", - "discord-api-types": "^0.37.31", - "form-data": "^4.0.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "split": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", - "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", - "dev": true, - "requires": { - "through": "2" - } - }, - "stacktracey": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", - "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", - "dev": true, - "requires": { - "as-table": "^1.0.36", - "get-source": "^2.0.12" - } - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" - }, - "stop-iteration-iterator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dev": true, - "requires": { - "internal-slot": "^1.0.4" - } - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, - "strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" - }, - "supertape": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/supertape/-/supertape-8.3.0.tgz", - "integrity": "sha512-dcMylmkr1Mctr5UBCrlvZynuBRuLvlkWJLGXdL/PcI41BERnObO+kV0PeZhH5n6lwVnvK2xfvZyN32WIAPf/tw==", - "dev": true, - "requires": { - "@cloudcmd/stub": "^4.0.0", - "@putout/cli-keypress": "^1.0.0", - "@putout/cli-validate-args": "^1.0.1", - "@supertape/engine-loader": "^1.0.0", - "@supertape/formatter-fail": "^3.0.0", - "@supertape/formatter-json-lines": "^2.0.0", - "@supertape/formatter-progress-bar": "^3.0.0", - "@supertape/formatter-short": "^2.0.0", - "@supertape/formatter-tap": "^3.0.0", - "@supertape/operator-stub": "^3.0.0", - "cli-progress": "^3.8.2", - "deep-equal": "^2.0.3", - "fullstore": "^3.0.0", - "glob": "^8.0.3", - "jest-diff": "^29.0.1", - "once": "^1.4.0", - "resolve": "^1.17.0", - "stacktracey": "^2.1.7", - "strip-ansi": "^7.0.0", - "try-to-catch": "^3.0.0", - "wraptile": "^3.0.0", - "yargs-parser": "^21.0.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "tap-dot": { - "version": "git+ssh://git@github.com/cloudrac3r/tap-dot.git#223a4e67a6f7daf015506a12a7af74605f06c7f4", - "integrity": "sha512-tHte0Cqt0Unnfz3zbhtk8ByNoh9KA7xXKWIC6/UUNJcyueR9DBlTx1YCH6TH7rIKaz8aBNqCV9HCCpAWilOOAQ==", - "dev": true, - "from": "tap-dot@github:cloudrac3r/tap-dot#223a4e67a6f7daf015506a12a7af74605f06c7f4", - "requires": { - "colorette": "^1.0.5", - "tap-out": "github:cloudrac3r/tap-out#1b4ec6084aedb9f44ccaa0c7185ff9bfd83da771" - } - }, - "tap-out": { - "version": "git+ssh://git@github.com/cloudrac3r/tap-out.git#1b4ec6084aedb9f44ccaa0c7185ff9bfd83da771", - "integrity": "sha512-55eUSaX5AeEOqJMRlj9XSqUlLV/yYPOPeC3kOFqjmorq6/jlH5kIeqpgLNW5PlPEAuggzYREYYXqrN8E37ZPfQ==", - "dev": true, - "from": "tap-out@github:cloudrac3r/tap-out#1b4ec6084aedb9f44ccaa0c7185ff9bfd83da771", - "requires": { - "re-emitter": "1.1.4", - "readable-stream": "^4.3.0", - "split": "^1.0.1" - }, - "dependencies": { - "buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "readable-stream": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.3.0.tgz", - "integrity": "sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==", - "dev": true, - "requires": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10" - } - } - } - }, - "tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "requires": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "requires": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - } - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, - "toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" - }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "try-catch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/try-catch/-/try-catch-3.0.1.tgz", - "integrity": "sha512-91yfXw1rr/P6oLpHSyHDOHm0vloVvUoo9FVdw8YwY05QjJQG9OT0LUxe2VRAzmHG+0CUOmI3nhxDUMLxDN/NEQ==", - "dev": true - }, - "try-to-catch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/try-to-catch/-/try-to-catch-3.0.1.tgz", - "integrity": "sha512-hOY83V84Hx/1sCzDSaJA+Xz2IIQOHRvjxzt+F0OjbQGPZ6yLPLArMA0gw/484MlfUkQbCpKYMLX3VDCAjWKfzQ==", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "unhomoglyph": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/unhomoglyph/-/unhomoglyph-1.0.6.tgz", - "integrity": "sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==" - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "requires": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - } - }, - "which-collection": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", - "dev": true, - "requires": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" - } - }, - "which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", - "dev": true, - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "wraptile": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/wraptile/-/wraptile-3.0.0.tgz", - "integrity": "sha512-23LJhkIw940uTcDFyJZmNyO0z8lEINOTGCr4vR5YCG3urkdXwduRIhivBm9wKaVynLHYvxoHHYbKsDiafCLp6w==", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true - } } } diff --git a/package.json b/package.json index e3ca70f..f483842 100644 --- a/package.json +++ b/package.json @@ -18,21 +18,23 @@ "better-sqlite3": "^8.3.0", "cloudstorm": "^0.7.0", "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#24508e701e91d5a00fa5e773ced874d9ee8c889b", - "heatsync": "^2.4.0", + "heatsync": "^2.4.1", "js-yaml": "^4.1.0", "matrix-appservice": "^2.0.0", "matrix-js-sdk": "^24.1.0", "mixin-deep": "^2.0.1", "node-fetch": "^2.6.7", - "snowtransfer": "^0.7.0" + "snowtransfer": "^0.7.0", + "try-to-catch": "^3.0.1" }, "devDependencies": { "@types/node": "^18.16.0", "@types/node-fetch": "^2.6.3", + "cross-env": "^7.0.3", "supertape": "^8.3.0", "tap-dot": "github:cloudrac3r/tap-dot#223a4e67a6f7daf015506a12a7af74605f06c7f4" }, "scripts": { - "test": "FORCE_COLOR=true supertape --format tap test/test.js | tap-dot" + "test": "cross-env FORCE_COLOR=true supertape --format tap test/test.js | tap-dot" } } diff --git a/test/test.js b/test/test.js index de399f3..3008abf 100644 --- a/test/test.js +++ b/test/test.js @@ -8,8 +8,9 @@ const passthrough = require("../passthrough") const db = new sqlite("db/ooye.db") // @ts-ignore -const sync = new HeatSync({persistent: false}) +const sync = new HeatSync({watchFS: false}) Object.assign(passthrough, { config, sync, db }) require("../d2m/actions/create-room.test") +require("../d2m/converters/user-to-mxid.test") \ No newline at end of file From 3bc29def419911d016ea7d42fcdcc31257d7048a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 8 May 2023 23:37:51 +1200 Subject: [PATCH 06/99] switch to using api functions over mreq --- .vscode/tasks.json | 12 ++-------- d2m/actions/create-room.js | 14 +++++------- d2m/actions/create-space.js | 8 +++---- d2m/actions/register-user.js | 8 +++++-- d2m/converters/message-to-event.js | 4 ++-- d2m/converters/user-to-mxid.js | 3 ++- matrix/api.js | 35 +++++++++++++++++++++++++++++- stdin.js | 7 +++--- types.d.ts | 4 ++++ 9 files changed, 64 insertions(+), 31 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index bb95546..9a830be 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -10,15 +10,7 @@ }, "problemMatcher": [], "label": "npm: test", - "detail": "cross-env FORCE_COLOR=true supertape --format tap test/test.js | tap-dot", - "presentation": { - "echo": false, - "reveal": "always", - "focus": false, - "panel": "shared", - "showReuseMessage": false, - "clear": true - } + "detail": "cross-env FORCE_COLOR=true supertape --format tap test/test.js | tap-dot" } ] -} \ No newline at end of file +} diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 214d026..7e9abed 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -5,10 +5,10 @@ const DiscordTypes = require("discord-api-types/v10") const passthrough = require("../../passthrough") const { discord, sync, db } = passthrough -/** @type {import("../../matrix/mreq")} */ -const mreq = sync.require("../../matrix/mreq") /** @type {import("../../matrix/file")} */ const file = sync.require("../../matrix/file") +/** @type {import("../../matrix/api")} */ +const api = sync.require("../../matrix/api") function kstateStripConditionals(kstate) { for (const [k, content] of Object.entries(kstate)) { @@ -51,8 +51,7 @@ function stateToKState(events) { * @param {string} roomID */ async function roomToKState(roomID) { - /** @type {import("../../types").Event.BaseStateEvent[]} */ - const root = await mreq.mreq("GET", `/client/v3/rooms/${roomID}/state`) + const root = await api.getAllState(roomID) return stateToKState(root) } @@ -63,7 +62,7 @@ async function roomToKState(roomID) { function applyKStateDiffToRoom(roomID, kstate) { const events = kstateToState(kstate) return Promise.all(events.map(({type, state_key, content}) => - mreq.mreq("PUT", `/client/v3/rooms/${roomID}/state/${type}/${state_key}`, content) + api.sendState(roomID, type, state_key, content) )) } @@ -131,8 +130,7 @@ async function channelToKState(channel, guild) { * @param {any} kstate */ async function createRoom(channel, guild, spaceID, kstate) { - /** @type {import("../../types").R.RoomCreated} */ - const root = await mreq.mreq("POST", "/client/v3/createRoom", { + const root = await api.createRoom({ name: channel.name, topic: channel.topic || undefined, preset: "private_chat", @@ -144,7 +142,7 @@ async function createRoom(channel, guild, spaceID, kstate) { db.prepare("INSERT INTO channel_room (channel_id, room_id) VALUES (?, ?)").run(channel.id, root.room_id) // Put the newly created child into the space - await mreq.mreq("PUT", `/client/v3/rooms/${spaceID}/state/m.space.child/${root.room_id}`, { + await api.sendState(spaceID, "m.space.child", root.room_id, { via: ["cadence.moe"] // TODO: use the proper server }) } diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index b3d7e95..517dad4 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -2,14 +2,14 @@ const passthrough = require("../../passthrough") const { sync, db } = passthrough -/** @type {import("../../matrix/mreq")} */ -const mreq = sync.require("../../matrix/mreq") +/** @type {import("../../matrix/api")} */ +const api = sync.require("../../matrix/api") /** * @param {import("discord-api-types/v10").RESTGetAPIGuildResult} guild */ function createSpace(guild) { - return mreq.mreq("POST", "/client/v3/createRoom", { + return api.createRoom({ name: guild.name, preset: "private_chat", visibility: "private", @@ -37,7 +37,7 @@ function createSpace(guild) { } } ] - }).then(/** @param {import("../../types").R.RoomCreated} root */ root => { + }).then(root => { db.prepare("INSERT INTO guild_space (guild_id, space_id) VALUES (?, ?)").run(guild.id, root.room_id) return root }) diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index 8f31f23..f970218 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -8,12 +8,16 @@ const { discord, sync, db } = passthrough const api = sync.require("../../matrix/api") /** @type {import("../../matrix/file")} */ const file = sync.require("../../matrix/file") +/** @type {import("../converters/user-to-mxid")} */ +const userToMxid = sync.require("../converters/user-to-mxid") /** * A sim is an account that is being simulated by the bridge to copy events from the other side. * @param {import("discord-api-types/v10").APIUser} user */ async function createSim(user) { - assert.notEqual(user.discriminator, "0000", "user is not a webhook") - api.register("_ooye_example") + const simName = userToMxid.userToSimName(user) + const appservicePrefix = "_ooye_" + const localpart = appservicePrefix + simName + await api.register(localpart) } diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 6f9bc37..dd6aabc 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -4,7 +4,7 @@ const markdown = require("discord-markdown") /** * @param {import("discord-api-types/v10").APIMessage} message - * @returns {import("../../types").M_Room_Message_content} + * @returns {import("../../types").Event.M_Room_Message} */ function messageToEvent(message) { const body = message.content @@ -25,4 +25,4 @@ function messageToEvent(message) { } } -module.exports.messageToEvent = messageToEvent \ No newline at end of file +module.exports.messageToEvent = messageToEvent diff --git a/d2m/converters/user-to-mxid.js b/d2m/converters/user-to-mxid.js index 15b997f..35d9368 100644 --- a/d2m/converters/user-to-mxid.js +++ b/d2m/converters/user-to-mxid.js @@ -43,6 +43,7 @@ function* generateLocalpartAlternatives(preferences) { } /** + * Whole process for checking the database and generating the right sim name. * @param {import("discord-api-types/v10").APIUser} user * @returns {string} */ @@ -71,4 +72,4 @@ function userToSimName(user) { throw new Error(`Ran out of suggestions when generating sim name. downcased: "${downcased}"`) } -module.exports.userToSimName = userToSimName \ No newline at end of file +module.exports.userToSimName = userToSimName diff --git a/matrix/api.js b/matrix/api.js index 4335585..88c62d8 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -1,5 +1,7 @@ // @ts-check +const assert = require("assert") + const passthrough = require("../passthrough") const { discord, sync, db } = passthrough /** @type {import("./mreq")} */ @@ -8,6 +10,7 @@ const mreq = sync.require("./mreq") const file = sync.require("./file") /** + * @param {string} username * @returns {Promise} */ function register(username) { @@ -17,4 +20,34 @@ function register(username) { }) } -module.exports.register = register \ No newline at end of file +/** + * @returns {Promise} + */ +function createRoom(content) { + return mreq.mreq("POST", "/client/v3/createRoom", content) +} + +/** + * @param {string} roomID + * @returns {Promise} + */ +function getAllState(roomID) { + return mreq.mreq("GET", `/client/v3/rooms/${roomID}/state`) +} + +/** + * @param {string} roomID + * @param {string} type + * @param {string} stateKey + * @returns {Promise} + */ +function sendState(roomID, type, stateKey, content) { + assert.ok(type) + assert.ok(stateKey) + return mreq.mreq("PUT", `/client/v3/rooms/${roomID}/state/${type}/${stateKey}`, content) +} + +module.exports.register = register +module.exports.createRoom = createRoom +module.exports.getAllState = getAllState +module.exports.sendState = sendState diff --git a/stdin.js b/stdin.js index a57c044..551d7d2 100644 --- a/stdin.js +++ b/stdin.js @@ -6,9 +6,10 @@ const util = require("util") const passthrough = require("./passthrough") const { discord, config, sync, db } = passthrough -const createSpace = sync.require("./d2m/actions/create-space.js") -const createRoom = sync.require("./d2m/actions/create-room.js") -const mreq = sync.require("./matrix/mreq.js") +const createSpace = sync.require("./d2m/actions/create-space") +const createRoom = sync.require("./d2m/actions/create-room") +const mreq = sync.require("./matrix/mreq") +const api = sync.require("./matrix/api") const guildID = "112760669178241024" const extraContext = {} diff --git a/types.d.ts b/types.d.ts index 8d15d6b..b3a9acc 100644 --- a/types.d.ts +++ b/types.d.ts @@ -54,4 +54,8 @@ namespace R { access_token: string device_id: string } + + export type EventSent = { + event_id: string + } } From 1e7e66dc31a6c6a4d1c83399f9e7551d86913f93 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 9 May 2023 00:58:46 +1200 Subject: [PATCH 07/99] register -> invite -> join -> send flow --- d2m/actions/create-space.js | 9 +++-- d2m/actions/register-user.js | 53 +++++++++++++++++++++++++++-- d2m/actions/send-message.js | 34 +++++++++--------- d2m/converters/user-to-mxid.test.js | 2 +- matrix/api.js | 50 +++++++++++++++++++++++---- matrix/api.test.js | 23 +++++++++++++ matrix/mreq.js | 2 -- matrix/txnid.js | 2 +- stdin.js | 1 + test/test.js | 3 +- types.d.ts | 5 +++ 11 files changed, 150 insertions(+), 34 deletions(-) create mode 100644 matrix/api.test.js diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index 517dad4..4218c1f 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -8,8 +8,8 @@ const api = sync.require("../../matrix/api") /** * @param {import("discord-api-types/v10").RESTGetAPIGuildResult} guild */ -function createSpace(guild) { - return api.createRoom({ +async function createSpace(guild) { + const roomID = api.createRoom({ name: guild.name, preset: "private_chat", visibility: "private", @@ -37,10 +37,9 @@ function createSpace(guild) { } } ] - }).then(root => { - db.prepare("INSERT INTO guild_space (guild_id, space_id) VALUES (?, ?)").run(guild.id, root.room_id) - return root }) + db.prepare("INSERT INTO guild_space (guild_id, space_id) VALUES (?, ?)").run(guild.id, roomID) + return roomID } module.exports.createSpace = createSpace diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index f970218..a5fd0ef 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -1,6 +1,7 @@ // @ts-check const assert = require("assert") +const reg = require("../../matrix/read-registration") const passthrough = require("../../passthrough") const { discord, sync, db } = passthrough @@ -14,10 +15,58 @@ const userToMxid = sync.require("../converters/user-to-mxid") /** * A sim is an account that is being simulated by the bridge to copy events from the other side. * @param {import("discord-api-types/v10").APIUser} user + * @returns mxid */ async function createSim(user) { + // Choose sim name const simName = userToMxid.userToSimName(user) - const appservicePrefix = "_ooye_" - const localpart = appservicePrefix + simName + const localpart = reg.namespace_prefix + simName + const mxid = "@" + localpart + ":cadence.moe" + + // Save chosen name in the database forever + db.prepare("INSERT INTO sim (discord_id, sim_name, localpart, mxid) VALUES (?, ?, ?, ?)").run(user.id, simName, localpart, mxid) + + // Register matrix user with that name await api.register(localpart) + return mxid } + +/** + * Ensure a sim is registered for the user. + * If there is already a sim, use that one. If there isn't one yet, register a new sim. + * @returns mxid + */ +async function ensureSim(user) { + let mxid = null + const existing = db.prepare("SELECT mxid FROM sim WHERE discord_id = ?").pluck().get(user.id) + if (existing) { + mxid = existing + } else { + mxid = await createSim(user) + } + return mxid +} + +/** + * Ensure a sim is registered for the user and is joined to the room. + * @returns mxid + */ +async function ensureSimJoined(user, roomID) { + // Ensure room ID is really an ID, not an alias + assert.ok(roomID[0] === "!") + + // Ensure user + const mxid = await ensureSim(user) + + // Ensure joined + const existing = db.prepare("SELECT * FROM sim_member WHERE room_id = ? and mxid = ?").get(roomID, mxid) + if (!existing) { + await api.inviteToRoom(roomID, mxid) + await api.joinRoom(roomID, mxid) + db.prepare("INSERT INTO sim_member (room_id, mxid) VALUES (?, ?)").run(roomID, mxid) + } + return mxid +} + +module.exports.ensureSim = ensureSim +module.exports.ensureSimJoined = ensureSimJoined diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index 1f71a66..0a425ee 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -1,27 +1,29 @@ // @ts-check -const reg = require("../../matrix/read-registration.js") -const makeTxnId = require("../../matrix/txnid.js") const fetch = require("node-fetch").default -const messageToEvent = require("../converters/message-to-event.js") +const reg = require("../../matrix/read-registration.js") + +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") /** * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message */ -function sendMessage(message) { +async function sendMessage(message) { const event = messageToEvent.messageToEvent(message) - return fetch(`https://matrix.cadence.moe/_matrix/client/v3/rooms/!VwVlIAjOjejUpDhlbA:cadence.moe/send/m.room.message/${makeTxnId()}?user_id=@_ooye_example:cadence.moe`, { - method: "PUT", - body: JSON.stringify(event), - headers: { - Authorization: `Bearer ${reg.as_token}` - } - }).then(res => res.text()).then(text => { - // {"event_id":"$4Zxs0fMmYlbo-sTlMmSEvwIs9b4hcg6yORzK0Ems84Q"} - console.log(text) - }).catch(err => { - console.log(err) - }) + const roomID = "!VwVlIAjOjejUpDhlbA:cadence.moe" + let senderMxid = null + if (!message.webhook_id) { + senderMxid = await registerUser.ensureSimJoined(message.author, roomID) + } + const eventID = api.sendEvent(roomID, "m.room.message", event, senderMxid) + return eventID } module.exports.sendMessage = sendMessage diff --git a/d2m/converters/user-to-mxid.test.js b/d2m/converters/user-to-mxid.test.js index 7cda6d7..4c721fc 100644 --- a/d2m/converters/user-to-mxid.test.js +++ b/d2m/converters/user-to-mxid.test.js @@ -13,7 +13,7 @@ test("user2name: works on normal name", t => { }) test("user2name: works on emojis", t => { - t.equal(userToSimName({username: "Cookie 🍪", discriminator: "0001"}), "cookie") + t.equal(userToSimName({username: "🍪 Cookie Monster 🍪", discriminator: "0001"}), "cookie_monster") }) test("user2name: works on crazy name", t => { diff --git a/matrix/api.js b/matrix/api.js index 88c62d8..e3a2600 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -8,6 +8,15 @@ const { discord, sync, db } = passthrough const mreq = sync.require("./mreq") /** @type {import("./file")} */ const file = sync.require("./file") +/** @type {import("./txnid")} */ +const makeTxnId = sync.require("./txnid") + +function path(p, mxid = null) { + if (!mxid) return p + const u = new URL(p, "http://localhost") + u.searchParams.set("user_id", mxid) + return u.pathname + "?" + u.searchParams.toString() +} /** * @param {string} username @@ -21,10 +30,27 @@ function register(username) { } /** - * @returns {Promise} + * @returns {Promise} room ID */ -function createRoom(content) { - return mreq.mreq("POST", "/client/v3/createRoom", content) +async function createRoom(content) { + /** @type {import("../types").R.RoomCreated} */ + const root = await mreq.mreq("POST", "/client/v3/createRoom", content) + return root.room_id +} + +/** + * @returns {Promise} room ID + */ +async function joinRoom(roomIDOrAlias, mxid) { + /** @type {import("../types").R.RoomJoined} */ + const root = await mreq.mreq("POST", path(`/client/v3/join/${roomIDOrAlias}`, mxid)) + return root.room_id +} + +async function inviteToRoom(roomID, mxidToInvite, mxid) { + await mreq.mreq("POST", path(`/client/v3/rooms/${roomID}/invite`, mxid), { + user_id: mxidToInvite + }) } /** @@ -39,15 +65,27 @@ function getAllState(roomID) { * @param {string} roomID * @param {string} type * @param {string} stateKey - * @returns {Promise} + * @returns {Promise} event ID */ -function sendState(roomID, type, stateKey, content) { +async function sendState(roomID, type, stateKey, content, mxid) { assert.ok(type) assert.ok(stateKey) - return mreq.mreq("PUT", `/client/v3/rooms/${roomID}/state/${type}/${stateKey}`, content) + /** @type {import("../types").R.EventSent} */ + const root = await mreq.mreq("PUT", path(`/client/v3/rooms/${roomID}/state/${type}/${stateKey}`, mxid), content) + return root.event_id } +async function sendEvent(roomID, type, content, mxid) { + /** @type {import("../types").R.EventSent} */ + const root = await mreq.mreq("PUT", path(`/client/v3/rooms/${roomID}/send/${type}/${makeTxnId.makeTxnId()}`, mxid), content) + return root.event_id +} + +module.exports.path = path module.exports.register = register module.exports.createRoom = createRoom +module.exports.joinRoom = joinRoom +module.exports.inviteToRoom = inviteToRoom module.exports.getAllState = getAllState module.exports.sendState = sendState +module.exports.sendEvent = sendEvent diff --git a/matrix/api.test.js b/matrix/api.test.js new file mode 100644 index 0000000..f54c665 --- /dev/null +++ b/matrix/api.test.js @@ -0,0 +1,23 @@ +const {test} = require("supertape") +const assert = require("assert") +const {path} = require("./api") + +test("api path: no change for plain path", t => { + t.equal(path("/hello/world"), "/hello/world") +}) + +test("api path: add mxid to the URL", t => { + t.equal(path("/hello/world", "12345"), "/hello/world?user_id=12345") +}) + +test("api path: empty path with mxid", t => { + t.equal(path("", "12345"), "/?user_id=12345") +}) + +test("api path: existing query parameters with mxid", t => { + t.equal(path("/hello/world?foo=bar&baz=qux", "12345"), "/hello/world?foo=bar&baz=qux&user_id=12345") +}) + +test("api path: real world mxid", t => { + t.equal(path("/hello/world", "@cookie_monster:cadence.moe"), "/hello/world?user_id=%40cookie_monster%3Acadence.moe") +}) diff --git a/matrix/mreq.js b/matrix/mreq.js index 1345f78..6c4eaa3 100644 --- a/matrix/mreq.js +++ b/matrix/mreq.js @@ -16,8 +16,6 @@ class MatrixServerError extends Error { this.data = data /** @type {string} */ this.errcode = data.errcode - /** @type {string} */ - this.error = data.error } } diff --git a/matrix/txnid.js b/matrix/txnid.js index 1e26378..a3568df 100644 --- a/matrix/txnid.js +++ b/matrix/txnid.js @@ -2,6 +2,6 @@ let now = Date.now() -module.exports = function makeTxnId() { +module.exports.makeTxnId = function makeTxnId() { return now++ } diff --git a/stdin.js b/stdin.js index 551d7d2..99345ab 100644 --- a/stdin.js +++ b/stdin.js @@ -8,6 +8,7 @@ const { discord, config, sync, db } = passthrough const createSpace = sync.require("./d2m/actions/create-space") const createRoom = sync.require("./d2m/actions/create-room") +const registerUser = sync.require("./d2m/actions/register-user") const mreq = sync.require("./matrix/mreq") const api = sync.require("./matrix/api") const guildID = "112760669178241024" diff --git a/test/test.js b/test/test.js index 3008abf..1068136 100644 --- a/test/test.js +++ b/test/test.js @@ -13,4 +13,5 @@ const sync = new HeatSync({watchFS: false}) Object.assign(passthrough, { config, sync, db }) require("../d2m/actions/create-room.test") -require("../d2m/converters/user-to-mxid.test") \ No newline at end of file +require("../d2m/converters/user-to-mxid.test") +require("../matrix/api.test") diff --git a/types.d.ts b/types.d.ts index b3a9acc..bc24329 100644 --- a/types.d.ts +++ b/types.d.ts @@ -4,6 +4,7 @@ export type AppServiceRegistrationConfig = { hs_token: string url: string sender_localpart: string + namespace_prefix: string protocols: [string] rate_limited: boolean } @@ -43,6 +44,10 @@ namespace R { room_id: string } + export type RoomJoined = { + room_id: string + } + export type FileUploaded = { content_uri: string } From 7526d63690f5ab64450e82e95b246ae5d0a3f1ad Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 9 May 2023 08:03:57 +1200 Subject: [PATCH 08/99] get createRoom and ensureRoom interface working --- d2m/actions/create-room.js | 53 ++++++++++++++++++++++++++++++++----- d2m/actions/send-message.js | 4 ++- d2m/event-dispatcher.js | 1 + matrix/api.js | 8 +++++- 4 files changed, 57 insertions(+), 9 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 7e9abed..b254951 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -124,13 +124,15 @@ async function channelToKState(channel, guild) { } /** + * Create a bridge room, store the relationship in the database, and add it to the guild's space. * @param {import("discord-api-types/v10").APIGuildTextChannel} channel * @param guild * @param {string} spaceID * @param {any} kstate + * @returns {Promise} room ID */ async function createRoom(channel, guild, spaceID, kstate) { - const root = await api.createRoom({ + const roomID = await api.createRoom({ name: channel.name, topic: channel.topic || undefined, preset: "private_chat", @@ -139,12 +141,14 @@ async function createRoom(channel, guild, spaceID, kstate) { initial_state: kstateToState(kstate) }) - db.prepare("INSERT INTO channel_room (channel_id, room_id) VALUES (?, ?)").run(channel.id, root.room_id) + db.prepare("INSERT INTO channel_room (channel_id, room_id) VALUES (?, ?)").run(channel.id, roomID) // Put the newly created child into the space - await api.sendState(spaceID, "m.space.child", root.room_id, { + await api.sendState(spaceID, "m.space.child", roomID, { // TODO: should I deduplicate with the equivalent code from syncRoom? via: ["cadence.moe"] // TODO: use the proper server }) + + return roomID } /** @@ -158,22 +162,46 @@ function channelToGuild(channel) { return guild } +/* + Ensure flow: + 1. Get IDs + 2. Does room exist? If so great! + (it doesn't, so it needs to be created) + 3. Get kstate for channel + 4. Create room, return new ID + + New combined flow with ensure / sync: + 1. Get IDs + 2. Does room exist? + 2.5: If room does exist AND don't need to sync: return here + 3. Get kstate for channel + 4. Create room with kstate if room doesn't exist + 5. Get and update room state with kstate if room does exist +*/ + /** * @param {string} channelID + * @param {boolean} shouldActuallySync false if just need to ensure room exists (which is a quick database check), true if also want to sync room data when it does exist (slow) + * @returns {Promise} room ID */ -async function syncRoom(channelID) { +async function _syncRoom(channelID, shouldActuallySync) { /** @ts-ignore @type {import("discord-api-types/v10").APIGuildChannel} */ const channel = discord.channels.get(channelID) assert.ok(channel) const guild = channelToGuild(channel) - const {spaceID, channelKState} = await channelToKState(channel, guild) - /** @type {string?} */ const existing = db.prepare("SELECT room_id from channel_room WHERE channel_id = ?").pluck().get(channel.id) if (!existing) { + const {spaceID, channelKState} = await channelToKState(channel, guild) return createRoom(channel, guild, spaceID, channelKState) } else { + if (!shouldActuallySync) { + return existing // only need to ensure room exists, and it does. return the room ID + } + + const {spaceID, channelKState} = await channelToKState(channel, guild) + // sync channel state to room const roomKState = await roomToKState(existing) const roomDiff = diffKState(roomKState, channelKState) @@ -187,10 +215,20 @@ async function syncRoom(channelID) { } }) const spaceApply = applyKStateDiffToRoom(spaceID, spaceDiff) - return Promise.all([roomApply, spaceApply]) + await Promise.all([roomApply, spaceApply]) + + return existing } } +function ensureRoom(channelID) { + return _syncRoom(channelID, false) +} + +function syncRoom(channelID) { + return _syncRoom(channelID, true) +} + async function createAllForGuild(guildID) { const channelIDs = discord.guildChannelMap.get(guildID) assert.ok(channelIDs) @@ -200,6 +238,7 @@ async function createAllForGuild(guildID) { } module.exports.createRoom = createRoom +module.exports.ensureRoom = ensureRoom module.exports.syncRoom = syncRoom module.exports.createAllForGuild = createAllForGuild module.exports.kstateToState = kstateToState diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index 0a425ee..630cf48 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -11,13 +11,15 @@ const messageToEvent = sync.require("../converters/message-to-event") 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 */ async function sendMessage(message) { const event = messageToEvent.messageToEvent(message) - const roomID = "!VwVlIAjOjejUpDhlbA:cadence.moe" + const roomID = await createRoom.ensureRoom(message.channel_id) let senderMxid = null if (!message.webhook_id) { senderMxid = await registerUser.ensureSimJoined(message.author, roomID) diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index e4de37e..8539044 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -16,6 +16,7 @@ module.exports = { * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message */ onMessageCreate(client, message) { + if (message.guild_id !== "112760669178241024") return // TODO: activate on other servers (requires the space creation flow to be done first) sendMessage.sendMessage(message) }, diff --git a/matrix/api.js b/matrix/api.js index e3a2600..04b7cd1 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -11,7 +11,12 @@ const file = sync.require("./file") /** @type {import("./txnid")} */ const makeTxnId = sync.require("./txnid") -function path(p, mxid = null) { +/** + * @param {string} p endpoint to access + * @param {string} [mxid] optional: user to act as, for the ?user_id parameter + * @returns {string} the new endpoint + */ +function path(p, mxid) { if (!mxid) return p const u = new URL(p, "http://localhost") u.searchParams.set("user_id", mxid) @@ -65,6 +70,7 @@ function getAllState(roomID) { * @param {string} roomID * @param {string} type * @param {string} stateKey + * @param {string} [mxid] * @returns {Promise} event ID */ async function sendState(roomID, type, stateKey, content, mxid) { From da6603d258d882ec9a72ac7c7403d2e5504ddca5 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 9 May 2023 15:29:46 +1200 Subject: [PATCH 09/99] send sim messages to the proper rooms --- d2m/actions/register-user.js | 10 +++++++++- d2m/actions/send-message.js | 2 +- d2m/converters/user-to-mxid.js | 1 + d2m/converters/user-to-mxid.test.js | 4 ++++ d2m/discord-packets.js | 7 ++++--- d2m/event-dispatcher.js | 3 --- matrix/api.js | 4 ++++ matrix/mreq.js | 5 +++-- matrix/read-registration.test.js | 11 +++++++++++ test/test.js | 1 + 10 files changed, 38 insertions(+), 10 deletions(-) create mode 100644 matrix/read-registration.test.js diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index a5fd0ef..508dd39 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -24,10 +24,18 @@ async function createSim(user) { const mxid = "@" + localpart + ":cadence.moe" // Save chosen name in the database forever + // Making this database change right away so that in a concurrent registration, the 2nd registration will already have generated a different localpart because it can see this row when it generates db.prepare("INSERT INTO sim (discord_id, sim_name, localpart, mxid) VALUES (?, ?, ?, ?)").run(user.id, simName, localpart, mxid) // Register matrix user with that name - await api.register(localpart) + try { + await api.register(localpart) + } catch (e) { + // If user creation fails, manually undo the database change. Still isn't perfect, but should help. + // (A transaction would be preferable, but I don't think it's safe to leave transaction open across event loop ticks.) + db.prepare("DELETE FROM sim WHERE discord_id = ?").run(user.id) + throw e + } return mxid } diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index 630cf48..f828134 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -28,4 +28,4 @@ async function sendMessage(message) { return eventID } -module.exports.sendMessage = sendMessage +module.exports.sendMessage = sendMessage \ No newline at end of file diff --git a/d2m/converters/user-to-mxid.js b/d2m/converters/user-to-mxid.js index 35d9368..89e47a4 100644 --- a/d2m/converters/user-to-mxid.js +++ b/d2m/converters/user-to-mxid.js @@ -44,6 +44,7 @@ function* generateLocalpartAlternatives(preferences) { /** * Whole process for checking the database and generating the right sim name. + * It is very important this is not an async function: once the name has been chosen, the calling function should be able to immediately claim that name into the database in the same event loop tick. * @param {import("discord-api-types/v10").APIUser} user * @returns {string} */ diff --git a/d2m/converters/user-to-mxid.test.js b/d2m/converters/user-to-mxid.test.js index 4c721fc..8c4c430 100644 --- a/d2m/converters/user-to-mxid.test.js +++ b/d2m/converters/user-to-mxid.test.js @@ -31,3 +31,7 @@ test("user2name: adds number suffix if name is unavailable (new username format) test("user2name: uses ID if name becomes too short", t => { t.equal(userToSimName({username: "f***", discriminator: "0001", id: "9"}), "9") }) + +test("user2name: uses ID when name has only disallowed characters", t => { + t.equal(userToSimName({username: "!@#$%^&*", discriminator: "0001", id: "9"}), "9") +}) \ No newline at end of file diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index 0d16cdc..3786393 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -6,15 +6,16 @@ const DiscordTypes = require("discord-api-types/v10") const passthrough = require("../passthrough") const { sync } = passthrough -/** @type {typeof import("./event-dispatcher")} */ -const eventDispatcher = sync.require("./event-dispatcher") - const utils = { /** * @param {import("./discord-client")} client * @param {import("cloudstorm").IGatewayMessage} message */ onPacket(client, message) { + // requiring this later so that the client is already constructed by the time event-dispatcher is loaded + /** @type {typeof import("./event-dispatcher")} */ + const eventDispatcher = sync.require("./event-dispatcher") + if (message.t === "READY") { if (client.ready) return client.ready = true diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 8539044..83a1a70 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -2,9 +2,6 @@ const {sync} = require("../passthrough") -/** @type {import("./actions/create-space")}) */ -const createSpace = sync.require("./actions/create-space") - /** @type {import("./actions/send-message")}) */ const sendMessage = sync.require("./actions/send-message") diff --git a/matrix/api.js b/matrix/api.js index 04b7cd1..dbc39bc 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -28,6 +28,7 @@ function path(p, mxid) { * @returns {Promise} */ function register(username) { + console.log(`[api] register: ${username}`) return mreq.mreq("POST", "/client/v3/register", { type: "m.login.application_service", username @@ -38,6 +39,7 @@ function register(username) { * @returns {Promise} room ID */ async function createRoom(content) { + console.log(`[api] create room:`, content) /** @type {import("../types").R.RoomCreated} */ const root = await mreq.mreq("POST", "/client/v3/createRoom", content) return root.room_id @@ -74,6 +76,7 @@ function getAllState(roomID) { * @returns {Promise} event ID */ async function sendState(roomID, type, stateKey, content, mxid) { + console.log(`[api] state: ${roomID}: ${type}/${stateKey}`) assert.ok(type) assert.ok(stateKey) /** @type {import("../types").R.EventSent} */ @@ -82,6 +85,7 @@ async function sendState(roomID, type, stateKey, content, mxid) { } async function sendEvent(roomID, type, content, mxid) { + console.log(`[api] event to ${roomID} as ${mxid || "default sim"}`) /** @type {import("../types").R.EventSent} */ const root = await mreq.mreq("PUT", path(`/client/v3/rooms/${roomID}/send/${type}/${makeTxnId.makeTxnId()}`, mxid), content) return root.event_id diff --git a/matrix/mreq.js b/matrix/mreq.js index 6c4eaa3..df34d91 100644 --- a/matrix/mreq.js +++ b/matrix/mreq.js @@ -11,11 +11,12 @@ const reg = sync.require("./read-registration.js") const baseUrl = "https://matrix.cadence.moe/_matrix" class MatrixServerError extends Error { - constructor(data) { + constructor(data, opts) { super(data.error || data.errcode) this.data = data /** @type {string} */ this.errcode = data.errcode + this.opts = opts } } @@ -38,7 +39,7 @@ async function mreq(method, url, body, extra = {}) { const res = await fetch(baseUrl + url, opts) const root = await res.json() - if (!res.ok || root.errcode) throw new MatrixServerError(root) + if (!res.ok || root.errcode) throw new MatrixServerError(root, opts) return root } diff --git a/matrix/read-registration.test.js b/matrix/read-registration.test.js new file mode 100644 index 0000000..9c7f828 --- /dev/null +++ b/matrix/read-registration.test.js @@ -0,0 +1,11 @@ +const {test} = require("supertape") +const assert = require("assert") +const reg = require("./read-registration") + +test("reg: has necessary parameters", t => { + const propertiesToCheck = ["sender_localpart", "id", "as_token", "namespace_prefix"] + t.deepEqual( + propertiesToCheck.filter(p => p in reg), + propertiesToCheck + ) +}) \ No newline at end of file diff --git a/test/test.js b/test/test.js index 1068136..4e01708 100644 --- a/test/test.js +++ b/test/test.js @@ -12,6 +12,7 @@ const sync = new HeatSync({watchFS: false}) Object.assign(passthrough, { config, sync, db }) +require("../matrix/read-registration.test") require("../d2m/actions/create-room.test") require("../d2m/converters/user-to-mxid.test") require("../matrix/api.test") From 4d8b74f61f194e3084e40e7693acbf314717a4ef Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 9 May 2023 17:13:59 +1200 Subject: [PATCH 10/99] adding basic reactions to discord messages --- d2m/actions/add-reaction.js | 36 ++++++++++++++++++++++++++++++++++++ d2m/actions/register-user.js | 2 ++ d2m/actions/send-message.js | 8 +++----- d2m/event-dispatcher.js | 5 ++++- 4 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 d2m/actions/add-reaction.js diff --git a/d2m/actions/add-reaction.js b/d2m/actions/add-reaction.js new file mode 100644 index 0000000..82449cd --- /dev/null +++ b/d2m/actions/add-reaction.js @@ -0,0 +1,36 @@ +// @ts-check + +const assert = require("assert") + +const passthrough = require("../../passthrough") +const { discord, sync, db } = 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") + +/** + * @param {import("discord-api-types/v10").GatewayMessageReactionAddDispatchData} data + */ +async function addReaction(data) { + const user = data.member?.user + assert.ok(user && user.username) + // TODO: should add my own sent messages to event_message so they can be reacted to? + const parentID = db.prepare("SELECT event_id FROM event_message WHERE message_id = ? AND part = 0").pluck().get(data.message_id) // 0 = primary + if (!parentID) return // TODO: how to handle reactions for unbridged messages? is there anything I can do? + assert.equal(typeof parentID, "string") + const roomID = await createRoom.ensureRoom(data.channel_id) + const senderMxid = await registerUser.ensureSimJoined(user, roomID) + const eventID = api.sendEvent(roomID, "m.reaction", { + "m.relates_to": { + rel_type: "m.annotation", + event_id: parentID, + key: data.emoji.name + } + }, senderMxid) + return eventID +} + +module.exports.addReaction = addReaction diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index 508dd39..04b0998 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -42,6 +42,7 @@ async function createSim(user) { /** * Ensure a sim is registered for the user. * If there is already a sim, use that one. If there isn't one yet, register a new sim. + * @param {import("discord-api-types/v10").APIUser} user * @returns mxid */ async function ensureSim(user) { @@ -57,6 +58,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 * @returns mxid */ async function ensureSimJoined(user, roomID) { diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index f828134..fb181d2 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -1,8 +1,5 @@ // @ts-check -const fetch = require("node-fetch").default -const reg = require("../../matrix/read-registration.js") - const passthrough = require("../../passthrough") const { discord, sync, db } = passthrough /** @type {import("../converters/message-to-event")} */ @@ -24,8 +21,9 @@ async function sendMessage(message) { if (!message.webhook_id) { senderMxid = await registerUser.ensureSimJoined(message.author, roomID) } - const eventID = api.sendEvent(roomID, "m.room.message", event, senderMxid) + const eventID = await api.sendEvent(roomID, "m.room.message", event, senderMxid) + db.prepare("INSERT INTO event_message (event_id, message_id, part) VALUES (?, ?, ?)").run(eventID, message.id, 0) // 0 is primary, 1 is supporting return eventID } -module.exports.sendMessage = sendMessage \ No newline at end of file +module.exports.sendMessage = sendMessage diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 83a1a70..eeee451 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -4,6 +4,8 @@ const {sync} = require("../passthrough") /** @type {import("./actions/send-message")}) */ const sendMessage = sync.require("./actions/send-message") +/** @type {import("./actions/add-reaction")}) */ +const addReaction = sync.require("./actions/add-reaction") // Grab Discord events we care about for the bridge, check them, and pass them on @@ -22,7 +24,8 @@ module.exports = { * @param {import("discord-api-types/v10").GatewayMessageReactionAddDispatchData} data */ onReactionAdd(client, data) { + if (data.emoji.id !== null) return // TOOD: image emoji reactions console.log(data) - return {} + addReaction.addReaction(data) } } From f418d51e555ee80681932e6b4f51b2cf618ec5e6 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 10 May 2023 17:40:31 +1200 Subject: [PATCH 11/99] refactor kstate, add stub user syncing function --- d2m/actions/create-room.js | 76 +++----------------------- d2m/actions/create-room.test.js | 76 +------------------------- d2m/actions/register-user.js | 10 ++++ matrix/kstate.js | 65 +++++++++++++++++++++++ matrix/kstate.test.js | 94 +++++++++++++++++++++++++++++++++ test/test.js | 3 +- 6 files changed, 181 insertions(+), 143 deletions(-) create mode 100644 matrix/kstate.js create mode 100644 matrix/kstate.test.js diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index b254951..7f2c799 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -9,50 +9,15 @@ const { discord, sync, db } = passthrough const file = sync.require("../../matrix/file") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") - -function kstateStripConditionals(kstate) { - for (const [k, content] of Object.entries(kstate)) { - if ("$if" in content) { - if (content.$if) delete content.$if - else delete kstate[k] - } - } - return kstate -} - -function kstateToState(kstate) { - const events = [] - for (const [k, content] of Object.entries(kstate)) { - // conditional for whether a key is even part of the kstate (doing this declaratively on json is hard, so represent it as a property instead.) - if ("$if" in content && !content.$if) continue - delete content.$if - - const [type, state_key] = k.split("/") - assert.ok(typeof type === "string") - assert.ok(typeof state_key === "string") - events.push({type, state_key, content}) - } - return events -} - -/** - * @param {import("../../types").Event.BaseStateEvent[]} events - * @returns {any} - */ -function stateToKState(events) { - const kstate = {} - for (const event of events) { - kstate[event.type + "/" + event.state_key] = event.content - } - return kstate -} +/** @type {import("../../matrix/kstate")} */ +const ks = sync.require("../../matrix/kstate") /** * @param {string} roomID */ async function roomToKState(roomID) { const root = await api.getAllState(roomID) - return stateToKState(root) + return ks.stateToKState(root) } /** @@ -60,33 +25,12 @@ async function roomToKState(roomID) { * @params {any} kstate */ function applyKStateDiffToRoom(roomID, kstate) { - const events = kstateToState(kstate) + const events = ks.kstateToState(kstate) return Promise.all(events.map(({type, state_key, content}) => api.sendState(roomID, type, state_key, content) )) } -function diffKState(actual, target) { - const diff = {} - // go through each key that it should have - for (const key of Object.keys(target)) { - if (key in actual) { - // diff - try { - assert.deepEqual(actual[key], target[key]) - } catch (e) { - // they differ. reassign the target - diff[key] = target[key] - } - } else { - // not present, needs to be added - diff[key] = target[key] - } - // keys that are missing in "actual" will not be deleted on "target" (no action) - } - return diff -} - /** * @param {import("discord-api-types/v10").APIGuildTextChannel} channel * @param {import("discord-api-types/v10").APIGuild} guild @@ -98,7 +42,7 @@ async function channelToKState(channel, guild) { const avatarEventContent = {} if (guild.icon) { avatarEventContent.discord_path = file.guildIcon(guild) - avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) + avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) // TODO: somehow represent future values in kstate (callbacks?), while still allowing for diffing, so test cases don't need to touch the media API } const channelKState = { @@ -138,7 +82,7 @@ async function createRoom(channel, guild, spaceID, kstate) { preset: "private_chat", visibility: "private", invite: ["@cadence:cadence.moe"], // TODO - initial_state: kstateToState(kstate) + initial_state: ks.kstateToState(kstate) }) db.prepare("INSERT INTO channel_room (channel_id, room_id) VALUES (?, ?)").run(channel.id, roomID) @@ -204,12 +148,12 @@ async function _syncRoom(channelID, shouldActuallySync) { // sync channel state to room const roomKState = await roomToKState(existing) - const roomDiff = diffKState(roomKState, channelKState) + const roomDiff = ks.diffKState(roomKState, channelKState) const roomApply = applyKStateDiffToRoom(existing, roomDiff) // sync room as space member const spaceKState = await roomToKState(spaceID) - const spaceDiff = diffKState(spaceKState, { + const spaceDiff = ks.diffKState(spaceKState, { [`m.space.child/${existing}`]: { via: ["cadence.moe"] // TODO: use the proper server } @@ -241,8 +185,4 @@ module.exports.createRoom = createRoom module.exports.ensureRoom = ensureRoom module.exports.syncRoom = syncRoom module.exports.createAllForGuild = createAllForGuild -module.exports.kstateToState = kstateToState -module.exports.stateToKState = stateToKState -module.exports.diffKState = diffKState module.exports.channelToKState = channelToKState -module.exports.kstateStripConditionals = kstateStripConditionals diff --git a/d2m/actions/create-room.test.js b/d2m/actions/create-room.test.js index 5ce52e8..ab390fc 100644 --- a/d2m/actions/create-room.test.js +++ b/d2m/actions/create-room.test.js @@ -1,80 +1,8 @@ -const {kstateToState, stateToKState, diffKState, channelToKState, kstateStripConditionals} = require("./create-room") +const {channelToKState} = require("./create-room") +const {kstateStripConditionals} = require("../../matrix/kstate") const {test} = require("supertape") const testData = require("../../test/data") -test("kstate2state: general", t => { - t.deepEqual(kstateToState({ - "m.room.name/": {name: "test name"}, - "m.room.member/@cadence:cadence.moe": {membership: "join"} - }), [ - { - type: "m.room.name", - state_key: "", - content: { - name: "test name" - } - }, - { - type: "m.room.member", - state_key: "@cadence:cadence.moe", - content: { - membership: "join" - } - } - ]) -}) - -test("state2kstate: general", t => { - t.deepEqual(stateToKState([ - { - type: "m.room.name", - state_key: "", - content: { - name: "test name" - } - }, - { - type: "m.room.member", - state_key: "@cadence:cadence.moe", - content: { - membership: "join" - } - } - ]), { - "m.room.name/": {name: "test name"}, - "m.room.member/@cadence:cadence.moe": {membership: "join"} - }) -}) - -test("diffKState: detects edits", t => { - t.deepEqual( - diffKState({ - "m.room.name/": {name: "test name"}, - "same/": {a: 2} - }, { - "m.room.name/": {name: "edited name"}, - "same/": {a: 2} - }), - { - "m.room.name/": {name: "edited name"} - } - ) -}) - -test("diffKState: detects new properties", t => { - t.deepEqual( - diffKState({ - "m.room.name/": {name: "test name"}, - }, { - "m.room.name/": {name: "test name"}, - "new/": {a: 2} - }), - { - "new/": {a: 2} - } - ) -}) - test("channel2room: general", async t => { t.deepEqual( kstateStripConditionals(await channelToKState(testData.channel.general, testData.guild.general).then(x => x.channelKState)), diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index 04b0998..89bac2c 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -78,5 +78,15 @@ async function ensureSimJoined(user, roomID) { return mxid } +/** + * @param {import("discord-api-types/v10").APIUser} user + * @param {Required>} member + */ +async function memberToStateContent(user, member) { + return { + displayname: member.nick || user.username + } +} + module.exports.ensureSim = ensureSim module.exports.ensureSimJoined = ensureSimJoined diff --git a/matrix/kstate.js b/matrix/kstate.js new file mode 100644 index 0000000..398b1b6 --- /dev/null +++ b/matrix/kstate.js @@ -0,0 +1,65 @@ +// @ts-check + +const assert = require("assert") + +/** Mutates the input. */ +function kstateStripConditionals(kstate) { + for (const [k, content] of Object.entries(kstate)) { + // conditional for whether a key is even part of the kstate (doing this declaratively on json is hard, so represent it as a property instead.) + if ("$if" in content) { + if (content.$if) delete content.$if + else delete kstate[k] + } + } + return kstate +} + +function kstateToState(kstate) { + const events = [] + kstateStripConditionals(kstate) + for (const [k, content] of Object.entries(kstate)) { + const [type, state_key] = k.split("/") + assert.ok(typeof type === "string") + assert.ok(typeof state_key === "string") + events.push({type, state_key, content}) + } + return events +} + +/** + * @param {import("../types").Event.BaseStateEvent[]} events + * @returns {any} + */ +function stateToKState(events) { + const kstate = {} + for (const event of events) { + kstate[event.type + "/" + event.state_key] = event.content + } + return kstate +} + +function diffKState(actual, target) { + const diff = {} + // go through each key that it should have + for (const key of Object.keys(target)) { + if (key in actual) { + // diff + try { + assert.deepEqual(actual[key], target[key]) + } catch (e) { + // they differ. reassign the target + diff[key] = target[key] + } + } else { + // not present, needs to be added + diff[key] = target[key] + } + // keys that are missing in "actual" will not be deleted on "target" (no action) + } + return diff +} + +module.exports.kstateStripConditionals = kstateStripConditionals +module.exports.kstateToState = kstateToState +module.exports.stateToKState = stateToKState +module.exports.diffKState = diffKState diff --git a/matrix/kstate.test.js b/matrix/kstate.test.js new file mode 100644 index 0000000..ed59e9d --- /dev/null +++ b/matrix/kstate.test.js @@ -0,0 +1,94 @@ +const {kstateToState, stateToKState, diffKState, kstateStripConditionals} = require("./kstate") +const {test} = require("supertape") + +test("kstate strip: strips false conditions", t => { + t.deepEqual(kstateStripConditionals({ + a: {$if: false, value: 2}, + b: {value: 4} + }), { + b: {value: 4} + }) +}) + +test("kstate strip: keeps true conditions while removing $if", t => { + t.deepEqual(kstateStripConditionals({ + a: {$if: true, value: 2}, + b: {value: 4} + }), { + a: {value: 2}, + b: {value: 4} + }) +}) + +test("kstate2state: general", t => { + t.deepEqual(kstateToState({ + "m.room.name/": {name: "test name"}, + "m.room.member/@cadence:cadence.moe": {membership: "join"} + }), [ + { + type: "m.room.name", + state_key: "", + content: { + name: "test name" + } + }, + { + type: "m.room.member", + state_key: "@cadence:cadence.moe", + content: { + membership: "join" + } + } + ]) +}) + +test("state2kstate: general", t => { + t.deepEqual(stateToKState([ + { + type: "m.room.name", + state_key: "", + content: { + name: "test name" + } + }, + { + type: "m.room.member", + state_key: "@cadence:cadence.moe", + content: { + membership: "join" + } + } + ]), { + "m.room.name/": {name: "test name"}, + "m.room.member/@cadence:cadence.moe": {membership: "join"} + }) +}) + +test("diffKState: detects edits", t => { + t.deepEqual( + diffKState({ + "m.room.name/": {name: "test name"}, + "same/": {a: 2} + }, { + "m.room.name/": {name: "edited name"}, + "same/": {a: 2} + }), + { + "m.room.name/": {name: "edited name"} + } + ) +}) + +test("diffKState: detects new properties", t => { + t.deepEqual( + diffKState({ + "m.room.name/": {name: "test name"}, + }, { + "m.room.name/": {name: "test name"}, + "new/": {a: 2} + }), + { + "new/": {a: 2} + } + ) +}) diff --git a/test/test.js b/test/test.js index 4e01708..bf0023e 100644 --- a/test/test.js +++ b/test/test.js @@ -12,7 +12,8 @@ const sync = new HeatSync({watchFS: false}) Object.assign(passthrough, { config, sync, db }) +require("../matrix/kstate.test") +require("../matrix/api.test") require("../matrix/read-registration.test") require("../d2m/actions/create-room.test") require("../d2m/converters/user-to-mxid.test") -require("../matrix/api.test") From 22dde9faf7bf2f88eab06164d66fd34dd106f8ae Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 10 May 2023 22:15:20 +1200 Subject: [PATCH 12/99] completed user syncing. it occurs on message send --- d2m/actions/create-room.js | 2 + d2m/actions/register-user.js | 76 ++++++++++++++++++++++++++++++++++-- d2m/actions/send-message.js | 5 +++ matrix/file.js | 11 ++++++ 4 files changed, 90 insertions(+), 4 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 7f2c799..96a9671 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -144,6 +144,8 @@ async function _syncRoom(channelID, shouldActuallySync) { return existing // only need to ensure room exists, and it does. return the room ID } + console.log(`[room sync] to matrix: ${channel.name}`) + const {spaceID, channelKState} = await channelToKState(channel, guild) // sync channel state to room diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index 89bac2c..cc5f515 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -80,13 +80,81 @@ async function ensureSimJoined(user, roomID) { /** * @param {import("discord-api-types/v10").APIUser} user - * @param {Required>} member + * @param {Omit} member */ -async function memberToStateContent(user, member) { - return { - displayname: member.nick || user.username +async function memberToStateContent(user, member, guildID) { + let displayname = user.username + if (member.nick && member.nick !== displayname) displayname = member.nick + " | " + displayname // prepend nick if present + + const content = { + displayname, + membership: "join", + "moe.cadence.ooye.member": { + }, + "uk.half-shot.discord.member": { + bot: !!user.bot, + displayColor: user.accent_color, + id: user.id, + username: user.discriminator.length === 4 ? `${user.username}#${user.discriminator}` : `@${user.username}` + } + } + + if (member.avatar || user.avatar) { + // const avatarPath = file.userAvatar(user) // the user avatar only + const avatarPath = file.memberAvatar(guildID, user, member) // the member avatar or the user avatar + content["moe.cadence.ooye.member"].avatar = avatarPath + content.avatar_url = await file.uploadDiscordFileToMxc(avatarPath) + } + + return content +} + +function calculateProfileEventContentHash(content) { + return `${content.displayname}\u0000${content.avatar_url}` +} + +/** + * @param {import("discord-api-types/v10").APIUser} user + * @param {Omit} member + */ +async function syncUser(user, member, guildID, roomID) { + const mxid = await ensureSimJoined(user, roomID) + const content = await memberToStateContent(user, member, guildID) + const profileEventContentHash = calculateProfileEventContentHash(content) + const existingHash = db.prepare("SELECT profile_event_content_hash FROM sim_member WHERE room_id = ? AND mxid = ?").pluck().get(roomID, mxid) + // only do the actual sync if the hash has changed since we last looked + if (existingHash !== profileEventContentHash) { + await api.sendState(roomID, "m.room.member", mxid, content, mxid) + db.prepare("UPDATE sim_member SET profile_event_content_hash = ? WHERE room_id = ? AND mxid = ?").run(profileEventContentHash, roomID, mxid) + } +} + +async function syncAllUsersInRoom(roomID) { + const mxids = db.prepare("SELECT mxid FROM sim_member WHERE room_id = ?").pluck().all(roomID) + + const channelID = db.prepare("SELECT channel_id FROM channel_room WHERE room_id = ?").pluck().get(roomID) + assert.ok(typeof channelID === "string") + /** @ts-ignore @type {import("discord-api-types/v10").APIGuildChannel} */ + const channel = discord.channels.get(channelID) + const guildID = channel.guild_id + assert.ok(typeof guildID === "string") + + for (const mxid of mxids) { + const userID = db.prepare("SELECT discord_id FROM sim WHERE mxid = ?").pluck().get(mxid) + assert.ok(typeof userID === "string") + + /** @ts-ignore @type {Required} */ + const member = await discord.snow.guild.getGuildMember(guildID, userID) + /** @ts-ignore @type {Required} user */ + const user = member.user + assert.ok(user) + + console.log(`[user sync] to matrix: ${user.username} in ${channel.name}`) + await syncUser(user, member, guildID, roomID) } } module.exports.ensureSim = ensureSim module.exports.ensureSimJoined = ensureSimJoined +module.exports.syncUser = syncUser +module.exports.syncAllUsersInRoom = syncAllUsersInRoom diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index fb181d2..2630430 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -1,5 +1,7 @@ // @ts-check +const assert = require("assert") + const passthrough = require("../../passthrough") const { discord, sync, db } = passthrough /** @type {import("../converters/message-to-event")} */ @@ -15,11 +17,14 @@ const createRoom = sync.require("../actions/create-room") * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message */ async function sendMessage(message) { + assert.ok(message.member) + const event = messageToEvent.messageToEvent(message) const roomID = await createRoom.ensureRoom(message.channel_id) let senderMxid = null if (!message.webhook_id) { senderMxid = await registerUser.ensureSimJoined(message.author, roomID) + await registerUser.syncUser(message.author, message.member, message.guild_id, roomID) } const eventID = await api.sendEvent(roomID, "m.room.message", event, senderMxid) db.prepare("INSERT INTO event_message (event_id, message_id, part) VALUES (?, ?, ?)").run(eventID, message.id, 0) // 0 is primary, 1 is supporting diff --git a/matrix/file.js b/matrix/file.js index 137a096..077d527 100644 --- a/matrix/file.js +++ b/matrix/file.js @@ -58,5 +58,16 @@ function guildIcon(guild) { return `/icons/${guild.id}/${guild.icon}.png?size=${IMAGE_SIZE}` } +function userAvatar(user) { + return `/avatars/${user.id}/${user.avatar}.png?size=${IMAGE_SIZE}` +} + +function memberAvatar(guildID, user, member) { + if (!member.avatar) return userAvatar(user) + return `/guilds/${guildID}/users/${user.id}/avatars/${member.avatar}.png?size=${IMAGE_SIZE}` +} + module.exports.guildIcon = guildIcon +module.exports.userAvatar = userAvatar +module.exports.memberAvatar = memberAvatar module.exports.uploadDiscordFileToMxc = uploadDiscordFileToMxc From 38d7db5071f52c1bd4db5fbf4e6a955b38d355df Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 10 May 2023 23:17:37 +1200 Subject: [PATCH 13/99] seed initial setup --- matrix/api.js | 14 ++++++++++++++ matrix/file.js | 9 ++++++++- seed.js | 35 +++++++++++++++++++++++++++++++++++ test/test.js | 1 - 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 seed.js diff --git a/matrix/api.js b/matrix/api.js index dbc39bc..846ea64 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -91,6 +91,18 @@ async function sendEvent(roomID, type, content, mxid) { return root.event_id } +async function profileSetDisplayname(mxid, displayname) { + await mreq.mreq("PUT", path(`/client/v3/profile/${mxid}/displayname`, mxid), { + displayname + }) +} + +async function profileSetAvatarUrl(mxid, avatar_url) { + await mreq.mreq("PUT", path(`/client/v3/profile/${mxid}/avatar_url`, mxid), { + avatar_url + }) +} + module.exports.path = path module.exports.register = register module.exports.createRoom = createRoom @@ -99,3 +111,5 @@ module.exports.inviteToRoom = inviteToRoom module.exports.getAllState = getAllState module.exports.sendState = sendState module.exports.sendEvent = sendEvent +module.exports.profileSetDisplayname = profileSetDisplayname +module.exports.profileSetAvatarUrl = profileSetAvatarUrl diff --git a/matrix/file.js b/matrix/file.js index 077d527..4578d4a 100644 --- a/matrix/file.js +++ b/matrix/file.js @@ -17,7 +17,14 @@ const inflight = new Map() * @param {string} path */ async function uploadDiscordFileToMxc(path) { - const url = DISCORD_IMAGES_BASE + path + let url + if (path.startsWith("http")) { + // TODO: this is cheating to make seed.js easier. due a refactor or a name change since it's not soley for discord? + // possibly could be good to save non-discord external URLs under a user-specified key rather than simply using the url? + url = path + } else { + url = DISCORD_IMAGES_BASE + path + } // Are we uploading this file RIGHT NOW? Return the same inflight promise with the same resolution let existing = inflight.get(url) diff --git a/seed.js b/seed.js new file mode 100644 index 0000000..d84ca8d --- /dev/null +++ b/seed.js @@ -0,0 +1,35 @@ +// @ts-check + +const assert = require("assert") +const sqlite = require("better-sqlite3") +const HeatSync = require("heatsync") + +const config = require("./config") +const passthrough = require("./passthrough") +const db = new sqlite("db/ooye.db") + +const sync = new HeatSync({watchFS: false}) + +Object.assign(passthrough, { config, sync, db }) + +const api = require("./matrix/api") +const file = require("./matrix/file") +const reg = require("./matrix/read-registration") + +;(async () => { + // ensure registration is correctly set... + + // test connection to homeserver... + + // upload initial images... + const avatarUrl = await file.uploadDiscordFileToMxc("https://cadence.moe/friends/out_of_your_element_rev_2.jpg") + + // set profile data on homeserver... + await api.profileSetDisplayname(`@${reg.sender_localpart}:cadence.moe`, "Out Of Your Element") + await api.profileSetAvatarUrl(`@${reg.sender_localpart}:cadence.moe`, avatarUrl) + + // database ddl... + + // add initial rows to database, like adding the bot to sim... + +})() diff --git a/test/test.js b/test/test.js index bf0023e..2c44bb3 100644 --- a/test/test.js +++ b/test/test.js @@ -7,7 +7,6 @@ const config = require("../config") const passthrough = require("../passthrough") const db = new sqlite("db/ooye.db") -// @ts-ignore const sync = new HeatSync({watchFS: false}) Object.assign(passthrough, { config, sync, db }) From e1d7ced87d5f7f10be83c1b2cf723b1d65bbef3f Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 12 May 2023 17:35:37 +1200 Subject: [PATCH 14/99] begin converting discord attachments and stickers --- d2m/actions/send-message.js | 23 ++++-- d2m/converters/message-to-event.js | 94 +++++++++++++++++++++---- d2m/converters/message-to-event.test.js | 28 ++++++++ matrix/file.js | 7 ++ test/data.js | 74 +++++++++++++++---- test/test.js | 1 + 6 files changed, 194 insertions(+), 33 deletions(-) create mode 100644 d2m/converters/message-to-event.test.js diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index 2630430..738c59a 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -19,16 +19,31 @@ const createRoom = sync.require("../actions/create-room") async function sendMessage(message) { assert.ok(message.member) - const event = messageToEvent.messageToEvent(message) const roomID = await createRoom.ensureRoom(message.channel_id) + let senderMxid = null if (!message.webhook_id) { senderMxid = await registerUser.ensureSimJoined(message.author, roomID) await registerUser.syncUser(message.author, message.member, message.guild_id, roomID) } - const eventID = await api.sendEvent(roomID, "m.room.message", event, senderMxid) - db.prepare("INSERT INTO event_message (event_id, message_id, part) VALUES (?, ?, ?)").run(eventID, message.id, 0) // 0 is primary, 1 is supporting - return eventID + + const events = await messageToEvent.messageToEvent(message) + const eventIDs = [] + let eventPart = 0 // 0 is primary, 1 is supporting + 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, part) VALUES (?, ?, ?)").run(eventID, message.id, eventPart) + + eventPart = 1 // TODO: use more intelligent algorithm to determine whether primary or supporting + eventIDs.push(eventID) + } + + return eventIDs } module.exports.sendMessage = sendMessage diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index dd6aabc..86be8ac 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -2,27 +2,93 @@ const markdown = require("discord-markdown") +const passthrough = require("../../passthrough") +const { sync, db } = passthrough +/** @type {import("../../matrix/file")} */ +const file = sync.require("../../matrix/file") + /** * @param {import("discord-api-types/v10").APIMessage} message - * @returns {import("../../types").Event.M_Room_Message} */ -function messageToEvent(message) { +async function messageToEvent(message) { + const events = [] + + // Text content appears first const body = message.content const html = markdown.toHTML(body, { - /* discordCallback: { - user: Function, - channel: Function, - role: Function, - everyone: Function, - here: Function - } */ + discordCallback: { + user: node => { + const mxid = db.prepare("SELECT mxid FROM sim WHERE discord_id = ?").pluck().get(node.id) + if (mxid) { + return "https://matrix.to/#/" + mxid + } else { + return "@" + node.id + } + }, + channel: node => { + const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(node.id) + if (roomID) { + return "https://matrix.to/#/" + roomID + } else { + return "#" + node.id + } + }, + role: node => + "@&" + node.id, + everyone: node => + "@room", + here: node => + "@here" + } }, null, null) - return { - msgtype: "m.text", - body: body, - format: "org.matrix.custom.html", - formatted_body: html + const isPlaintext = body === html + if (isPlaintext) { + events.push({ + $type: "m.room.message", + msgtype: "m.text", + body: body + }) + } else { + events.push({ + $type: "m.room.message", + msgtype: "m.text", + body: body, + format: "org.matrix.custom.html", + formatted_body: html + }) } + + // Then attachments + const attachmentEvents = await Promise.all(message.attachments.map(async attachment => { + // TODO: handle large files differently - link them instead of uploading + if (attachment.content_type?.startsWith("image/") && attachment.width && attachment.height) { + return { + $type: "m.room.message", + msgtype: "m.image", + url: await file.uploadDiscordFileToMxc(attachment.url), + external_url: attachment.url, + body: attachment.filename, + // TODO: filename: attachment.filename and then use body as the caption + info: { + mimetype: attachment.content_type, + w: attachment.width, + h: attachment.height, + size: attachment.size + } + } + } else { + return { + $type: "m.room.message", + msgtype: "m.text", + body: "Unsupported attachment:\n" + JSON.stringify(attachment, null, 2) + } + } + })) + events.push(...attachmentEvents) + + // Then stickers + + return events } module.exports.messageToEvent = messageToEvent diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js new file mode 100644 index 0000000..07a45f7 --- /dev/null +++ b/d2m/converters/message-to-event.test.js @@ -0,0 +1,28 @@ +const {test} = require("supertape") +const assert = require("assert") +const {messageToEvent} = require("./message-to-event") +const data = require("../../test/data") + +test("message2event: stickers", async t => { + const events = await messageToEvent(data.message.sticker) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "can have attachments too" + }, { + $type: "m.room.message", + msgtype: "m.image", + url: "mxc://cadence.moe/ZDCNYnkPszxGKgObUIFmvjus", + body: "image.png", + external_url: "https://cdn.discordapp.com/attachments/122155380120748034/1106366167486038016/image.png", + info: { + mimetype: "image/png", + w: 333, + h: 287, + size: 127373, + }, + }, { + $type: "m.sticker", + todo: "todo" + }]) +}) diff --git a/matrix/file.js b/matrix/file.js index 4578d4a..62a4550 100644 --- a/matrix/file.js +++ b/matrix/file.js @@ -74,7 +74,14 @@ function memberAvatar(guildID, user, member) { return `/guilds/${guildID}/users/${user.id}/avatars/${member.avatar}.png?size=${IMAGE_SIZE}` } +function emoji(emojiID, animated) { + const base = `/emojis/${emojiID}` + if (animated) return base + ".gif" + else return base + ".png" +} + module.exports.guildIcon = guildIcon module.exports.userAvatar = userAvatar module.exports.memberAvatar = memberAvatar +module.exports.emoji = emoji module.exports.uploadDiscordFileToMxc = uploadDiscordFileToMxc diff --git a/test/data.js b/test/data.js index e6a49c8..c94d132 100644 --- a/test/data.js +++ b/test/data.js @@ -6,18 +6,18 @@ module.exports = { channel: { general: { type: 0, - topic: 'https://docs.google.com/document/d/blah/edit | I spread, pipe, and whip because it is my will. :headstone:', + topic: "https://docs.google.com/document/d/blah/edit | I spread, pipe, and whip because it is my will. :headstone:", rate_limit_per_user: 0, position: 0, permission_overwrites: [], parent_id: null, nsfw: false, - name: 'collective-unconscious' , - last_pin_timestamp: '2023-04-06T09:51:57+00:00', - last_message_id: '1103832925784514580', - id: '112760669178241024', + name: "collective-unconscious" , + last_pin_timestamp: "2023-04-06T09:51:57+00:00", + last_message_id: "1103832925784514580", + id: "112760669178241024", default_thread_rate_limit_per_user: 0, - guild_id: '112760669178241024' + guild_id: "112760669178241024" } }, room: { @@ -45,41 +45,85 @@ module.exports = { }, guild: { general: { - owner_id: '112760500130975744', + owner_id: "112760500130975744", premium_tier: 3, stickers: [], max_members: 500000, - splash: '86a34ed02524b972918bef810087f8e7', + splash: "86a34ed02524b972918bef810087f8e7", explicit_content_filter: 0, afk_channel_id: null, nsfw_level: 0, description: null, - preferred_locale: 'en-US', - system_channel_id: '112760669178241024', + preferred_locale: "en-US", + system_channel_id: "112760669178241024", mfa_level: 0, /** @type {300} */ afk_timeout: 300, - id: '112760669178241024', - icon: 'a_f83622e09ead74f0c5c527fe241f8f8c', + id: "112760669178241024", + icon: "a_f83622e09ead74f0c5c527fe241f8f8c", emojis: [], premium_subscription_count: 14, roles: [], discovery_splash: null, default_message_notifications: 1, - region: 'deprecated', + region: "deprecated", max_video_channel_users: 25, verification_level: 0, application_id: null, premium_progress_bar_enabled: false, - banner: 'a_a666ae551605a2d8cda0afd591c0af3a', + banner: "a_a666ae551605a2d8cda0afd591c0af3a", features: [], vanity_url_code: null, hub_type: null, public_updates_channel_id: null, rules_channel_id: null, - name: 'Psychonauts 3', + name: "Psychonauts 3", max_stage_video_channel_users: 300, system_channel_flags: 0|0 } + }, + message: { + // Display order is text content, attachments, then stickers + 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 2c44bb3..ae6aea6 100644 --- a/test/test.js +++ b/test/test.js @@ -14,5 +14,6 @@ Object.assign(passthrough, { config, sync, db }) require("../matrix/kstate.test") require("../matrix/api.test") require("../matrix/read-registration.test") +require("../d2m/converters/message-to-event.test") require("../d2m/actions/create-room.test") require("../d2m/converters/user-to-mxid.test") From 512f61422d868235d0bf143df86157da14a5e6ed Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 15 May 2023 17:25:05 +1200 Subject: [PATCH 15/99] continue on stickers --- d2m/converters/message-to-event.test.js | 8 +++++++- matrix/file.js | 16 ++++++++++++++++ notes.md | 16 ++++++++++++++++ test/data.js | 13 ++++++++++++- 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index 07a45f7..323dc78 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -23,6 +23,12 @@ test("message2event: stickers", async t => { }, }, { $type: "m.sticker", - todo: "todo" + body: "pomu puff - damn that tiny lil bitch really chuffing. puffing that fat ass dart", + info: { + mimetype: "image/png" + // thumbnail_url + // thumbnail_info + }, + url: "mxc://" }]) }) diff --git a/matrix/file.js b/matrix/file.js index 62a4550..a373676 100644 --- a/matrix/file.js +++ b/matrix/file.js @@ -80,8 +80,24 @@ function emoji(emojiID, animated) { else return base + ".png" } +const stickerFormat = new Map([ + [1, {label: "PNG", ext: "png", mime: "image/png"}], + [2, {label: "APNG", ext: "png", mime: "image/apng"}], + [3, {label: "LOTTIE", ext: "json", mime: "application/json"}], + [4, {label: "GIF", ext: "gif", mime: "image/gif"}] +]) + +function sticker(sticker) { + const format = stickerFormat.get(sticker.format_type) + if (!format) throw new Error(`No such format ${sticker.format_type} for sticker ${JSON.stringify(sticker)}`) + const ext = format.ext + return `/stickers/${sticker.id}.${ext}` +} + module.exports.guildIcon = guildIcon module.exports.userAvatar = userAvatar module.exports.memberAvatar = memberAvatar module.exports.emoji = emoji +module.exports.stickerFormat = stickerFormat +module.exports.sticker = sticker module.exports.uploadDiscordFileToMxc = uploadDiscordFileToMxc diff --git a/notes.md b/notes.md index e63d9e5..3491682 100644 --- a/notes.md +++ b/notes.md @@ -60,6 +60,22 @@ The context-sensitive /invite command will invite Matrix users to the correspond # d2m events +## Login - backfill + +Need to backfill any messages that were missed while offline. + +After logging in, check last_message_id on each channel and compare against database to see if anything has been missed. However, mustn't interpret old channels from before the bridge was created as being "new". So, something has been missed if: + +- The last_message_id is not in the table of bridged messages +- The channel is already set up with a bridged room +- A message has been bridged in that channel before + +(If either of the last two conditions is false, that means the channel predates the bridge and we haven't actually missed anything there.) + +For channels that have missed messages, use the getChannelMessages function, and bridge each in turn. + +Can use custom transaction ID (?) to send the original timestamps to Matrix. See appservice docs for details. + ## Message sent 1. Transform content. diff --git a/test/data.js b/test/data.js index c94d132..9704c8d 100644 --- a/test/data.js +++ b/test/data.js @@ -47,7 +47,18 @@ module.exports = { general: { owner_id: "112760500130975744", premium_tier: 3, - stickers: [], + stickers: [{ + version: 1683838696974, + type: 2, + tags: "sunglasses", + name: "pomu puff", + id: "1106323941183717586", + guild_id: "112760669178241024", + format_type: 1, + description: "damn that tiny lil bitch really chuffing. puffing that fat ass dart", + available: true, + asset: "" + }], max_members: 500000, splash: "86a34ed02524b972918bef810087f8e7", explicit_content_filter: 0, From 584f19a01140ba1fde2ec35fd26aac28961a94a4 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 28 Jun 2023 23:38:58 +1200 Subject: [PATCH 16/99] fix a variety of bugs that snuck in --- d2m/actions/create-room.js | 6 ++++-- d2m/actions/send-message.js | 6 +++--- d2m/event-dispatcher.js | 2 +- matrix/api.js | 2 +- package-lock.json | 6 +++--- test/data.js | 2 +- 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 96a9671..c4baa2d 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -58,7 +58,7 @@ async function channelToKState(channel, guild) { "m.room.join_rules/": { join_rule: "restricted", allow: [{ - type: "m.room.membership", + type: "m.room_membership", room_id: spaceID }] } @@ -179,7 +179,9 @@ async function createAllForGuild(guildID) { const channelIDs = discord.guildChannelMap.get(guildID) assert.ok(channelIDs) for (const channelID of channelIDs) { - await syncRoom(channelID).then(r => console.log(`synced ${channelID}:`, r)) + if (discord.channels.get(channelID)?.type === DiscordTypes.ChannelType.GuildText) { // TODO: guild sync thread channels and such. maybe make a helper function to check if a given channel is syncable? + await syncRoom(channelID).then(r => console.log(`synced ${channelID}:`, r)) + } } } diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index 738c59a..897f7c0 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -15,14 +15,14 @@ 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 sendMessage(message) { - assert.ok(message.member) - +async function sendMessage(message, guild) { const roomID = await createRoom.ensureRoom(message.channel_id) let senderMxid = null if (!message.webhook_id) { + assert(message.member) senderMxid = await registerUser.ensureSimJoined(message.author, roomID) await registerUser.syncUser(message.author, message.member, message.guild_id, roomID) } diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index eeee451..baf19a8 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -24,7 +24,7 @@ module.exports = { * @param {import("discord-api-types/v10").GatewayMessageReactionAddDispatchData} data */ onReactionAdd(client, data) { - if (data.emoji.id !== null) return // TOOD: image emoji reactions + if (data.emoji.id !== null) return // TODO: image emoji reactions console.log(data) addReaction.addReaction(data) } diff --git a/matrix/api.js b/matrix/api.js index 846ea64..3ec014d 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -78,7 +78,7 @@ function getAllState(roomID) { async function sendState(roomID, type, stateKey, content, mxid) { console.log(`[api] state: ${roomID}: ${type}/${stateKey}`) assert.ok(type) - assert.ok(stateKey) + assert.ok(typeof stateKey === "string") /** @type {import("../types").R.EventSent} */ const root = await mreq.mreq("PUT", path(`/client/v3/rooms/${roomID}/state/${type}/${stateKey}`, mxid), content) return root.event_id diff --git a/package-lock.json b/package-lock.json index 9556331..224e232 100644 --- a/package-lock.json +++ b/package-lock.json @@ -478,9 +478,9 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/better-sqlite3": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-8.3.0.tgz", - "integrity": "sha512-JTmvBZL/JLTc+3Msbvq6gK6elbU9/wVMqiudplHrVJpr7sVMR9KJrNhZAbW+RhXKlpMcuEhYkdcHa3TXKNXQ1w==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-8.4.0.tgz", + "integrity": "sha512-NmsNW1CQvqMszu/CFAJ3pLct6NEFlNfuGM6vw72KHkjOD1UDnL96XNN1BMQc1hiHo8vE2GbOWQYIpZ+YM5wrZw==", "hasInstallScript": true, "dependencies": { "bindings": "^1.5.0", diff --git a/test/data.js b/test/data.js index 9704c8d..6a65b5f 100644 --- a/test/data.js +++ b/test/data.js @@ -33,7 +33,7 @@ module.exports = { "m.room.join_rules/": { join_rule: "restricted", allow: [{ - type: "m.room.membership", + type: "m.room_membership", room_id: "!jjWAGMeQdNrVZSSfvz:cadence.moe" }] }, From 740ddc36d12a222d84f52b913f1f6cf5bd0f664d Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 29 Jun 2023 00:06:56 +1200 Subject: [PATCH 17/99] support d->m stickers --- d2m/actions/create-room.js | 33 +++++++++++++++++++------ d2m/actions/send-message.js | 2 +- d2m/converters/message-to-event.js | 31 +++++++++++++++++++++-- d2m/converters/message-to-event.test.js | 4 +-- d2m/event-dispatcher.js | 8 +++--- matrix/file.js | 3 ++- test/data.js | 4 +-- 7 files changed, 67 insertions(+), 18 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index c4baa2d..479bb79 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -32,12 +32,13 @@ function applyKStateDiffToRoom(roomID, kstate) { } /** - * @param {import("discord-api-types/v10").APIGuildTextChannel} channel - * @param {import("discord-api-types/v10").APIGuild} guild + * @param {DiscordTypes.APIGuildTextChannel} channel + * @param {DiscordTypes.APIGuild} guild */ async function channelToKState(channel, guild) { const spaceID = db.prepare("SELECT space_id FROM guild_space WHERE guild_id = ?").pluck().get(guild.id) assert.ok(typeof spaceID === "string") + const customName = db.prepare("SELECT nick FROM channel_room WHERE channel_id = ?").pluck().get(channel.id) const avatarEventContent = {} if (guild.icon) { @@ -45,9 +46,27 @@ async function channelToKState(channel, guild) { avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) // TODO: somehow represent future values in kstate (callbacks?), while still allowing for diffing, so test cases don't need to touch the media API } + // TODO: Improve nasty nested ifs + let convertedName, convertedTopic + if (customName) { + convertedName = customName + if (channel.topic) { + convertedTopic = `${channel.name} | ${channel.topic}\n\nChannel ID: ${channel.id}\nGuild ID: ${guild.id}` + } else { + convertedTopic = `${channel.name}\n\nChannel ID: ${channel.id}\nGuild ID: ${guild.id}` + } + } else { + convertedName = channel.name + if (channel.topic) { + convertedTopic = `${channel.topic}\n\nChannel ID: ${channel.id}\nGuild ID: ${guild.id}` + } else { + convertedTopic = `Channel ID: ${channel.id}\nGuild ID: ${guild.id}` + } + } + const channelKState = { - "m.room.name/": {name: channel.name}, - "m.room.topic/": {$if: channel.topic, topic: channel.topic}, + "m.room.name/": {name: convertedName}, + "m.room.topic/": {topic: convertedTopic}, "m.room.avatar/": avatarEventContent, "m.room.guest_access/": {guest_access: "can_join"}, "m.room.history_visibility/": {history_visibility: "invited"}, @@ -69,7 +88,7 @@ async function channelToKState(channel, guild) { /** * Create a bridge room, store the relationship in the database, and add it to the guild's space. - * @param {import("discord-api-types/v10").APIGuildTextChannel} channel + * @param {DiscordTypes.APIGuildTextChannel} channel * @param guild * @param {string} spaceID * @param {any} kstate @@ -96,7 +115,7 @@ async function createRoom(channel, guild, spaceID, kstate) { } /** - * @param {import("discord-api-types/v10").APIGuildChannel} channel + * @param {DiscordTypes.APIGuildChannel} channel */ function channelToGuild(channel) { const guildID = channel.guild_id @@ -129,7 +148,7 @@ function channelToGuild(channel) { * @returns {Promise} room ID */ async function _syncRoom(channelID, shouldActuallySync) { - /** @ts-ignore @type {import("discord-api-types/v10").APIGuildChannel} */ + /** @ts-ignore @type {DiscordTypes.APIGuildChannel} */ const channel = discord.channels.get(channelID) assert.ok(channel) const guild = channelToGuild(channel) diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index 897f7c0..24a825a 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -27,7 +27,7 @@ async function sendMessage(message, guild) { await registerUser.syncUser(message.author, message.member, message.guild_id, roomID) } - const events = await messageToEvent.messageToEvent(message) + const events = await messageToEvent.messageToEvent(message, guild) const eventIDs = [] let eventPart = 0 // 0 is primary, 1 is supporting for (const event of events) { diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 86be8ac..549d104 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -1,16 +1,18 @@ // @ts-check +const assert = require("assert").strict const markdown = require("discord-markdown") const passthrough = require("../../passthrough") -const { sync, db } = passthrough +const { sync, db, discord } = passthrough /** @type {import("../../matrix/file")} */ const file = sync.require("../../matrix/file") /** * @param {import("discord-api-types/v10").APIMessage} message + * @param {import("discord-api-types/v10").APIGuild} guild */ -async function messageToEvent(message) { +async function messageToEvent(message, guild) { const events = [] // Text content appears first @@ -87,6 +89,31 @@ async function messageToEvent(message) { events.push(...attachmentEvents) // Then stickers + if (message.sticker_items) { + const stickerEvents = await Promise.all(message.sticker_items.map(async stickerItem => { + const format = file.stickerFormat.get(stickerItem.format_type) + if (format?.mime) { + let body = stickerItem.name + const sticker = guild.stickers.find(sticker => sticker.id === stickerItem.id) + if (sticker && sticker.description) body += ` - ${sticker.description}` + return { + $type: "m.sticker", + body, + info: { + mimetype: format.mime + }, + url: await file.uploadDiscordFileToMxc(file.sticker(stickerItem)) + } + } else { + return { + $type: "m.room.message", + msgtype: "m.text", + body: "Unsupported sticker format. Name: " + stickerItem.name + } + } + })) + events.push(...stickerEvents) + } return events } diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index 323dc78..c92cd85 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -4,7 +4,7 @@ const {messageToEvent} = require("./message-to-event") const data = require("../../test/data") test("message2event: stickers", async t => { - const events = await messageToEvent(data.message.sticker) + const events = await messageToEvent(data.message.sticker, data.guild.general) t.deepEqual(events, [{ $type: "m.room.message", msgtype: "m.text", @@ -29,6 +29,6 @@ test("message2event: stickers", async t => { // thumbnail_url // thumbnail_info }, - url: "mxc://" + url: "mxc://cadence.moe/UuUaLwXhkxFRwwWCXipDlBHn" }]) }) diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index baf19a8..fbcdca0 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -1,5 +1,4 @@ -// @ts-check - +const assert = require("assert").strict const {sync} = require("../passthrough") /** @type {import("./actions/send-message")}) */ @@ -15,8 +14,11 @@ module.exports = { * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message */ onMessageCreate(client, message) { + /** @ts-ignore @type {import("discord-api-types/v10").APIGuildChannel} */ + const channel = client.channels.get(message.channel_id) + const guild = client.guilds.get(channel.guild_id) if (message.guild_id !== "112760669178241024") return // TODO: activate on other servers (requires the space creation flow to be done first) - sendMessage.sendMessage(message) + sendMessage.sendMessage(message, guild) }, /** diff --git a/matrix/file.js b/matrix/file.js index a373676..64cd492 100644 --- a/matrix/file.js +++ b/matrix/file.js @@ -83,10 +83,11 @@ function emoji(emojiID, animated) { const stickerFormat = new Map([ [1, {label: "PNG", ext: "png", mime: "image/png"}], [2, {label: "APNG", ext: "png", mime: "image/apng"}], - [3, {label: "LOTTIE", ext: "json", mime: "application/json"}], + [3, {label: "LOTTIE", ext: "json", mime: null}], [4, {label: "GIF", ext: "gif", mime: "image/gif"}] ]) +/** @param {{id: string, format_type: number}} sticker */ function sticker(sticker) { const format = stickerFormat.get(sticker.format_type) if (!format) throw new Error(`No such format ${sticker.format_type} for sticker ${JSON.stringify(sticker)}`) diff --git a/test/data.js b/test/data.js index 6a65b5f..bb70cbe 100644 --- a/test/data.js +++ b/test/data.js @@ -22,8 +22,8 @@ module.exports = { }, room: { general: { - "m.room.name/": {name: "collective-unconscious"}, - "m.room.topic/": {topic: "https://docs.google.com/document/d/blah/edit | I spread, pipe, and whip because it is my will. :headstone:"}, + "m.room.name/": {name: "main"}, + "m.room.topic/": {topic: "collective-unconscious | https://docs.google.com/document/d/blah/edit | I spread, pipe, and whip because it is my will. :headstone:\n\nChannel ID: 112760669178241024\nGuild ID: 112760669178241024"}, "m.room.guest_access/": {guest_access: "can_join"}, "m.room.history_visibility/": {history_visibility: "invited"}, "m.space.parent/!jjWAGMeQdNrVZSSfvz:cadence.moe": { From d70199f890f546d3f8e5da2809708f6f6ee2f68b Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 29 Jun 2023 08:08:17 +1200 Subject: [PATCH 18/99] add appservice listener --- index.js | 7 ++++++- m2d/appservice.js | 8 ++++++++ m2d/event-dispatcher.js | 8 ++++++++ passthrough.js | 1 + 4 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 m2d/appservice.js create mode 100644 m2d/event-dispatcher.js diff --git a/index.js b/index.js index 807b49f..f4c59c6 100644 --- a/index.js +++ b/index.js @@ -9,13 +9,18 @@ const db = new sqlite("db/ooye.db") const sync = new HeatSync() -Object.assign(passthrough, { config, sync, db }) +Object.assign(passthrough, {config, sync, db}) const DiscordClient = require("./d2m/discord-client") const discord = new DiscordClient(config.discordToken) passthrough.discord = discord +const as = require("./m2d/appservice") +passthrough.as = as + +sync.require("./m2d/event-dispatcher") + ;(async () => { await discord.cloud.connect() console.log("Discord gateway started") diff --git a/m2d/appservice.js b/m2d/appservice.js new file mode 100644 index 0000000..e99a822 --- /dev/null +++ b/m2d/appservice.js @@ -0,0 +1,8 @@ +const reg = require("../matrix/read-registration") +const AppService = require("matrix-appservice").AppService +const as = new AppService({ + homeserverToken: reg.hs_token +}) +as.listen(+(new URL(reg.url).port)) + +module.exports = as diff --git a/m2d/event-dispatcher.js b/m2d/event-dispatcher.js new file mode 100644 index 0000000..136aae0 --- /dev/null +++ b/m2d/event-dispatcher.js @@ -0,0 +1,8 @@ +const assert = require("assert").strict +const {sync, as} = require("../passthrough") + +// Grab Matrix events we care about for the bridge, check them, and pass them on + +sync.addTemporaryListener(as, "type:m.room.message", event => { + console.log(event) +}) diff --git a/passthrough.js b/passthrough.js index 8ef75db..16de1bb 100644 --- a/passthrough.js +++ b/passthrough.js @@ -7,6 +7,7 @@ * @property {import("./d2m/discord-client")} discord * @property {import("heatsync")} sync * @property {import("better-sqlite3/lib/database")} db + * @property {import("matrix-appservice").AppService} as */ /** @type {Passthrough} */ // @ts-ignore From cc1210729f36b252f097abbcdde10868b7872367 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 30 Jun 2023 15:15:23 +1200 Subject: [PATCH 19/99] add test for member state content --- d2m/actions/register-user.js | 1 + d2m/actions/register-user.test.js | 24 +++++++++++++++++ test/data.js | 45 +++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 d2m/actions/register-user.test.js diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index cc5f515..1d1eb3d 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -154,6 +154,7 @@ async function syncAllUsersInRoom(roomID) { } } +module.exports._memberToStateContent = memberToStateContent module.exports.ensureSim = ensureSim module.exports.ensureSimJoined = ensureSimJoined module.exports.syncUser = syncUser diff --git a/d2m/actions/register-user.test.js b/d2m/actions/register-user.test.js new file mode 100644 index 0000000..7e23450 --- /dev/null +++ b/d2m/actions/register-user.test.js @@ -0,0 +1,24 @@ +const {channelToKState} = require("./create-room") +const {_memberToStateContent} = require("./register-user") +const {test} = require("supertape") +const testData = require("../../test/data") + +test("member2state: general", async t => { + t.deepEqual( + await _memberToStateContent(testData.member.sheep.user, testData.member.sheep, testData.guild.general.id), + { + avatar_url: "mxc://cadence.moe/rfemHmAtcprjLEiPiEuzPhpl", + displayname: "The Expert's Submarine | aprilsong", + membership: "join", + "moe.cadence.ooye.member": { + avatar: "/guilds/112760669178241024/users/134826546694193153/avatars/38dd359aa12bcd52dd3164126c587f8c.png?size=1024" + }, + "uk.half-shot.discord.member": { + bot: false, + displayColor: null, + id: "134826546694193153", + username: "@aprilsong" + } + } + ) +}) diff --git a/test/data.js b/test/data.js index bb70cbe..85b3cd4 100644 --- a/test/data.js +++ b/test/data.js @@ -93,6 +93,51 @@ module.exports = { system_channel_flags: 0|0 } }, + member: { + sheep: { + avatar: "38dd359aa12bcd52dd3164126c587f8c", + communication_disabled_until: null, + flags: 0, + joined_at: "2020-10-14T22:08:37.804000+00:00", + nick: "The Expert's Submarine", + pending: false, + premium_since: "2022-05-04T00:28:44.326000+00:00", + roles: [ + "112767366235959296", "118924814567211009", + "118923488755974146", "199995902742626304", + "204427286542417920", "217013981053845504", + "222168467627835392", "260993819204386816", + "265239342648131584", "271173313575780353", + "225744901915148298", "287733611912757249", + "318243902521868288", "348651574924541953", + "352291384021090304", "378402925128712193", + "392141548932038658", "393912152173576203", + "1123460940935991296", "872274377150980116", + "373336013109461013", "530220455085473813", + "768280323829137430", "842343433452257310", + "454567553738473472", "920107226528612383", + "1123528381514911745", "1040735082610167858", + "585531096071012409", "849737964090556488", + "660272211449479249" + ], + user: { + id: "134826546694193153", + username: "aprilsong", + avatar: "c754c120bce07ae3b3130e2b0e61d9dd", + discriminator: "0", + public_flags: 640, + flags: 640, + banner: "a3ad0693213f9dbf793b4159dbae0717", + accent_color: null, + global_name: "sheep", + avatar_decoration: null, + display_name: "sheep", + banner_color: null + }, + mute: false, + deaf: false + } + }, message: { // Display order is text content, attachments, then stickers sticker: { From 5bd1bc9a5bea0db9a2dbe2579c2c6a0b1fe783d2 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 30 Jun 2023 15:15:34 +1200 Subject: [PATCH 20/99] add webhook creation utility --- m2d/actions/register-webhook.js | 50 +++++++++++++++++++++++++++++++++ m2d/actions/send-message.js | 23 +++++++++++++++ test/test.js | 1 + types.d.ts | 5 ++++ 4 files changed, 79 insertions(+) create mode 100644 m2d/actions/register-webhook.js create mode 100644 m2d/actions/send-message.js diff --git a/m2d/actions/register-webhook.js b/m2d/actions/register-webhook.js new file mode 100644 index 0000000..511029b --- /dev/null +++ b/m2d/actions/register-webhook.js @@ -0,0 +1,50 @@ +// @ts-check + +const assert = require("assert").strict +const passthrough = require("../../passthrough") +const {discord, db} = passthrough + +/** + * Look in the database to find webhook credentials for a channel. + * (Note that the credentials may be invalid and need to be re-created if the webhook was interfered with from outside.) + * @param {string} channelID + * @param {boolean} forceCreate create a new webhook no matter what the database says about the state + * @returns id and token for a webhook for that channel + */ +async function ensureWebhook(channelID, forceCreate = false) { + if (!forceCreate) { + /** @type {{id: string, token: string} | null} */ + const row = db.prepare("SELECT webhook_id as id, webhook_token as token FROM webhook WHERE channel_id = ?").get(channelID) + if (row) { + return {created: false, ...row} + } + } + + // If we got here, we need to create a new webhook. + const webhook = await discord.snow.webhook.createWebhook(channelID, {name: "Out Of Your Element: Matrix Bridge"}) + assert(webhook.token) + db.prepare("REPLACE INTO webhook (channel_id, webhook_id, webhook_token) VALUES (?, ?, ?)").run(channelID, webhook.id, webhook.token) + return { + id: webhook.id, + token: webhook.token, + created: true + } +} + +/** + * @param {string} channelID + * @param {(webhook: import("../../types").WebhookCreds) => Promise} callback + * @returns Promise + * @template T + */ +async function withWebhook(channelID, callback) { + const webhook = await ensureWebhook(channelID, false) + return callback(webhook).catch(e => { + console.error(e) + // TODO: check if the error was webhook-related and if webhook.created === false, then: const webhook = ensureWebhook(channelID, true); return callback(webhook) + throw new Error(e) + }) +} + +module.exports.ensureWebhook = ensureWebhook +module.exports.withWebhook = withWebhook diff --git a/m2d/actions/send-message.js b/m2d/actions/send-message.js new file mode 100644 index 0000000..9c61107 --- /dev/null +++ b/m2d/actions/send-message.js @@ -0,0 +1,23 @@ +// @ts-check + +const assert = require("assert").strict +const DiscordTypes = require("discord-api-types/v10") +const passthrough = require("../../passthrough") +const {sync, discord, db} = passthrough + +/** @type {import("./register-webhook")} */ +const registerWebhook = sync.require("./register-webhook") + +/** + * @param {string} channelID + * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {name: string, file: Buffer}[]} data + */ +// param {DiscordTypes.RESTPostAPIWebhookWithTokenQuery & {wait: true, disableEveryone?: boolean}} options +async function sendMessage(channelID, data) { + const result = await registerWebhook.withWebhook(channelID, async webhook => { + return discord.snow.webhook.executeWebhook(webhook.id, webhook.token, data, {wait: true, disableEveryone: true}) + }) + return result +} + +module.exports.sendMessage = sendMessage diff --git a/test/test.js b/test/test.js index ae6aea6..5f06ae4 100644 --- a/test/test.js +++ b/test/test.js @@ -17,3 +17,4 @@ require("../matrix/read-registration.test") require("../d2m/converters/message-to-event.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 bc24329..aaa8db1 100644 --- a/types.d.ts +++ b/types.d.ts @@ -9,6 +9,11 @@ export type AppServiceRegistrationConfig = { rate_limited: boolean } +export type WebhookCreds = { + id: string + token: string +} + namespace Event { export type BaseStateEvent = { type: string From 07b9bab5eca3c3f3b070ef3d5df5a09d861d94cb Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 1 Jul 2023 17:02:03 +1200 Subject: [PATCH 21/99] updating discord libraries per recommendation --- package-lock.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 224e232..e4baa2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -679,11 +679,11 @@ } }, "node_modules/cloudstorm": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.7.0.tgz", - "integrity": "sha512-k+1u1kTdtlz3L6lnflAKMhkkZPoBl/2Du2czNvad2pYNOMBs8e0XZpSuCazC50Q29tzi08latn4SxtLbkws50A==", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.7.4.tgz", + "integrity": "sha512-EBeLY+VFP21lObPm3EHAhmtnch7irBD/AfRd6p+glXpYMU4vaCXzA+e7TI4UA2GCoN6LnX/skw4tk6GKyT0gIg==", "dependencies": { - "snowtransfer": "0.7.0" + "snowtransfer": "0.7.2" }, "engines": { "node": ">=12.0.0" @@ -917,9 +917,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.37.41", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.41.tgz", - "integrity": "sha512-FaPGBK9hx3zqSRX1x3KQWj+OElAJKmcyyfcdCy+U4AKv+gYuIkRySM7zd1So2sE4gc1DikkghkSBgBgKh6pe4Q==" + "version": "0.37.47", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.47.tgz", + "integrity": "sha512-rNif8IAv6duS2z47BMXq/V9kkrLfkAoiwpFY3sLxxbyKprk065zqf3HLTg4bEoxRSmi+Lhc7yqGDrG8C3j8GFA==" }, "node_modules/discord-markdown": { "version": "2.4.1", @@ -2458,12 +2458,12 @@ } }, "node_modules/snowtransfer": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.7.0.tgz", - "integrity": "sha512-vc7B46tO4QeK99z/pN8ISd8QvO9QB3Oo4qP7nYYhriIMOtjYkHMi8t6kUBPIJLbeX+h0NpfwxaGJfXNLm1ZQ5A==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.7.2.tgz", + "integrity": "sha512-EYFanl0T4yNul2WdBY/69+eo36eaKGkY4wEtMED+7wOFI70gdEsy4VQlToWb0WVm5/gvSiCMK4aLgA3EQGObAQ==", "dependencies": { "centra": "^2.6.0", - "discord-api-types": "^0.37.31", + "discord-api-types": "^0.37.46", "form-data": "^4.0.0" }, "engines": { From 1591bfc578ac69a19a76d421387d22ea2a865218 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 2 Jul 2023 01:40:54 +1200 Subject: [PATCH 22/99] fix webhooks sending (upstream bug) --- d2m/actions/create-space.js | 2 +- d2m/event-dispatcher.js | 2 +- package-lock.json | 59 ++++++++++++++++++++++++++----------- package.json | 4 +-- 4 files changed, 46 insertions(+), 21 deletions(-) diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index 4218c1f..e3b6da7 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -9,7 +9,7 @@ const api = sync.require("../../matrix/api") * @param {import("discord-api-types/v10").RESTGetAPIGuildResult} guild */ async function createSpace(guild) { - const roomID = api.createRoom({ + const roomID = await api.createRoom({ name: guild.name, preset: "private_chat", visibility: "private", diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index fbcdca0..1a1e30a 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -17,7 +17,7 @@ module.exports = { /** @ts-ignore @type {import("discord-api-types/v10").APIGuildChannel} */ const channel = client.channels.get(message.channel_id) const guild = client.guilds.get(channel.guild_id) - if (message.guild_id !== "112760669178241024") return // TODO: activate on other servers (requires the space creation flow to be done first) + if (message.guild_id !== "112760669178241024" && message.guild_id !== "497159726455455754") return // TODO: activate on other servers (requires the space creation flow to be done first) sendMessage.sendMessage(message, guild) }, diff --git a/package-lock.json b/package-lock.json index e4baa2e..fecb682 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "better-sqlite3": "^8.3.0", - "cloudstorm": "^0.7.0", + "cloudstorm": "^0.8.0", "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#24508e701e91d5a00fa5e773ced874d9ee8c889b", "heatsync": "^2.4.1", "js-yaml": "^4.1.0", @@ -18,7 +18,7 @@ "matrix-js-sdk": "^24.1.0", "mixin-deep": "^2.0.1", "node-fetch": "^2.6.7", - "snowtransfer": "^0.7.0", + "snowtransfer": "^0.8.0", "try-to-catch": "^3.0.1" }, "devDependencies": { @@ -605,6 +605,17 @@ "ieee754": "^1.2.1" } }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -625,11 +636,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/centra": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/centra/-/centra-2.6.0.tgz", - "integrity": "sha512-dgh+YleemrT8u85QL11Z6tYhegAs3MMxsaWAq/oXeAmYJ7VxL3SI9TZtnfaEvNDMAPolj25FXIb3S+HCI4wQaQ==" - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -679,11 +685,11 @@ } }, "node_modules/cloudstorm": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.7.4.tgz", - "integrity": "sha512-EBeLY+VFP21lObPm3EHAhmtnch7irBD/AfRd6p+glXpYMU4vaCXzA+e7TI4UA2GCoN6LnX/skw4tk6GKyT0gIg==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.8.0.tgz", + "integrity": "sha512-CT5/RKvSz1I0wmsf0SmZ2Jg9fPvqY67t9e2Y8n92vU0uEK5WmfPUyPOLZoYPMJwmktmsVCj4N6Pvka9gBIsY4g==", "dependencies": { - "snowtransfer": "0.7.2" + "snowtransfer": "0.8.0" }, "engines": { "node": ">=12.0.0" @@ -2458,13 +2464,13 @@ } }, "node_modules/snowtransfer": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.7.2.tgz", - "integrity": "sha512-EYFanl0T4yNul2WdBY/69+eo36eaKGkY4wEtMED+7wOFI70gdEsy4VQlToWb0WVm5/gvSiCMK4aLgA3EQGObAQ==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.8.0.tgz", + "integrity": "sha512-ang6qQsET4VX4u9mdZq6ynJvcm8HQfV6iZOHBh8Y3T0QkJLr6GAjzcv1et7BOXl1HDR/6NhD+j+ZGr8+imTclg==", "dependencies": { - "centra": "^2.6.0", - "discord-api-types": "^0.37.46", - "form-data": "^4.0.0" + "discord-api-types": "^0.37.47", + "form-data": "^4.0.0", + "undici": "^5.22.1" }, "engines": { "node": ">=12.0.0" @@ -2534,6 +2540,14 @@ "node": ">= 0.4" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2791,6 +2805,17 @@ "node": ">= 0.6" } }, + "node_modules/undici": { + "version": "5.22.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.1.tgz", + "integrity": "sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==", + "dependencies": { + "busboy": "^1.6.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/unhomoglyph": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/unhomoglyph/-/unhomoglyph-1.0.6.tgz", diff --git a/package.json b/package.json index f483842..b3e19eb 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "license": "MIT", "dependencies": { "better-sqlite3": "^8.3.0", - "cloudstorm": "^0.7.0", + "cloudstorm": "^0.8.0", "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#24508e701e91d5a00fa5e773ced874d9ee8c889b", "heatsync": "^2.4.1", "js-yaml": "^4.1.0", @@ -24,7 +24,7 @@ "matrix-js-sdk": "^24.1.0", "mixin-deep": "^2.0.1", "node-fetch": "^2.6.7", - "snowtransfer": "^0.7.0", + "snowtransfer": "^0.8.0", "try-to-catch": "^3.0.1" }, "devDependencies": { From d592a3c82eb8d771fe399dc0260784b1b198e293 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 2 Jul 2023 01:41:31 +1200 Subject: [PATCH 23/99] don't send "" for attachments without content --- d2m/converters/message-to-event.js | 84 +++++++++++++------------ d2m/converters/message-to-event.test.js | 17 +++++ matrix/api.js | 5 ++ stdin.js | 1 + test/data.js | 39 +++++++++++- 5 files changed, 104 insertions(+), 42 deletions(-) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 549d104..382e970 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -16,48 +16,50 @@ async function messageToEvent(message, guild) { const events = [] // Text content appears first - const body = message.content - const html = markdown.toHTML(body, { - discordCallback: { - user: node => { - const mxid = db.prepare("SELECT mxid FROM sim WHERE discord_id = ?").pluck().get(node.id) - if (mxid) { - return "https://matrix.to/#/" + mxid - } else { - return "@" + node.id - } - }, - channel: node => { - const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(node.id) - if (roomID) { - return "https://matrix.to/#/" + roomID - } else { - return "#" + node.id - } - }, - role: node => - "@&" + node.id, - everyone: node => - "@room", - here: node => - "@here" + if (message.content) { + const body = message.content + const html = markdown.toHTML(body, { + discordCallback: { + user: node => { + const mxid = db.prepare("SELECT mxid FROM sim WHERE discord_id = ?").pluck().get(node.id) + if (mxid) { + return "https://matrix.to/#/" + mxid + } else { + return "@" + node.id + } + }, + channel: node => { + const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(node.id) + if (roomID) { + return "https://matrix.to/#/" + roomID + } else { + return "#" + node.id + } + }, + role: node => + "@&" + node.id, + everyone: node => + "@room", + here: node => + "@here" + } + }, null, null) + const isPlaintext = body === html + if (isPlaintext) { + events.push({ + $type: "m.room.message", + msgtype: "m.text", + body: body + }) + } else { + events.push({ + $type: "m.room.message", + msgtype: "m.text", + body: body, + format: "org.matrix.custom.html", + formatted_body: html + }) } - }, null, null) - const isPlaintext = body === html - if (isPlaintext) { - events.push({ - $type: "m.room.message", - msgtype: "m.text", - body: body - }) - } else { - events.push({ - $type: "m.room.message", - msgtype: "m.text", - body: body, - format: "org.matrix.custom.html", - formatted_body: html - }) } // Then attachments diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index c92cd85..c318389 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -3,6 +3,23 @@ const assert = require("assert") const {messageToEvent} = require("./message-to-event") const data = require("../../test/data") +test("message2event: attachment with no content", async t => { + const events = await messageToEvent(data.message.attachment_no_content, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.image", + url: "mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM", + body: "image.png", + external_url: "https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png", + info: { + mimetype: "image/png", + w: 466, + h: 85, + size: 12919, + }, + }]) +}) + test("message2event: stickers", async t => { const events = await messageToEvent(data.message.sticker, data.guild.general) t.deepEqual(events, [{ diff --git a/matrix/api.js b/matrix/api.js index 3ec014d..ec85795 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -60,6 +60,10 @@ async function inviteToRoom(roomID, mxidToInvite, mxid) { }) } +async function leaveRoom(roomID, mxid) { + await mreq.mreq("POST", path(`/client/v3/rooms/${roomID}/leave`, mxid), {}) +} + /** * @param {string} roomID * @returns {Promise} @@ -108,6 +112,7 @@ module.exports.register = register module.exports.createRoom = createRoom module.exports.joinRoom = joinRoom module.exports.inviteToRoom = inviteToRoom +module.exports.leaveRoom = leaveRoom module.exports.getAllState = getAllState module.exports.sendState = sendState module.exports.sendEvent = sendEvent diff --git a/stdin.js b/stdin.js index 99345ab..1a5b8d1 100644 --- a/stdin.js +++ b/stdin.js @@ -11,6 +11,7 @@ const createRoom = sync.require("./d2m/actions/create-room") const registerUser = sync.require("./d2m/actions/register-user") const mreq = sync.require("./matrix/mreq") const api = sync.require("./matrix/api") +const sendMessage = sync.require("./m2d/actions/send-message") const guildID = "112760669178241024" const extraContext = {} diff --git a/test/data.js b/test/data.js index 85b3cd4..5efea36 100644 --- a/test/data.js +++ b/test/data.js @@ -140,6 +140,43 @@ module.exports = { }, message: { // Display order is text content, attachments, then stickers + attachment_no_content: { + id: "1124628646670389348", + type: 0, + content: "", + channel_id: "497161332244742154", + author: { + id: "320067006521147393", + username: "papiophidian", + global_name: "PapiOphidian", + avatar: "fb2b4535f7a108619e3edae12fcb16c5", + discriminator: "0", + public_flags: 4194880, + avatar_decoration: null + }, + attachments: [ + { + id: "1124628646431297546", + filename: "image.png", + size: 12919, + url: "https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png", + proxy_url: "https://media.discordapp.net/attachments/497161332244742154/1124628646431297546/image.png", + width: 466, + height: 85, + content_type: "image/png" + } + ], + embeds: [], + mentions: [], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2023-07-01T09:12:43.956000+00:00", + edited_timestamp: null, + flags: 0, + components: [] + }, sticker: { id: "1106366167788044450", type: 0, @@ -180,6 +217,6 @@ module.exports = { format_type: 1, name: "pomu puff" }] - } + } } } From 6e55e6d1b358707821f08b251940c2385757b204 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 3 Jul 2023 01:06:05 +1200 Subject: [PATCH 24/99] preparing for m2d --- m2d/actions/send-message.js | 2 +- m2d/converters/event-to-message.js | 30 +++++++++++++++++++++++ m2d/converters/event-to-message.test.js | 32 +++++++++++++++++++++++++ m2d/event-dispatcher.js | 4 ++++ test/test.js | 1 + types.d.ts | 12 ++++++++++ 6 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 m2d/converters/event-to-message.js create mode 100644 m2d/converters/event-to-message.test.js diff --git a/m2d/actions/send-message.js b/m2d/actions/send-message.js index 9c61107..3505b60 100644 --- a/m2d/actions/send-message.js +++ b/m2d/actions/send-message.js @@ -10,7 +10,7 @@ const registerWebhook = sync.require("./register-webhook") /** * @param {string} channelID - * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {name: string, file: Buffer}[]} data + * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]}[]} data */ // param {DiscordTypes.RESTPostAPIWebhookWithTokenQuery & {wait: true, disableEveryone?: boolean}} options async function sendMessage(channelID, data) { diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js new file mode 100644 index 0000000..f48b5da --- /dev/null +++ b/m2d/converters/event-to-message.js @@ -0,0 +1,30 @@ +// @ts-check + +const assert = require("assert").strict +const DiscordTypes = require("discord-api-types/v10") +const markdown = require("discord-markdown") + +const passthrough = require("../../passthrough") +const { sync, db, discord } = passthrough +/** @type {import("../../matrix/file")} */ +const file = sync.require("../../matrix/file") + +/** + * @param {import("../../types").Event.Outer} event + */ +function eventToMessage(event) { + /** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]})[]} */ + const messages = [] + + if (event.content.msgtype === "m.text") { + messages.push({ + content: event.content.body, + username: event.sender.replace(/^@/, ""), + avatar_url: undefined, // TODO: provide the URL to the avatar from the homeserver's content repo + }) + } + + return messages +} + +module.exports.eventToMessage = eventToMessage diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js new file mode 100644 index 0000000..a41beef --- /dev/null +++ b/m2d/converters/event-to-message.test.js @@ -0,0 +1,32 @@ +// @ts-check + +const {test} = require("supertape") +const assert = require("assert") +const {eventToMessage} = require("./event-to-message") +const data = require("../../test/data") + +test("event2message: janky test", t => { + t.deepEqual( + eventToMessage({ + age: 405299, + content: { + body: "test", + msgtype: "m.text" + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + }, + user_id: "@cadence:cadence.moe" + }), + [{ + username: "cadence:cadence.moe", + content: "test", + avatar_url: undefined + }] + ) +}) diff --git a/m2d/event-dispatcher.js b/m2d/event-dispatcher.js index 136aae0..789d9ed 100644 --- a/m2d/event-dispatcher.js +++ b/m2d/event-dispatcher.js @@ -1,3 +1,5 @@ +// @ts-check + const assert = require("assert").strict const {sync, as} = require("../passthrough") @@ -5,4 +7,6 @@ const {sync, as} = require("../passthrough") sync.addTemporaryListener(as, "type:m.room.message", event => { console.log(event) + // TODO: filter out events that were bridged discord messages (i.e. sent by OOYE) + // TODO: call sendMessage }) diff --git a/test/test.js b/test/test.js index 5f06ae4..f2f0912 100644 --- a/test/test.js +++ b/test/test.js @@ -18,3 +18,4 @@ require("../d2m/converters/message-to-event.test") require("../d2m/actions/create-room.test") require("../d2m/converters/user-to-mxid.test") require("../d2m/actions/register-user.test") +require("../m2d/converters/event-to-message.test") diff --git a/types.d.ts b/types.d.ts index aaa8db1..2181793 100644 --- a/types.d.ts +++ b/types.d.ts @@ -15,6 +15,18 @@ export type WebhookCreds = { } namespace Event { + export type Outer = { + type: string + room_id: string + sender: string + content: T + origin_server_ts: number + unsigned: any + event_id: string + user_id: string + age: number + } + export type BaseStateEvent = { type: string room_id: string From 3578ca28b5e43fd8aeddf1086cd4f49bc8cecb7e Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 3 Jul 2023 17:20:24 +1200 Subject: [PATCH 25/99] glue --- ...register-webhook.js => channel-webhook.js} | 13 ++++++ m2d/actions/send-event.js | 43 +++++++++++++++++++ m2d/actions/send-message.js | 23 ---------- m2d/event-dispatcher.js | 31 +++++++++++-- types.d.ts | 12 +++++- 5 files changed, 94 insertions(+), 28 deletions(-) rename m2d/actions/{register-webhook.js => channel-webhook.js} (77%) create mode 100644 m2d/actions/send-event.js delete mode 100644 m2d/actions/send-message.js diff --git a/m2d/actions/register-webhook.js b/m2d/actions/channel-webhook.js similarity index 77% rename from m2d/actions/register-webhook.js rename to m2d/actions/channel-webhook.js index 511029b..5e56859 100644 --- a/m2d/actions/register-webhook.js +++ b/m2d/actions/channel-webhook.js @@ -1,6 +1,7 @@ // @ts-check const assert = require("assert").strict +const DiscordTypes = require("discord-api-types/v10") const passthrough = require("../../passthrough") const {discord, db} = passthrough @@ -46,5 +47,17 @@ async function withWebhook(channelID, callback) { }) } +/** + * @param {string} channelID + * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]}[]} data + */ +async function sendMessageWithWebhook(channelID, data) { + const result = await withWebhook(channelID, async webhook => { + return discord.snow.webhook.executeWebhook(webhook.id, webhook.token, data, {wait: true, disableEveryone: true}) + }) + return result +} + module.exports.ensureWebhook = ensureWebhook module.exports.withWebhook = withWebhook +module.exports.sendMessageWithWebhook = sendMessageWithWebhook diff --git a/m2d/actions/send-event.js b/m2d/actions/send-event.js new file mode 100644 index 0000000..5eb8c04 --- /dev/null +++ b/m2d/actions/send-event.js @@ -0,0 +1,43 @@ +// @ts-check + +const assert = require("assert").strict +const DiscordTypes = require("discord-api-types/v10") +const passthrough = require("../../passthrough") +const {sync, discord, db} = passthrough + +/** @type {import("./channel-webhook")} */ +const channelWebhook = sync.require("./channel-webhook") +/** @type {import("../converters/event-to-message")} */ +const eventToMessage = sync.require("../converters/event-to-message") + +/** @param {import("../../types").Event.Outer} event */ +async function sendEvent(event) { + // TODO: matrix equivalents... + const roomID = await createRoom.ensureRoom(message.channel_id) + // TODO: no need to sync the member to the other side... right? + let senderMxid = null + if (!message.webhook_id) { + assert(message.member) + senderMxid = await registerUser.ensureSimJoined(message.author, roomID) + await registerUser.syncUser(message.author, message.member, message.guild_id, roomID) + } + + const messages = eventToMessage.eventToMessage(event) + assert(Array.isArray(messages)) + + /** @type {DiscordTypes.APIMessage[]} */ + const messageResponses = [] + let eventPart = 0 // 0 is primary, 1 is supporting + for (const message of messages) { + const messageResponse = await channelWebhook.sendMessageWithWebhook(channelID, message) + // TODO: are you sure about that? many to many? and we don't need to store which side it originated from? + db.prepare("INSERT INTO event_message (event_id, message_id, part) VALUES (?, ?, ?)").run(event.event_id, messageResponse.id, eventPart) + + eventPart = 1 // TODO: use more intelligent algorithm to determine whether primary or supporting + messageResponses.push(messageResponse) + } + + return messageResponses +} + +module.exports.sendEvent = sendEvent diff --git a/m2d/actions/send-message.js b/m2d/actions/send-message.js deleted file mode 100644 index 3505b60..0000000 --- a/m2d/actions/send-message.js +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-check - -const assert = require("assert").strict -const DiscordTypes = require("discord-api-types/v10") -const passthrough = require("../../passthrough") -const {sync, discord, db} = passthrough - -/** @type {import("./register-webhook")} */ -const registerWebhook = sync.require("./register-webhook") - -/** - * @param {string} channelID - * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]}[]} data - */ -// param {DiscordTypes.RESTPostAPIWebhookWithTokenQuery & {wait: true, disableEveryone?: boolean}} options -async function sendMessage(channelID, data) { - const result = await registerWebhook.withWebhook(channelID, async webhook => { - return discord.snow.webhook.executeWebhook(webhook.id, webhook.token, data, {wait: true, disableEveryone: true}) - }) - return result -} - -module.exports.sendMessage = sendMessage diff --git a/m2d/event-dispatcher.js b/m2d/event-dispatcher.js index 789d9ed..b8bfacf 100644 --- a/m2d/event-dispatcher.js +++ b/m2d/event-dispatcher.js @@ -1,12 +1,37 @@ // @ts-check +/** + * Grab Matrix events we care about, check them, and bridge them. + */ + const assert = require("assert").strict const {sync, as} = require("../passthrough") +const reg = require("../matrix/read-registration") +/** @type {import("./actions/send-event")} */ +const sendEvent = sync.require("./actions/send-event") -// Grab Matrix events we care about for the bridge, check them, and pass them on +const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex)) +/** + * Determine whether an event is the bridged representation of a discord message. + * Such messages shouldn't be bridged again. + * @param {import("../types").Event.Outer} event + */ +function eventOriginatedFromDiscord(event) { + if ( + // If it's from a user in the bridge's namespace... + userRegex.some(x => event.sender.match(x)) + // ...not counting the appservice's own user... + && !event.sender.startsWith(`@${reg.sender_localpart}:`) + ) { + // ...then it originated from discord + return true + } + + return false +} sync.addTemporaryListener(as, "type:m.room.message", event => { console.log(event) - // TODO: filter out events that were bridged discord messages (i.e. sent by OOYE) - // TODO: call sendMessage + if (eventOriginatedFromDiscord(event)) return + const messageResponses = sendEvent.sendEvent(event) }) diff --git a/types.d.ts b/types.d.ts index 2181793..19ef1f2 100644 --- a/types.d.ts +++ b/types.d.ts @@ -5,6 +5,16 @@ export type AppServiceRegistrationConfig = { url: string sender_localpart: string namespace_prefix: string + namespaces: { + users: { + exclusive: boolean + regex: string + }[] + aliases: { + exclusive: boolean + regex: string + }[] + } protocols: [string] rate_limited: boolean } @@ -23,8 +33,6 @@ namespace Event { origin_server_ts: number unsigned: any event_id: string - user_id: string - age: number } export type BaseStateEvent = { From 39cdba9f906b469d7264a64dcd2976d54810388b Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 4 Jul 2023 00:39:42 +1200 Subject: [PATCH 26/99] bridge both ways and prevent reflections --- d2m/actions/add-reaction.js | 1 - d2m/actions/send-message.js | 2 +- d2m/converters/message-to-event.test.js | 1 - d2m/event-dispatcher.js | 9 ++++++- m2d/actions/channel-webhook.js | 2 +- m2d/actions/send-event.js | 20 ++++++--------- m2d/converters/event-to-message.js | 1 - m2d/converters/event-to-message.test.js | 5 +--- m2d/converters/utils.js | 21 ++++++++++++++++ m2d/converters/utils.test.js | 16 ++++++++++++ m2d/event-dispatcher.js | 33 +++++++------------------ matrix/api.test.js | 1 - matrix/read-registration.test.js | 3 +-- stdin.js | 2 +- test/test.js | 1 + 15 files changed, 67 insertions(+), 51 deletions(-) create mode 100644 m2d/converters/utils.js create mode 100644 m2d/converters/utils.test.js diff --git a/d2m/actions/add-reaction.js b/d2m/actions/add-reaction.js index 82449cd..cd3d296 100644 --- a/d2m/actions/add-reaction.js +++ b/d2m/actions/add-reaction.js @@ -17,7 +17,6 @@ const createRoom = sync.require("../actions/create-room") async function addReaction(data) { const user = data.member?.user assert.ok(user && user.username) - // TODO: should add my own sent messages to event_message so they can be reacted to? const parentID = db.prepare("SELECT event_id FROM event_message WHERE message_id = ? AND part = 0").pluck().get(data.message_id) // 0 = primary if (!parentID) return // TODO: how to handle reactions for unbridged messages? is there anything I can do? assert.equal(typeof parentID, "string") diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index 24a825a..4f111b0 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, part) VALUES (?, ?, ?)").run(eventID, message.id, eventPart) + db.prepare("INSERT INTO event_message (event_id, message_id, part, source) VALUES (?, ?, ?, 1)").run(eventID, message.id, eventPart) // source 1 = discord eventPart = 1 // TODO: use more intelligent algorithm to determine whether primary or supporting eventIDs.push(eventID) diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index c318389..26cf1f1 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -1,5 +1,4 @@ const {test} = require("supertape") -const assert = require("assert") const {messageToEvent} = require("./message-to-event") const data = require("../../test/data") diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 1a1e30a..99c7792 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -1,5 +1,5 @@ const assert = require("assert").strict -const {sync} = require("../passthrough") +const {sync, db} = require("../passthrough") /** @type {import("./actions/send-message")}) */ const sendMessage = sync.require("./actions/send-message") @@ -18,6 +18,13 @@ module.exports = { const channel = client.channels.get(message.channel_id) const guild = client.guilds.get(channel.guild_id) if (message.guild_id !== "112760669178241024" && message.guild_id !== "497159726455455754") return // TODO: activate on other servers (requires the space creation flow to be done first) + if (message.webhook_id) { + const row = db.prepare("SELECT webhook_id FROM webhook WHERE webhook_id = ?").pluck().get(message.webhook_id) + if (row) { + // The message was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it. + return + } + } sendMessage.sendMessage(message, guild) }, diff --git a/m2d/actions/channel-webhook.js b/m2d/actions/channel-webhook.js index 5e56859..b62057b 100644 --- a/m2d/actions/channel-webhook.js +++ b/m2d/actions/channel-webhook.js @@ -49,7 +49,7 @@ async function withWebhook(channelID, callback) { /** * @param {string} channelID - * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]}[]} data + * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]}} data */ async function sendMessageWithWebhook(channelID, data) { const result = await withWebhook(channelID, async webhook => { diff --git a/m2d/actions/send-event.js b/m2d/actions/send-event.js index 5eb8c04..56a660f 100644 --- a/m2d/actions/send-event.js +++ b/m2d/actions/send-event.js @@ -12,28 +12,22 @@ const eventToMessage = sync.require("../converters/event-to-message") /** @param {import("../../types").Event.Outer} event */ async function sendEvent(event) { - // TODO: matrix equivalents... - const roomID = await createRoom.ensureRoom(message.channel_id) - // TODO: no need to sync the member to the other side... right? - let senderMxid = null - if (!message.webhook_id) { - assert(message.member) - senderMxid = await registerUser.ensureSimJoined(message.author, roomID) - await registerUser.syncUser(message.author, message.member, message.guild_id, roomID) - } + // TODO: we just assume the bridge has already been created + const channelID = db.prepare("SELECT channel_id FROM channel_room WHERE room_id = ?").pluck().get(event.room_id) + + // no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it const messages = eventToMessage.eventToMessage(event) - assert(Array.isArray(messages)) + assert(Array.isArray(messages)) // sanity /** @type {DiscordTypes.APIMessage[]} */ const messageResponses = [] let eventPart = 0 // 0 is primary, 1 is supporting for (const message of messages) { const messageResponse = await channelWebhook.sendMessageWithWebhook(channelID, message) - // TODO: are you sure about that? many to many? and we don't need to store which side it originated from? - db.prepare("INSERT INTO event_message (event_id, message_id, part) VALUES (?, ?, ?)").run(event.event_id, messageResponse.id, eventPart) + db.prepare("INSERT INTO event_message (event_id, message_id, part, source) VALUES (?, ?, ?, 0)").run(event.event_id, messageResponse.id, eventPart) // source 0 = matrix - eventPart = 1 // TODO: use more intelligent algorithm to determine whether primary or supporting + eventPart = 1 // TODO: use more intelligent algorithm to determine whether primary or supporting? messageResponses.push(messageResponse) } diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index f48b5da..8b41903 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -1,6 +1,5 @@ // @ts-check -const assert = require("assert").strict const DiscordTypes = require("discord-api-types/v10") const markdown = require("discord-markdown") diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index a41beef..e687059 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -1,14 +1,12 @@ // @ts-check const {test} = require("supertape") -const assert = require("assert") const {eventToMessage} = require("./event-to-message") const data = require("../../test/data") test("event2message: janky test", t => { t.deepEqual( eventToMessage({ - age: 405299, content: { body: "test", msgtype: "m.text" @@ -20,8 +18,7 @@ test("event2message: janky test", t => { type: "m.room.message", unsigned: { age: 405299 - }, - user_id: "@cadence:cadence.moe" + } }), [{ username: "cadence:cadence.moe", diff --git a/m2d/converters/utils.js b/m2d/converters/utils.js new file mode 100644 index 0000000..108da1f --- /dev/null +++ b/m2d/converters/utils.js @@ -0,0 +1,21 @@ +// @ts-check + +const reg = require("../../matrix/read-registration") +const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex)) +/** + * Determine whether an event is the bridged representation of a discord message. + * Such messages shouldn't be bridged again. + * @param {string} sender + */ +function eventSenderIsFromDiscord(sender) { + // If it's from a user in the bridge's namespace, then it originated from discord + // This includes messages sent by the appservice's bot user, because that is what's used for webhooks + // TODO: It would be nice if bridge system messages wouldn't trigger this check and could be bridged from matrix to discord, while webhook reflections would remain ignored... + if (userRegex.some(x => sender.match(x))) { + return true + } + + return false +} + +module.exports.eventSenderIsFromDiscord = eventSenderIsFromDiscord diff --git a/m2d/converters/utils.test.js b/m2d/converters/utils.test.js new file mode 100644 index 0000000..ae3159e --- /dev/null +++ b/m2d/converters/utils.test.js @@ -0,0 +1,16 @@ +// @ts-check + +const {test} = require("supertape") +const {eventSenderIsFromDiscord} = require("./utils") + +test("sender type: matrix user", t => { + t.notOk(eventSenderIsFromDiscord("@cadence:cadence.moe")) +}) + +test("sender type: ooye bot", t => { + t.ok(eventSenderIsFromDiscord("@_ooye_bot:cadence.moe")) +}) + +test("sender type: ooye puppet", t => { + t.ok(eventSenderIsFromDiscord("@_ooye_sheep:cadence.moe")) +}) diff --git a/m2d/event-dispatcher.js b/m2d/event-dispatcher.js index b8bfacf..01a3dcc 100644 --- a/m2d/event-dispatcher.js +++ b/m2d/event-dispatcher.js @@ -4,34 +4,19 @@ * Grab Matrix events we care about, check them, and bridge them. */ -const assert = require("assert").strict const {sync, as} = require("../passthrough") -const reg = require("../matrix/read-registration") + /** @type {import("./actions/send-event")} */ const sendEvent = sync.require("./actions/send-event") +/** @type {import("./converters/utils")} */ +const utils = sync.require("./converters/utils") -const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex)) + +sync.addTemporaryListener(as, "type:m.room.message", /** - * Determine whether an event is the bridged representation of a discord message. - * Such messages shouldn't be bridged again. - * @param {import("../types").Event.Outer} event + * @param {import("../types").Event.Outer} event it is a m.room.message because that's what this listener is filtering for */ -function eventOriginatedFromDiscord(event) { - if ( - // If it's from a user in the bridge's namespace... - userRegex.some(x => event.sender.match(x)) - // ...not counting the appservice's own user... - && !event.sender.startsWith(`@${reg.sender_localpart}:`) - ) { - // ...then it originated from discord - return true - } - - return false -} - -sync.addTemporaryListener(as, "type:m.room.message", event => { - console.log(event) - if (eventOriginatedFromDiscord(event)) return - const messageResponses = sendEvent.sendEvent(event) +async event => { + if (utils.eventSenderIsFromDiscord(event.sender)) return + const messageResponses = await sendEvent.sendEvent(event) }) diff --git a/matrix/api.test.js b/matrix/api.test.js index f54c665..6c74e50 100644 --- a/matrix/api.test.js +++ b/matrix/api.test.js @@ -1,5 +1,4 @@ const {test} = require("supertape") -const assert = require("assert") const {path} = require("./api") test("api path: no change for plain path", t => { diff --git a/matrix/read-registration.test.js b/matrix/read-registration.test.js index 9c7f828..c5b3ac8 100644 --- a/matrix/read-registration.test.js +++ b/matrix/read-registration.test.js @@ -1,5 +1,4 @@ const {test} = require("supertape") -const assert = require("assert") const reg = require("./read-registration") test("reg: has necessary parameters", t => { @@ -8,4 +7,4 @@ test("reg: has necessary parameters", t => { propertiesToCheck.filter(p => p in reg), propertiesToCheck ) -}) \ No newline at end of file +}) diff --git a/stdin.js b/stdin.js index 1a5b8d1..cd504f2 100644 --- a/stdin.js +++ b/stdin.js @@ -11,7 +11,7 @@ const createRoom = sync.require("./d2m/actions/create-room") const registerUser = sync.require("./d2m/actions/register-user") const mreq = sync.require("./matrix/mreq") const api = sync.require("./matrix/api") -const sendMessage = sync.require("./m2d/actions/send-message") +const sendEvent = sync.require("./m2d/actions/send-event") const guildID = "112760669178241024" const extraContext = {} diff --git a/test/test.js b/test/test.js index f2f0912..5805d09 100644 --- a/test/test.js +++ b/test/test.js @@ -19,3 +19,4 @@ require("../d2m/actions/create-room.test") require("../d2m/converters/user-to-mxid.test") require("../d2m/actions/register-user.test") require("../m2d/converters/event-to-message.test") +require("../m2d/converters/utils.test") From 61120d92c6b1cadc66e5b1433a9804b55689fc98 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 4 Jul 2023 17:19:17 +1200 Subject: [PATCH 27/99] m2d reactions (untested) --- d2m/actions/add-reaction.js | 4 ++-- m2d/actions/add-reaction.js | 25 +++++++++++++++++++++++++ m2d/converters/event-to-message.js | 3 ++- m2d/event-dispatcher.js | 15 +++++++++++++-- matrix/api.js | 13 +++++++------ types.d.ts | 8 ++++++++ types.js | 1 + 7 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 m2d/actions/add-reaction.js create mode 100644 types.js diff --git a/d2m/actions/add-reaction.js b/d2m/actions/add-reaction.js index cd3d296..49afca9 100644 --- a/d2m/actions/add-reaction.js +++ b/d2m/actions/add-reaction.js @@ -1,6 +1,6 @@ // @ts-check -const assert = require("assert") +const assert = require("assert").strict const passthrough = require("../../passthrough") const { discord, sync, db } = passthrough @@ -18,7 +18,7 @@ async function addReaction(data) { const user = data.member?.user assert.ok(user && user.username) const parentID = db.prepare("SELECT event_id FROM event_message WHERE message_id = ? AND part = 0").pluck().get(data.message_id) // 0 = primary - if (!parentID) return // TODO: how to handle reactions for unbridged messages? is there anything I can do? + if (!parentID) return // Nothing can be done if the parent message was never bridged. assert.equal(typeof parentID, "string") const roomID = await createRoom.ensureRoom(data.channel_id) const senderMxid = await registerUser.ensureSimJoined(user, roomID) diff --git a/m2d/actions/add-reaction.js b/m2d/actions/add-reaction.js new file mode 100644 index 0000000..342550d --- /dev/null +++ b/m2d/actions/add-reaction.js @@ -0,0 +1,25 @@ +// @ts-check + +const assert = require("assert").strict +const Ty = require("../../types") + +const passthrough = require("../../passthrough") +const { discord, sync, db } = passthrough + +/** + * @param {Ty.Event.Outer} event + */ +async function addReaction(event) { + const channelID = db.prepare("SELECT channel_id FROM channel_room WHERE room_id = ?").pluck().get(event.room_id) + if (!channelID) return // We just assume the bridge has already been created + const messageID = db.prepare("SELECT message_id FROM event_message WHERE event_id = ? AND part = 0").pluck().get(event.content["m.relates_to"].event_id) // 0 = primary + if (!messageID) return // Nothing can be done if the parent message was never bridged. + + // no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it + + const emoji = event.content["m.relates_to"].key // TODO: handle custom text or emoji reactions + + return discord.snow.channel.createReaction(channelID, messageID, emoji) +} + +module.exports.addReaction = addReaction diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index 8b41903..817ffff 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -1,5 +1,6 @@ // @ts-check +const Ty = require("../../types") const DiscordTypes = require("discord-api-types/v10") const markdown = require("discord-markdown") @@ -9,7 +10,7 @@ const { sync, db, discord } = passthrough const file = sync.require("../../matrix/file") /** - * @param {import("../../types").Event.Outer} event + * @param {Ty.Event.Outer} event */ function eventToMessage(event) { /** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]})[]} */ diff --git a/m2d/event-dispatcher.js b/m2d/event-dispatcher.js index 01a3dcc..82ebd75 100644 --- a/m2d/event-dispatcher.js +++ b/m2d/event-dispatcher.js @@ -4,19 +4,30 @@ * Grab Matrix events we care about, check them, and bridge them. */ +const Ty = require("../types") const {sync, as} = require("../passthrough") /** @type {import("./actions/send-event")} */ const sendEvent = sync.require("./actions/send-event") +/** @type {import("./actions/add-reaction")} */ +const addReaction = sync.require("./actions/add-reaction") /** @type {import("./converters/utils")} */ const utils = sync.require("./converters/utils") - sync.addTemporaryListener(as, "type:m.room.message", /** - * @param {import("../types").Event.Outer} event it is a m.room.message because that's what this listener is filtering for + * @param {Ty.Event.Outer} event it is a m.room.message because that's what this listener is filtering for */ async event => { if (utils.eventSenderIsFromDiscord(event.sender)) return const messageResponses = await sendEvent.sendEvent(event) }) + +sync.addTemporaryListener(as, "type:m.reaction", +/** + * @param {Ty.Event.Outer} event it is a m.reaction because that's what this listener is filtering for + */ +async event => { + if (utils.eventSenderIsFromDiscord(event.sender)) return + await addReaction.addReaction(event) +}) diff --git a/matrix/api.js b/matrix/api.js index ec85795..cf22933 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -1,5 +1,6 @@ // @ts-check +const Ty = require("../types") const assert = require("assert") const passthrough = require("../passthrough") @@ -25,7 +26,7 @@ function path(p, mxid) { /** * @param {string} username - * @returns {Promise} + * @returns {Promise} */ function register(username) { console.log(`[api] register: ${username}`) @@ -40,7 +41,7 @@ function register(username) { */ async function createRoom(content) { console.log(`[api] create room:`, content) - /** @type {import("../types").R.RoomCreated} */ + /** @type {Ty.R.RoomCreated} */ const root = await mreq.mreq("POST", "/client/v3/createRoom", content) return root.room_id } @@ -49,7 +50,7 @@ async function createRoom(content) { * @returns {Promise} room ID */ async function joinRoom(roomIDOrAlias, mxid) { - /** @type {import("../types").R.RoomJoined} */ + /** @type {Ty.R.RoomJoined} */ const root = await mreq.mreq("POST", path(`/client/v3/join/${roomIDOrAlias}`, mxid)) return root.room_id } @@ -66,7 +67,7 @@ async function leaveRoom(roomID, mxid) { /** * @param {string} roomID - * @returns {Promise} + * @returns {Promise} */ function getAllState(roomID) { return mreq.mreq("GET", `/client/v3/rooms/${roomID}/state`) @@ -83,14 +84,14 @@ async function sendState(roomID, type, stateKey, content, mxid) { console.log(`[api] state: ${roomID}: ${type}/${stateKey}`) assert.ok(type) assert.ok(typeof stateKey === "string") - /** @type {import("../types").R.EventSent} */ + /** @type {Ty.R.EventSent} */ const root = await mreq.mreq("PUT", path(`/client/v3/rooms/${roomID}/state/${type}/${stateKey}`, mxid), content) return root.event_id } async function sendEvent(roomID, type, content, mxid) { console.log(`[api] event to ${roomID} as ${mxid || "default sim"}`) - /** @type {import("../types").R.EventSent} */ + /** @type {Ty.R.EventSent} */ const root = await mreq.mreq("PUT", path(`/client/v3/rooms/${roomID}/send/${type}/${makeTxnId.makeTxnId()}`, mxid), content) return root.event_id } diff --git a/types.d.ts b/types.d.ts index 19ef1f2..01ff6a1 100644 --- a/types.d.ts +++ b/types.d.ts @@ -62,6 +62,14 @@ namespace Event { display_name?: string avatar_url?: string } + + export type M_Reaction = { + "m.relates_to": { + rel_type: "m.annotation" + event_id: string // the event that was reacted to + key: string // the unicode emoji, mxc uri, or reaction text + } + } } namespace R { diff --git a/types.js b/types.js new file mode 100644 index 0000000..4ba52ba --- /dev/null +++ b/types.js @@ -0,0 +1 @@ +module.exports = {} From 4cd9da49fd4690bbd873623b265462a393cf7f71 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 4 Jul 2023 17:35:29 +1200 Subject: [PATCH 28/99] add tests for convertNameAndTopic --- d2m/actions/create-room.js | 42 +++++++++++++++++++++------------ d2m/actions/create-room.test.js | 32 ++++++++++++++++++++++++- test/data.js | 6 ++--- 3 files changed, 60 insertions(+), 20 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 479bb79..98333b2 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -32,28 +32,19 @@ function applyKStateDiffToRoom(roomID, kstate) { } /** - * @param {DiscordTypes.APIGuildTextChannel} channel - * @param {DiscordTypes.APIGuild} guild + * @param {{id: string, name: string, topic?: string?}} channel + * @param {{id: string}} guild + * @param {string?} customName */ -async function channelToKState(channel, guild) { - const spaceID = db.prepare("SELECT space_id FROM guild_space WHERE guild_id = ?").pluck().get(guild.id) - assert.ok(typeof spaceID === "string") - const customName = db.prepare("SELECT nick FROM channel_room WHERE channel_id = ?").pluck().get(channel.id) - - const avatarEventContent = {} - if (guild.icon) { - avatarEventContent.discord_path = file.guildIcon(guild) - avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) // TODO: somehow represent future values in kstate (callbacks?), while still allowing for diffing, so test cases don't need to touch the media API - } - +function convertNameAndTopic(channel, guild, customName) { // TODO: Improve nasty nested ifs let convertedName, convertedTopic if (customName) { convertedName = customName if (channel.topic) { - convertedTopic = `${channel.name} | ${channel.topic}\n\nChannel ID: ${channel.id}\nGuild ID: ${guild.id}` + convertedTopic = `#${channel.name} | ${channel.topic}\n\nChannel ID: ${channel.id}\nGuild ID: ${guild.id}` } else { - convertedTopic = `${channel.name}\n\nChannel ID: ${channel.id}\nGuild ID: ${guild.id}` + convertedTopic = `#${channel.name}\n\nChannel ID: ${channel.id}\nGuild ID: ${guild.id}` } } else { convertedName = channel.name @@ -64,6 +55,26 @@ async function channelToKState(channel, guild) { } } + return [convertedName, convertedTopic] +} + +/** + * @param {DiscordTypes.APIGuildTextChannel} channel + * @param {DiscordTypes.APIGuild} guild + */ +async function channelToKState(channel, guild) { + const spaceID = db.prepare("SELECT space_id FROM guild_space WHERE guild_id = ?").pluck().get(guild.id) + assert.ok(typeof spaceID === "string") + + const customName = db.prepare("SELECT nick FROM channel_room WHERE channel_id = ?").pluck().get(channel.id) + const [convertedName, convertedTopic] = convertNameAndTopic(channel, guild, customName) + + const avatarEventContent = {} + if (guild.icon) { + avatarEventContent.discord_path = file.guildIcon(guild) + avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) // TODO: somehow represent future values in kstate (callbacks?), while still allowing for diffing, so test cases don't need to touch the media API + } + const channelKState = { "m.room.name/": {name: convertedName}, "m.room.topic/": {topic: convertedTopic}, @@ -209,3 +220,4 @@ module.exports.ensureRoom = ensureRoom module.exports.syncRoom = syncRoom module.exports.createAllForGuild = createAllForGuild module.exports.channelToKState = channelToKState +module.exports._convertNameAndTopic = convertNameAndTopic diff --git a/d2m/actions/create-room.test.js b/d2m/actions/create-room.test.js index ab390fc..ec5c3d3 100644 --- a/d2m/actions/create-room.test.js +++ b/d2m/actions/create-room.test.js @@ -1,4 +1,6 @@ -const {channelToKState} = require("./create-room") +// @ts-check + +const {channelToKState, _convertNameAndTopic} = require("./create-room") const {kstateStripConditionals} = require("../../matrix/kstate") const {test} = require("supertape") const testData = require("../../test/data") @@ -9,3 +11,31 @@ test("channel2room: general", async t => { testData.room.general ) }) + +test("convertNameAndTopic: custom name and topic", t => { + t.deepEqual( + _convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:"}, {id: "456"}, "hauntings"), + ["hauntings", "#the-twilight-zone | Spooky stuff here. :ghost:\n\nChannel ID: 123\nGuild ID: 456"] + ) +}) + +test("convertNameAndTopic: custom name, no topic", t => { + t.deepEqual( + _convertNameAndTopic({id: "123", name: "the-twilight-zone"}, {id: "456"}, "hauntings"), + ["hauntings", "#the-twilight-zone\n\nChannel ID: 123\nGuild ID: 456"] + ) +}) + +test("convertNameAndTopic: original name and topic", t => { + t.deepEqual( + _convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:"}, {id: "456"}, null), + ["the-twilight-zone", "Spooky stuff here. :ghost:\n\nChannel ID: 123\nGuild ID: 456"] + ) +}) + +test("convertNameAndTopic: original name, no topic", t => { + t.deepEqual( + _convertNameAndTopic({id: "123", name: "the-twilight-zone"}, {id: "456"}, null), + ["the-twilight-zone", "Channel ID: 123\nGuild ID: 456"] + ) +}) diff --git a/test/data.js b/test/data.js index 5efea36..49bbeaf 100644 --- a/test/data.js +++ b/test/data.js @@ -23,7 +23,7 @@ module.exports = { room: { general: { "m.room.name/": {name: "main"}, - "m.room.topic/": {topic: "collective-unconscious | https://docs.google.com/document/d/blah/edit | I spread, pipe, and whip because it is my will. :headstone:\n\nChannel ID: 112760669178241024\nGuild ID: 112760669178241024"}, + "m.room.topic/": {topic: "#collective-unconscious | https://docs.google.com/document/d/blah/edit | I spread, pipe, and whip because it is my will. :headstone:\n\nChannel ID: 112760669178241024\nGuild ID: 112760669178241024"}, "m.room.guest_access/": {guest_access: "can_join"}, "m.room.history_visibility/": {history_visibility: "invited"}, "m.space.parent/!jjWAGMeQdNrVZSSfvz:cadence.moe": { @@ -48,7 +48,6 @@ module.exports = { owner_id: "112760500130975744", premium_tier: 3, stickers: [{ - version: 1683838696974, type: 2, tags: "sunglasses", name: "pomu puff", @@ -56,8 +55,7 @@ module.exports = { guild_id: "112760669178241024", format_type: 1, description: "damn that tiny lil bitch really chuffing. puffing that fat ass dart", - available: true, - asset: "" + available: true }], max_members: 500000, splash: "86a34ed02524b972918bef810087f8e7", From bd32fe6c6d60747da35fa4eabf0a9432787efea4 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 5 Jul 2023 08:41:15 +1200 Subject: [PATCH 29/99] reactions working --- d2m/event-dispatcher.js | 10 ++++++---- index.js | 4 ++++ m2d/actions/add-reaction.js | 4 +++- notes.md | 2 +- package-lock.json | 20 ++++++++++---------- 5 files changed, 24 insertions(+), 16 deletions(-) diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 99c7792..1686b5f 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -14,10 +14,6 @@ module.exports = { * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message */ onMessageCreate(client, message) { - /** @ts-ignore @type {import("discord-api-types/v10").APIGuildChannel} */ - const channel = client.channels.get(message.channel_id) - const guild = client.guilds.get(channel.guild_id) - if (message.guild_id !== "112760669178241024" && message.guild_id !== "497159726455455754") return // TODO: activate on other servers (requires the space creation flow to be done first) if (message.webhook_id) { const row = db.prepare("SELECT webhook_id FROM webhook WHERE webhook_id = ?").pluck().get(message.webhook_id) if (row) { @@ -25,6 +21,11 @@ module.exports = { return } } + /** @type {import("discord-api-types/v10").APIGuildChannel} */ + const channel = client.channels.get(message.channel_id) + if (!channel.guild_id) return // Nothing we can do in direct messages. + const guild = client.guilds.get(channel.guild_id) + if (message.guild_id !== "112760669178241024" && message.guild_id !== "497159726455455754") return // TODO: activate on other servers (requires the space creation flow to be done first) sendMessage.sendMessage(message, guild) }, @@ -33,6 +34,7 @@ module.exports = { * @param {import("discord-api-types/v10").GatewayMessageReactionAddDispatchData} data */ onReactionAdd(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. if (data.emoji.id !== null) return // TODO: image emoji reactions console.log(data) addReaction.addReaction(data) diff --git a/index.js b/index.js index f4c59c6..233d518 100644 --- a/index.js +++ b/index.js @@ -21,6 +21,10 @@ passthrough.as = as sync.require("./m2d/event-dispatcher") +discord.snow.requestHandler.on("requestError", data => { + console.error("request error", data) +}) + ;(async () => { await discord.cloud.connect() console.log("Discord gateway started") diff --git a/m2d/actions/add-reaction.js b/m2d/actions/add-reaction.js index 342550d..d259ddb 100644 --- a/m2d/actions/add-reaction.js +++ b/m2d/actions/add-reaction.js @@ -17,7 +17,9 @@ async function addReaction(event) { // no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it - const emoji = event.content["m.relates_to"].key // TODO: handle custom text or emoji reactions + let emoji = event.content["m.relates_to"].key // TODO: handle custom text or emoji reactions + emoji = encodeURIComponent(emoji) + emoji = emoji.replace(/%EF%B8%8F/g, "") return discord.snow.channel.createReaction(channelID, messageID, emoji) } diff --git a/notes.md b/notes.md index 3491682..ec2b9bb 100644 --- a/notes.md +++ b/notes.md @@ -104,7 +104,7 @@ Can use custom transaction ID (?) to send the original timestamps to Matrix. See ## Reaction removed -1. Remove reaction on matrix. +1. Remove reaction on matrix. Just redact the event. ## Member data changed diff --git a/package-lock.json b/package-lock.json index fecb682..4908aa6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -685,11 +685,11 @@ } }, "node_modules/cloudstorm": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.8.0.tgz", - "integrity": "sha512-CT5/RKvSz1I0wmsf0SmZ2Jg9fPvqY67t9e2Y8n92vU0uEK5WmfPUyPOLZoYPMJwmktmsVCj4N6Pvka9gBIsY4g==", + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.8.2.tgz", + "integrity": "sha512-G/P6/+LwXjiS6AmheRG+07DmmsrpHpt21JFMhe+rW8VagFOOKemC2Bcru+Qncl/5jdjZC2gzjKpjfdTjfUm+iw==", "dependencies": { - "snowtransfer": "0.8.0" + "snowtransfer": "^0.8.2" }, "engines": { "node": ">=12.0.0" @@ -1938,9 +1938,9 @@ } }, "node_modules/node-fetch": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", - "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -2464,9 +2464,9 @@ } }, "node_modules/snowtransfer": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.8.0.tgz", - "integrity": "sha512-ang6qQsET4VX4u9mdZq6ynJvcm8HQfV6iZOHBh8Y3T0QkJLr6GAjzcv1et7BOXl1HDR/6NhD+j+ZGr8+imTclg==", + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.8.2.tgz", + "integrity": "sha512-fAmaJSpFZqGwAvbrhT3XOWwhbiuHOgxN8pGeKnDDW0f8zdkPmSQT9aekXhFr1WukB94NIALYGcyIXe902p8S4A==", "dependencies": { "discord-api-types": "^0.37.47", "form-data": "^4.0.0", From 9569fda168930a73cd8cbc7c70a238f7b88c0d99 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 5 Jul 2023 12:04:28 +1200 Subject: [PATCH 30/99] streamline the convertNameAndTopic function --- d2m/actions/create-room.js | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 98333b2..0fd0646 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -37,25 +37,17 @@ function applyKStateDiffToRoom(roomID, kstate) { * @param {string?} customName */ function convertNameAndTopic(channel, guild, customName) { - // TODO: Improve nasty nested ifs - let convertedName, convertedTopic - if (customName) { - convertedName = customName - if (channel.topic) { - convertedTopic = `#${channel.name} | ${channel.topic}\n\nChannel ID: ${channel.id}\nGuild ID: ${guild.id}` - } else { - convertedTopic = `#${channel.name}\n\nChannel ID: ${channel.id}\nGuild ID: ${guild.id}` - } - } else { - convertedName = channel.name - if (channel.topic) { - convertedTopic = `${channel.topic}\n\nChannel ID: ${channel.id}\nGuild ID: ${guild.id}` - } else { - convertedTopic = `Channel ID: ${channel.id}\nGuild ID: ${guild.id}` - } - } + const convertedName = customName || channel.name; + const maybeTopicWithPipe = channel.topic ? ` | ${channel.topic}` : ''; + const maybeTopicWithNewlines = channel.topic ? `${channel.topic}\n\n` : ''; + const channelIDPart = `Channel ID: ${channel.id}`; + const guildIDPart = `Guild ID: ${guild.id}`; - return [convertedName, convertedTopic] + const convertedTopic = customName + ? `#${channel.name}${maybeTopicWithPipe}\n\n${channelIDPart}\n${guildIDPart}` + : `${maybeTopicWithNewlines}${channelIDPart}\n${guildIDPart}`; + + return [convertedName, convertedTopic]; } /** From 1a4f92db973dda8cbf9180d57041bbe7d5b9a105 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 7 Jul 2023 17:31:39 +1200 Subject: [PATCH 31/99] start getting d2m formatted body conversion --- d2m/converters/message-to-event.js | 64 +++++++++++--------- d2m/converters/message-to-event.test.js | 20 +++++++ stdin.js | 1 + test/data.js | 78 +++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 26 deletions(-) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 382e970..90023ee 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -8,6 +8,34 @@ const { sync, db, discord } = passthrough /** @type {import("../../matrix/file")} */ const file = sync.require("../../matrix/file") +function getDiscordParseCallbacks(message, useHTML) { + return { + 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 + if (mxid && useHTML) { + return `@${username}` + } else { + return `@${username}:` + } + }, + channel: node => { + const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(node.id) + if (roomID && useHTML) { + return "https://matrix.to/#/" + roomID + } else { + return "#" + node.id + } + }, + role: node => + "@&" + node.id, + everyone: node => + "@room", + here: node => + "@here" + } +} + /** * @param {import("discord-api-types/v10").APIMessage} message * @param {import("discord-api-types/v10").APIGuild} guild @@ -17,34 +45,18 @@ async function messageToEvent(message, guild) { // Text content appears first if (message.content) { - const body = message.content - const html = markdown.toHTML(body, { - discordCallback: { - user: node => { - const mxid = db.prepare("SELECT mxid FROM sim WHERE discord_id = ?").pluck().get(node.id) - if (mxid) { - return "https://matrix.to/#/" + mxid - } else { - return "@" + node.id - } - }, - channel: node => { - const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(node.id) - if (roomID) { - return "https://matrix.to/#/" + roomID - } else { - return "#" + node.id - } - }, - role: node => - "@&" + node.id, - everyone: node => - "@room", - here: node => - "@here" - } + const html = markdown.toHTML(message.content, { + discordCallback: getDiscordParseCallbacks(message, true) }, null, null) + + const body = markdown.toHTML(message.content, { + discordCallback: getDiscordParseCallbacks(message, false), //TODO: library bug!! + discordOnly: true, + escapeHTML: false, + }, null, null) + const isPlaintext = body === html + if (isPlaintext) { events.push({ $type: "m.room.message", diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index 26cf1f1..1f3a844 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -2,6 +2,26 @@ const {test} = require("supertape") const {messageToEvent} = require("./message-to-event") const data = require("../../test/data") +test("message2event: simple plaintext", async t => { + const events = await messageToEvent(data.message.simple_plaintext, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "ayy lmao" + }]) +}) + +test("message2event: simple user mention", async t => { + const events = await messageToEvent(data.message.simple_user_mention, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "@crunch god: Tell me about Phil, renowned martial arts master and creator of the Chin Trick", + format: "org.matrix.custom.html", + formatted_body: '@crunch god Tell me about Phil, renowned martial arts master and creator of the Chin Trick' + }]) +}) + test("message2event: attachment with no content", async t => { const events = await messageToEvent(data.message.attachment_no_content, data.guild.general) t.deepEqual(events, [{ diff --git a/stdin.js b/stdin.js index cd504f2..7e0db89 100644 --- a/stdin.js +++ b/stdin.js @@ -6,6 +6,7 @@ const util = require("util") const passthrough = require("./passthrough") const { discord, config, sync, db } = passthrough +const data = sync.require("./test/data") const createSpace = sync.require("./d2m/actions/create-space") const createRoom = sync.require("./d2m/actions/create-room") const registerUser = sync.require("./d2m/actions/register-user") diff --git a/test/data.js b/test/data.js index 49bbeaf..f4326a6 100644 --- a/test/data.js +++ b/test/data.js @@ -138,6 +138,84 @@ module.exports = { }, message: { // Display order is text content, attachments, then stickers + simple_plaintext: { + id: "1126733830494093453", + type: 0, + content: "ayy lmao", + channel_id: "112760669178241024", + author: { + id: "111604486476181504", + username: "kyuugryphon", + avatar: "e4ce31267ca524d19be80e684d4cafa1", + discriminator: "0", + public_flags: 0, + flags: 0, + banner: null, + accent_color: null, + global_name: "KyuuGryphon", + avatar_decoration: null, + display_name: "KyuuGryphon", + banner_color: null + }, + attachments: [], + embeds: [], + mentions: [], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2023-07-07T04:37:58.892000+00:00", + edited_timestamp: null, + flags: 0, + components: [] + }, + simple_user_mention: { + id: "1126739682080858234", + type: 0, + content: "<@820865262526005258> Tell me about Phil, renowned martial arts master and creator of the Chin Trick", + channel_id: "112760669178241024", + author: { + id: "114147806469554185", + username: "extremity", + avatar: "6628aaf6b27219c36e2d3b5cfd6d0ee6", + discriminator: "0", + public_flags: 768, + flags: 768, + banner: null, + accent_color: null, + global_name: "Extremity", + avatar_decoration: null, + display_name: "Extremity", + banner_color: null + }, + attachments: [], + embeds: [], + mentions: [ + { + id: "820865262526005258", + username: "crunch god", + avatar: "f7a75ca031c1d2326e0f3ca5213eea47", + discriminator: "8889", + public_flags: 0, + flags: 0, + bot: true, + banner: null, + accent_color: null, + global_name: null, + avatar_decoration: null, + display_name: null, + banner_color: null + } + ], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2023-07-07T05:01:14.019000+00:00", + edited_timestamp: null, + flags: 0, + components: [] + }, attachment_no_content: { id: "1124628646670389348", type: 0, From 1d6e833b222fa6ff4bd6c9e549005fa5bf499124 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 7 Jul 2023 19:28:23 +1200 Subject: [PATCH 32/99] update discord-markdown to fix tests --- d2m/actions/register-user.test.js | 34 ++++---- d2m/converters/message-to-event.test.js | 110 ++++++++++++------------ package-lock.json | 18 ++-- package.json | 2 +- 4 files changed, 82 insertions(+), 82 deletions(-) diff --git a/d2m/actions/register-user.test.js b/d2m/actions/register-user.test.js index 7e23450..0afce50 100644 --- a/d2m/actions/register-user.test.js +++ b/d2m/actions/register-user.test.js @@ -4,21 +4,21 @@ const {test} = require("supertape") const testData = require("../../test/data") test("member2state: general", async t => { - t.deepEqual( - await _memberToStateContent(testData.member.sheep.user, testData.member.sheep, testData.guild.general.id), - { - avatar_url: "mxc://cadence.moe/rfemHmAtcprjLEiPiEuzPhpl", - displayname: "The Expert's Submarine | aprilsong", - membership: "join", - "moe.cadence.ooye.member": { - avatar: "/guilds/112760669178241024/users/134826546694193153/avatars/38dd359aa12bcd52dd3164126c587f8c.png?size=1024" - }, - "uk.half-shot.discord.member": { - bot: false, - displayColor: null, - id: "134826546694193153", - username: "@aprilsong" - } - } - ) + t.deepEqual( + await _memberToStateContent(testData.member.sheep.user, testData.member.sheep, testData.guild.general.id), + { + avatar_url: "mxc://cadence.moe/rfemHmAtcprjLEiPiEuzPhpl", + displayname: "The Expert's Submarine | aprilsong", + membership: "join", + "moe.cadence.ooye.member": { + avatar: "/guilds/112760669178241024/users/134826546694193153/avatars/38dd359aa12bcd52dd3164126c587f8c.png?size=1024" + }, + "uk.half-shot.discord.member": { + bot: false, + displayColor: null, + id: "134826546694193153", + username: "@aprilsong" + } + } + ) }) diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index 1f3a844..6e673f1 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -3,68 +3,68 @@ const {messageToEvent} = require("./message-to-event") const data = require("../../test/data") test("message2event: simple plaintext", async t => { - const events = await messageToEvent(data.message.simple_plaintext, data.guild.general) - t.deepEqual(events, [{ - $type: "m.room.message", - msgtype: "m.text", - body: "ayy lmao" - }]) + const events = await messageToEvent(data.message.simple_plaintext, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "ayy lmao" + }]) }) test("message2event: simple user mention", async t => { - const events = await messageToEvent(data.message.simple_user_mention, data.guild.general) - t.deepEqual(events, [{ - $type: "m.room.message", - msgtype: "m.text", - body: "@crunch god: Tell me about Phil, renowned martial arts master and creator of the Chin Trick", - format: "org.matrix.custom.html", - formatted_body: '@crunch god Tell me about Phil, renowned martial arts master and creator of the Chin Trick' - }]) + const events = await messageToEvent(data.message.simple_user_mention, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "@crunch god: Tell me about Phil, renowned martial arts master and creator of the Chin Trick", + format: "org.matrix.custom.html", + formatted_body: '@crunch god Tell me about Phil, renowned martial arts master and creator of the Chin Trick' + }]) }) test("message2event: attachment with no content", async t => { - const events = await messageToEvent(data.message.attachment_no_content, data.guild.general) - t.deepEqual(events, [{ - $type: "m.room.message", - msgtype: "m.image", - url: "mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM", - body: "image.png", - external_url: "https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png", - info: { - mimetype: "image/png", - w: 466, - h: 85, - size: 12919, - }, - }]) + const events = await messageToEvent(data.message.attachment_no_content, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.image", + url: "mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM", + body: "image.png", + external_url: "https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png", + info: { + mimetype: "image/png", + w: 466, + h: 85, + size: 12919, + }, + }]) }) test("message2event: stickers", async t => { - const events = await messageToEvent(data.message.sticker, data.guild.general) - t.deepEqual(events, [{ - $type: "m.room.message", - msgtype: "m.text", - body: "can have attachments too" - }, { - $type: "m.room.message", - msgtype: "m.image", - url: "mxc://cadence.moe/ZDCNYnkPszxGKgObUIFmvjus", - body: "image.png", - external_url: "https://cdn.discordapp.com/attachments/122155380120748034/1106366167486038016/image.png", - info: { - mimetype: "image/png", - w: 333, - h: 287, - size: 127373, - }, - }, { - $type: "m.sticker", - body: "pomu puff - damn that tiny lil bitch really chuffing. puffing that fat ass dart", - info: { - mimetype: "image/png" - // thumbnail_url - // thumbnail_info - }, - url: "mxc://cadence.moe/UuUaLwXhkxFRwwWCXipDlBHn" - }]) + const events = await messageToEvent(data.message.sticker, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "can have attachments too" + }, { + $type: "m.room.message", + msgtype: "m.image", + url: "mxc://cadence.moe/ZDCNYnkPszxGKgObUIFmvjus", + body: "image.png", + external_url: "https://cdn.discordapp.com/attachments/122155380120748034/1106366167486038016/image.png", + info: { + mimetype: "image/png", + w: 333, + h: 287, + size: 127373, + }, + }, { + $type: "m.sticker", + body: "pomu puff - damn that tiny lil bitch really chuffing. puffing that fat ass dart", + info: { + mimetype: "image/png" + // thumbnail_url + // thumbnail_info + }, + url: "mxc://cadence.moe/UuUaLwXhkxFRwwWCXipDlBHn" + }]) }) diff --git a/package-lock.json b/package-lock.json index 4908aa6..7dcde49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "better-sqlite3": "^8.3.0", "cloudstorm": "^0.8.0", - "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#24508e701e91d5a00fa5e773ced874d9ee8c889b", + "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#df495b152fdc48fb22284ecda9a988e6df61bf99", "heatsync": "^2.4.1", "js-yaml": "^4.1.0", "matrix-appservice": "^2.0.0", @@ -685,11 +685,11 @@ } }, "node_modules/cloudstorm": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.8.2.tgz", - "integrity": "sha512-G/P6/+LwXjiS6AmheRG+07DmmsrpHpt21JFMhe+rW8VagFOOKemC2Bcru+Qncl/5jdjZC2gzjKpjfdTjfUm+iw==", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.8.3.tgz", + "integrity": "sha512-4c2rqFFvzM4P3pcnjnGUlYuyBjx/xnMew6imB0sFwmNLITLCTLYa3qGkrnhI1g/tM0fqg+Gr+EmDHiDZfEr9LQ==", "dependencies": { - "snowtransfer": "^0.8.2" + "snowtransfer": "^0.8.3" }, "engines": { "node": ">=12.0.0" @@ -929,7 +929,7 @@ }, "node_modules/discord-markdown": { "version": "2.4.1", - "resolved": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#24508e701e91d5a00fa5e773ced874d9ee8c889b", + "resolved": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#df495b152fdc48fb22284ecda9a988e6df61bf99", "license": "MIT", "dependencies": { "simple-markdown": "^0.7.2" @@ -2464,9 +2464,9 @@ } }, "node_modules/snowtransfer": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.8.2.tgz", - "integrity": "sha512-fAmaJSpFZqGwAvbrhT3XOWwhbiuHOgxN8pGeKnDDW0f8zdkPmSQT9aekXhFr1WukB94NIALYGcyIXe902p8S4A==", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.8.3.tgz", + "integrity": "sha512-0X6NLFBUKppYT5VH/mVQNGX+ufv0AndunZC84MqGAR/3rfTIGQblgGJlHlDQbeCytlXdMpgRHIGQnBFlE094NQ==", "dependencies": { "discord-api-types": "^0.37.47", "form-data": "^4.0.0", diff --git a/package.json b/package.json index b3e19eb..7fb8cc6 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "dependencies": { "better-sqlite3": "^8.3.0", "cloudstorm": "^0.8.0", - "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#24508e701e91d5a00fa5e773ced874d9ee8c889b", + "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#df495b152fdc48fb22284ecda9a988e6df61bf99", "heatsync": "^2.4.1", "js-yaml": "^4.1.0", "matrix-appservice": "^2.0.0", From 0f4f4041601a70d72aaa8d64187b87144eb5fcf5 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 11 Jul 2023 08:01:11 +1200 Subject: [PATCH 33/99] store the channel_id on event_message --- d2m/actions/send-message.js | 2 +- d2m/converters/message-to-event.js | 16 +++- d2m/converters/message-to-event.test.js | 22 +++++ m2d/actions/send-event.js | 2 +- test/data.js | 113 ++++++++++++++++++++++++ 5 files changed, 150 insertions(+), 5 deletions(-) diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index 4f111b0..f5fe5ef 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, part, source) VALUES (?, ?, ?, 1)").run(eventID, message.id, eventPart) // source 1 = discord + 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) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 90023ee..fc76bf2 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -45,12 +45,22 @@ async function messageToEvent(message, guild) { // Text content appears first if (message.content) { - const html = markdown.toHTML(message.content, { + let content = message.content + content = content.replace(/https:\/\/(?:ptb\.|canary\.|www\.)?discord(?:app)?\.com\/channels\/([0-9]+)\/([0-9]+)\/([0-9]+)/, (whole, guildID, channelID, messageID) => { + const row = db.prepare("SELECT room_id, event_id FROM event_message INNER JOIN channel_room USING (channel_id) WHERE channel_id = ? AND message_id = ? AND part = 0").get(channelID, messageID) + if (row) { + return `https://matrix.to/#/${row.room_id}/${row.event_id}` + } else { + return `${whole} [event not found]` + } + }) + + const html = markdown.toHTML(content, { discordCallback: getDiscordParseCallbacks(message, true) }, null, null) - const body = markdown.toHTML(message.content, { - discordCallback: getDiscordParseCallbacks(message, false), //TODO: library bug!! + const body = markdown.toHTML(content, { + discordCallback: getDiscordParseCallbacks(message, false), discordOnly: true, escapeHTML: false, }, null, null) diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index 6e673f1..a456a94 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -22,6 +22,28 @@ test("message2event: simple user mention", async t => { }]) }) +test("message2event: simple room mention", async t => { + const events = await messageToEvent(data.message.simple_room_mention, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "@crunch god: Tell me about Phil, renowned martial arts master and creator of the Chin Trick", + format: "org.matrix.custom.html", + formatted_body: '@crunch god Tell me about Phil, renowned martial arts master and creator of the Chin Trick' + }]) +}) + +test("message2event: simple message link", async t => { + const events = await messageToEvent(data.message.simple_message_link, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "https://matrix.to/#/!kLRqKKUQXcibIMtOpl:cadence.moe/$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg", + format: "org.matrix.custom.html", + formatted_body: 'https://matrix.to/#/!kLRqKKUQXcibIMtOpl:cadence.moe/$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg' + }]) +}) + test("message2event: attachment with no content", async t => { const events = await messageToEvent(data.message.attachment_no_content, data.guild.general) t.deepEqual(events, [{ diff --git a/m2d/actions/send-event.js b/m2d/actions/send-event.js index 56a660f..c05f08c 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, part, source) VALUES (?, ?, ?, 0)").run(event.event_id, messageResponse.id, eventPart) // source 0 = matrix + 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 eventPart = 1 // TODO: use more intelligent algorithm to determine whether primary or supporting? messageResponses.push(messageResponse) diff --git a/test/data.js b/test/data.js index f4326a6..52b8770 100644 --- a/test/data.js +++ b/test/data.js @@ -216,6 +216,119 @@ module.exports = { flags: 0, components: [] }, + simple_message_link: { + id: "1126788210308161626", + type: 0, + content: "https://ptb.discord.com/channels/112760669178241024/112760669178241024/1126786462646550579", + channel_id: "112760669178241024", + author: { + id: "113340068197859328", + username: "kumaccino", + avatar: "b48302623a12bc7c59a71328f72ccb39", + discriminator: "0", + public_flags: 128, + flags: 128, + banner: null, + accent_color: null, + global_name: "kumaccino", + avatar_decoration: null, + display_name: "kumaccino", + banner_color: null + }, + attachments: [], + embeds: [], + mentions: [], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2023-07-07T08:14:04.050000+00:00", + edited_timestamp: null, + flags: 0, + components: [] + }, + simple_reply: { + id: "1126604870762369124", + type: 19, + content: "", + channel_id: "112760669178241024", + author: { + id: "116718249567059974", + username: "rnl", + avatar: "67e70f6424eead669e076b44474164c3", + discriminator: "0", + public_flags: 768, + flags: 768, + banner: null, + accent_color: null, + global_name: "▲", + avatar_decoration: null, + display_name: "▲", + banner_color: null + }, + attachments: [], + embeds: [], + mentions: [ + { + id: "113340068197859328", + username: "kumaccino", + avatar: "b48302623a12bc7c59a71328f72ccb39", + discriminator: "0", + public_flags: 128, + flags: 128, + banner: null, + accent_color: null, + global_name: "kumaccino", + avatar_decoration: null, + display_name: "kumaccino", + banner_color: null + } + ], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2023-07-06T20:05:32.496000+00:00", + edited_timestamp: null, + flags: 0, + components: [], + message_reference: { + channel_id: "112760669178241024", + message_id: "1126577139723026564", + guild_id: "112760669178241024" + }, + referenced_message: { + id: "1126577139723026564", + type: 0, + content: "this message was replied to", + channel_id: "112760669178241024", + author: { + id: "113340068197859328", + username: "kumaccino", + avatar: "b48302623a12bc7c59a71328f72ccb39", + discriminator: "0", + public_flags: 128, + flags: 128, + banner: null, + accent_color: null, + global_name: "kumaccino", + avatar_decoration: null, + display_name: "kumaccino", + banner_color: null + }, + attachments: [], + embeds: [], + mentions: [], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2023-07-06T18:15:20.901000+00:00", + edited_timestamp: null, + flags: 0, + components: [] + } + }, attachment_no_content: { id: "1124628646670389348", type: 0, From 5326b7d6be1eacac987ec83a6f38d1b24b5eb0cc Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 11 Jul 2023 10:10:47 +1200 Subject: [PATCH 34/99] get room mentions formatting working --- d2m/converters/message-to-event.js | 8 ++-- d2m/converters/message-to-event.test.js | 4 +- d2m/discord-client.js | 3 +- index.js | 2 +- scripts/save-channel-names-to-db.js | 58 +++++++++++++++++++++++++ test/data.js | 57 ++++++++++++++++++++++++ 6 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 scripts/save-channel-names-to-db.js diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index fc76bf2..9999df9 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -20,11 +20,11 @@ function getDiscordParseCallbacks(message, useHTML) { } }, channel: node => { - const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(node.id) - if (roomID && useHTML) { - return "https://matrix.to/#/" + roomID + 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) { + return `#${nick || name}` } else { - return "#" + node.id + return `#${nick || name}` } }, role: node => diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index a456a94..e3fcb06 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -27,9 +27,9 @@ test("message2event: simple room mention", async t => { t.deepEqual(events, [{ $type: "m.room.message", msgtype: "m.text", - body: "@crunch god: Tell me about Phil, renowned martial arts master and creator of the Chin Trick", + body: "#main", format: "org.matrix.custom.html", - formatted_body: '@crunch god Tell me about Phil, renowned martial arts master and creator of the Chin Trick' + formatted_body: '#main' }]) }) diff --git a/d2m/discord-client.js b/d2m/discord-client.js index 2092718..91682bd 100644 --- a/d2m/discord-client.js +++ b/d2m/discord-client.js @@ -12,8 +12,9 @@ const discordPackets = sync.require("./discord-packets") class DiscordClient { /** * @param {string} discordToken + * @param {boolean} listen whether to set up the event listeners for OOYE to operate */ - constructor(discordToken) { + constructor(discordToken, listen) { this.discordToken = discordToken this.snow = new SnowTransfer(discordToken) this.cloud = new CloudStorm(discordToken, { diff --git a/index.js b/index.js index 233d518..447e944 100644 --- a/index.js +++ b/index.js @@ -13,7 +13,7 @@ Object.assign(passthrough, {config, sync, db}) const DiscordClient = require("./d2m/discord-client") -const discord = new DiscordClient(config.discordToken) +const discord = new DiscordClient(config.discordToken, true) passthrough.discord = discord const as = require("./m2d/appservice") diff --git a/scripts/save-channel-names-to-db.js b/scripts/save-channel-names-to-db.js new file mode 100644 index 0000000..a70b1bb --- /dev/null +++ b/scripts/save-channel-names-to-db.js @@ -0,0 +1,58 @@ +// @ts-check + +const sqlite = require("better-sqlite3") +const HeatSync = require("heatsync") + +const config = require("../config") +const passthrough = require("../passthrough") +const db = new sqlite("db/ooye.db") + +const sync = new HeatSync({watchFS: false}) + +Object.assign(passthrough, {config, sync, db}) + +const DiscordClient = require("../d2m/discord-client") + +const discord = new DiscordClient(config.discordToken, false) +passthrough.discord = discord + +;(async () => { + await discord.cloud.connect() + console.log("Discord gateway started") + + const f = event => onPacket(discord, event, () => discord.cloud.off("event", f)) + discord.cloud.on("event", f) +})() + +const expectedGuilds = new Set() + +const prepared = db.prepare("UPDATE channel_room SET name = ? WHERE channel_id = ?") + +/** @param {DiscordClient} discord */ +function onPacket(discord, event, unsubscribe) { + if (event.t === "READY") { + for (const obj of event.d.guilds) { + expectedGuilds.add(obj.id) + } + + } else if (event.t === "GUILD_CREATE") { + expectedGuilds.delete(event.d.id) + + // Store the channel. + for (const channel of event.d.channels || []) { + prepared.run(channel.name, channel.id) + } + + // Checked them all? + if (expectedGuilds.size === 0) { + discord.cloud.disconnect() + unsubscribe() + + // I don't know why node keeps running. + setTimeout(() => { + console.log("Stopping now.") + process.exit() + }, 1500).unref() + } + } +} diff --git a/test/data.js b/test/data.js index 52b8770..d2c586a 100644 --- a/test/data.js +++ b/test/data.js @@ -216,6 +216,63 @@ module.exports = { flags: 0, components: [] }, + simple_room_mention: { + type: 0, + tts: false, + timestamp: "2023-07-10T20:04:25.939000+00:00", + referenced_message: null, + pinned: false, + nonce: "1128054139385806848", + mentions: [], + mention_roles: [], + mention_everyone: false, + member: { + roles: [ + "112767366235959296", "118924814567211009", + "204427286542417920", "199995902742626304", + "222168467627835392", "238028326281805825", + "259806643414499328", "265239342648131584", + "271173313575780353", "287733611912757249", + "225744901915148298", "305775031223320577", + "318243902521868288", "348651574924541953", + "349185088157777920", "378402925128712193", + "392141548932038658", "393912152173576203", + "482860581670486028", "495384759074160642", + "638988388740890635", "373336013109461013", + "530220455085473813", "454567553738473472", + "790724320824655873", "1123518980456452097", + "1040735082610167858", "695946570482450442", + "1123460940935991296", "849737964090556488" + ], + premium_since: null, + pending: false, + nick: null, + mute: false, + joined_at: "2015-11-11T09:55:40.321000+00:00", + flags: 0, + deaf: false, + communication_disabled_until: null, + avatar: null + }, + id: "1128054143064494233", + flags: 0, + embeds: [], + edited_timestamp: null, + content: "<#112760669178241024>", + components: [], + channel_id: "266767590641238027", + author: { + username: "kumaccino", + public_flags: 128, + id: "113340068197859328", + global_name: "kumaccino", + discriminator: "0", + avatar_decoration: null, + avatar: "b48302623a12bc7c59a71328f72ccb39" + }, + attachments: [], + guild_id: "112760669178241024" + }, simple_message_link: { id: "1126788210308161626", type: 0, From 328ae74b61033dcbd95432465d699270f98face1 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 11 Jul 2023 16:51:30 +1200 Subject: [PATCH 35/99] support rich replies, support basic m.mentions --- d2m/actions/send-message.js | 2 +- d2m/converters/message-to-event.js | 120 ++++++- d2m/converters/message-to-event.test.js | 126 +++++++ matrix/api.js | 22 ++ package.json | 2 +- test/data.js | 428 ++++++++++++++++++++++++ types.d.ts | 5 + 7 files changed, 689 insertions(+), 16 deletions(-) diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index f5fe5ef..ff3c1de 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -27,7 +27,7 @@ async function sendMessage(message, guild) { await registerUser.syncUser(message.author, message.member, message.guild_id, roomID) } - const events = await messageToEvent.messageToEvent(message, guild) + const events = await messageToEvent.messageToEvent(message, guild, api) const eventIDs = [] let eventPart = 0 // 0 is primary, 1 is supporting for (const event of events) { diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 9999df9..0e94a70 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -2,6 +2,7 @@ const assert = require("assert").strict const markdown = require("discord-markdown") +const DiscordTypes = require("discord-api-types/v10") const passthrough = require("../../passthrough") const { sync, db, discord } = passthrough @@ -39,10 +40,56 @@ function getDiscordParseCallbacks(message, useHTML) { /** * @param {import("discord-api-types/v10").APIMessage} message * @param {import("discord-api-types/v10").APIGuild} guild + * @param {import("../../matrix/api")} api simple-as-nails dependency injection for the matrix API */ -async function messageToEvent(message, guild) { +async function messageToEvent(message, guild, api) { const events = [] + /** + @type {{room?: boolean, user_ids?: string[]}} + We should consider the following scenarios for mentions: + 1. TODO A discord user rich-replies to a matrix user with a text post + + The matrix user needs to be m.mentioned in the text event + + The matrix user needs to have their name/mxid/link in the text event (notification fallback) + - So prepend their `@name:` to the start of the plaintext body + 2. TODO A discord user rich-replies to a matrix user with an image event only + + The matrix user needs to be m.mentioned in the image event + + The matrix user needs to have their name/mxid in the image event's body field, alongside the filename (notification fallback) + - So append their name to the filename body, I guess!!! + 3. TODO A discord user `@`s a matrix user in the text body of their text box + + The matrix user needs to be m.mentioned in the text event + + No change needed to the text event content: it already has their name + - So make sure we don't do anything in this case. + */ + const mentions = {} + let repliedToEventId = null + let repliedToEventRoomId = null + let repliedToEventSenderMxid = null + let repliedToEventOriginallyFromMatrix = false + + function addMention(mxid) { + if (!mentions.user_ids) mentions.user_ids = [] + mentions.user_ids.push(mxid) + } + + // Mentions scenarios 1 and 2, part A. i.e. translate relevant message.mentions to m.mentions + // (Still need to do scenarios 1 and 2 part B, and scenario 3.) + if (message.type === DiscordTypes.MessageType.Reply && message.message_reference?.message_id) { + const row = db.prepare("SELECT event_id, room_id, source FROM event_message INNER JOIN channel_room USING (channel_id) WHERE message_id = ? AND part = 0").get(message.message_reference.message_id) + if (row) { + repliedToEventId = row.event_id + repliedToEventRoomId = row.room_id + repliedToEventOriginallyFromMatrix = row.source === 0 // source 0 = matrix + } + } + if (repliedToEventOriginallyFromMatrix) { + // Need to figure out who sent that event... + const event = await api.getEvent(repliedToEventRoomId, repliedToEventId) + repliedToEventSenderMxid = event.sender + // Need to add the sender to m.mentions + addMention(repliedToEventSenderMxid) + } + // Text content appears first if (message.content) { let content = message.content @@ -55,33 +102,63 @@ async function messageToEvent(message, guild) { } }) - const html = markdown.toHTML(content, { + let html = markdown.toHTML(content, { discordCallback: getDiscordParseCallbacks(message, true) }, null, null) - const body = markdown.toHTML(content, { + let body = markdown.toHTML(content, { discordCallback: getDiscordParseCallbacks(message, false), discordOnly: true, escapeHTML: false, }, null, null) + // Fallback body/formatted_body for replies + if (repliedToEventId) { + let repliedToDisplayName + let repliedToUserHtml + if (repliedToEventOriginallyFromMatrix && repliedToEventSenderMxid) { + const match = repliedToEventSenderMxid.match(/^@([^:]*)/) + assert(match) + repliedToDisplayName = match[1] || "a Matrix user" // grab the localpart as the display name, whatever + repliedToUserHtml = `${repliedToDisplayName}` + } else { + repliedToDisplayName = message.referenced_message?.author.global_name || message.referenced_message?.author.username || "a Discord user" + repliedToUserHtml = repliedToDisplayName + } + const repliedToContent = message.referenced_message?.content || "[Replied-to message content wasn't provided by Discord]" + const repliedToHtml = markdown.toHTML(repliedToContent, { + discordCallback: getDiscordParseCallbacks(message, true) + }, null, null) + const repliedToBody = markdown.toHTML(repliedToContent, { + discordCallback: getDiscordParseCallbacks(message, false), + discordOnly: true, + escapeHTML: false, + }, null, null) + html = `
In reply to ${repliedToUserHtml}` + + `
${repliedToHtml}
` + + html + body = (`${repliedToDisplayName}: ` // scenario 1 part B for mentions + + repliedToBody).split("\n").map(line => "> " + line).join("\n") + + "\n\n" + body + } + + const newTextMessageEvent = { + $type: "m.room.message", + "m.mentions": mentions, + msgtype: "m.text", + body: body + } + const isPlaintext = body === html - if (isPlaintext) { - events.push({ - $type: "m.room.message", - msgtype: "m.text", - body: body - }) - } else { - events.push({ - $type: "m.room.message", - msgtype: "m.text", - body: body, + if (!isPlaintext) { + Object.assign(newTextMessageEvent, { format: "org.matrix.custom.html", formatted_body: html }) } + + events.push(newTextMessageEvent) } // Then attachments @@ -90,6 +167,7 @@ async function messageToEvent(message, guild) { if (attachment.content_type?.startsWith("image/") && attachment.width && attachment.height) { return { $type: "m.room.message", + "m.mentions": mentions, msgtype: "m.image", url: await file.uploadDiscordFileToMxc(attachment.url), external_url: attachment.url, @@ -105,6 +183,7 @@ async function messageToEvent(message, guild) { } else { return { $type: "m.room.message", + "m.mentions": mentions, msgtype: "m.text", body: "Unsupported attachment:\n" + JSON.stringify(attachment, null, 2) } @@ -122,6 +201,7 @@ async function messageToEvent(message, guild) { if (sticker && sticker.description) body += ` - ${sticker.description}` return { $type: "m.sticker", + "m.mentions": mentions, body, info: { mimetype: format.mime @@ -131,6 +211,7 @@ async function messageToEvent(message, guild) { } else { return { $type: "m.room.message", + "m.mentions": mentions, msgtype: "m.text", body: "Unsupported sticker format. Name: " + stickerItem.name } @@ -139,6 +220,17 @@ async function messageToEvent(message, guild) { events.push(...stickerEvents) } + // Rich replies + if (repliedToEventId) { + Object.assign(events[0], { + "m.relates_to": { + "m.in_reply_to": { + event_id: repliedToEventId + } + } + }) + } + return events } diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index e3fcb06..5fa16f8 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -1,11 +1,39 @@ const {test} = require("supertape") const {messageToEvent} = require("./message-to-event") const data = require("../../test/data") +const Ty = require("../../types") + +/** + * @param {string} roomID + * @param {string} eventID + * @returns {(roomID: string, eventID: string) => Promise>} + */ +function mockGetEvent(t, roomID_in, eventID_in, outer) { + return async function(roomID, eventID) { + t.equal(roomID, roomID_in) + t.equal(eventID, eventID_in) + return new Promise(resolve => { + setTimeout(() => { + resolve({ + event_id: eventID_in, + room_id: roomID_in, + origin_server_ts: 1680000000000, + unsigned: { + age: 2245, + transaction_id: "$local.whatever" + }, + ...outer + }) + }) + }) + } +} test("message2event: simple plaintext", async t => { const events = await messageToEvent(data.message.simple_plaintext, data.guild.general) t.deepEqual(events, [{ $type: "m.room.message", + "m.mentions": {}, msgtype: "m.text", body: "ayy lmao" }]) @@ -15,6 +43,7 @@ test("message2event: simple user mention", async t => { const events = await messageToEvent(data.message.simple_user_mention, data.guild.general) t.deepEqual(events, [{ $type: "m.room.message", + "m.mentions": {}, msgtype: "m.text", body: "@crunch god: Tell me about Phil, renowned martial arts master and creator of the Chin Trick", format: "org.matrix.custom.html", @@ -26,6 +55,7 @@ test("message2event: simple room mention", async t => { const events = await messageToEvent(data.message.simple_room_mention, data.guild.general) t.deepEqual(events, [{ $type: "m.room.message", + "m.mentions": {}, msgtype: "m.text", body: "#main", format: "org.matrix.custom.html", @@ -37,6 +67,7 @@ test("message2event: simple message link", async t => { const events = await messageToEvent(data.message.simple_message_link, data.guild.general) t.deepEqual(events, [{ $type: "m.room.message", + "m.mentions": {}, msgtype: "m.text", body: "https://matrix.to/#/!kLRqKKUQXcibIMtOpl:cadence.moe/$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg", format: "org.matrix.custom.html", @@ -48,6 +79,7 @@ test("message2event: attachment with no content", async t => { const events = await messageToEvent(data.message.attachment_no_content, data.guild.general) t.deepEqual(events, [{ $type: "m.room.message", + "m.mentions": {}, msgtype: "m.image", url: "mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM", body: "image.png", @@ -65,10 +97,12 @@ test("message2event: stickers", async t => { const events = await messageToEvent(data.message.sticker, data.guild.general) t.deepEqual(events, [{ $type: "m.room.message", + "m.mentions": {}, msgtype: "m.text", body: "can have attachments too" }, { $type: "m.room.message", + "m.mentions": {}, msgtype: "m.image", url: "mxc://cadence.moe/ZDCNYnkPszxGKgObUIFmvjus", body: "image.png", @@ -81,6 +115,7 @@ test("message2event: stickers", async t => { }, }, { $type: "m.sticker", + "m.mentions": {}, body: "pomu puff - damn that tiny lil bitch really chuffing. puffing that fat ass dart", info: { mimetype: "image/png" @@ -90,3 +125,94 @@ test("message2event: stickers", async t => { url: "mxc://cadence.moe/UuUaLwXhkxFRwwWCXipDlBHn" }]) }) + +test("message2event: skull webp attachment with content", async t => { + const events = await messageToEvent(data.message.skull_webp_attachment_with_content, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.mentions": {}, + msgtype: "m.text", + body: "Image" + }, { + $type: "m.room.message", + "m.mentions": {}, + msgtype: "m.image", + body: "skull.webp", + info: { + w: 1200, + h: 628, + mimetype: "image/webp", + size: 74290 + }, + external_url: "https://cdn.discordapp.com/attachments/112760669178241024/1128084747910918195/skull.webp", + url: "mxc://cadence.moe/sDxWmDErBhYBxtDcJQgBETes" + }]) +}) + +test("message2event: reply to skull webp attachment with content", async t => { + const events = await messageToEvent(data.message.reply_to_skull_webp_attachment_with_content, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.relates_to": { + "m.in_reply_to": { + event_id: "$oLyUTyZ_7e_SUzGNWZKz880ll9amLZvXGbArJCKai2Q" + } + }, + "m.mentions": {}, + msgtype: "m.text", + body: "> Extremity: Image\n\nReply", + format: "org.matrix.custom.html", + formatted_body: + '
In reply to Extremity' + + '
Image
' + + 'Reply' + }, { + $type: "m.room.message", + "m.mentions": {}, + msgtype: "m.image", + body: "RDT_20230704_0936184915846675925224905.jpg", + info: { + w: 2048, + h: 1536, + mimetype: "image/jpeg", + size: 85906 + }, + external_url: "https://cdn.discordapp.com/attachments/112760669178241024/1128084851023675515/RDT_20230704_0936184915846675925224905.jpg", + url: "mxc://cadence.moe/WlAbFSiNRIHPDEwKdyPeGywa" + }]) +}) + +test("message2event: simple reply to matrix user", async t => { + const events = await messageToEvent(data.message.simple_reply_to_matrix_user, data.guild.general, { + getEvent: mockGetEvent(t, "!kLRqKKUQXcibIMtOpl:cadence.moe", "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4", { + type: "m.room.message", + content: { + msgtype: "m.text", + body: "so can you reply to my webhook uwu" + }, + sender: "@cadence:cadence.moe" + }) + }) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.relates_to": { + "m.in_reply_to": { + event_id: "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4" + } + }, + "m.mentions": { + user_ids: [ + "@cadence:cadence.moe" + ] + }, + msgtype: "m.text", + body: "> cadence: so can you reply to my webhook uwu\n\nReply", + format: "org.matrix.custom.html", + formatted_body: + '
In reply to cadence' + + '
so can you reply to my webhook uwu
' + + 'Reply' + }]) +}) + +// TODO: read "edits of replies" in the spec diff --git a/matrix/api.js b/matrix/api.js index cf22933..ed9980b 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -65,6 +65,17 @@ async function leaveRoom(roomID, mxid) { await mreq.mreq("POST", path(`/client/v3/rooms/${roomID}/leave`, mxid), {}) } +/** + * @param {string} roomID + * @param {string} eventID + * @template T + */ +async function getEvent(roomID, eventID) { + /** @type {Ty.Event.Outer} */ + const root = await mreq.mreq("GET", `/client/v3/rooms/${roomID}/event/${eventID}`) + return root +} + /** * @param {string} roomID * @returns {Promise} @@ -73,6 +84,15 @@ function getAllState(roomID) { return mreq.mreq("GET", `/client/v3/rooms/${roomID}/state`) } +/** + * "Any of the AS's users must be in the room. This API is primarily for Application Services and should be faster to respond than /members as it can be implemented more efficiently on the server." + * @param {string} roomID + * @returns {Promise<{joined: {[mxid: string]: Ty.R.RoomMember}}>} + */ +function getJoinedMembers(roomID) { + return mreq.mreq("GET", `/client/v3/rooms/${roomID}/joined_members`) +} + /** * @param {string} roomID * @param {string} type @@ -114,7 +134,9 @@ module.exports.createRoom = createRoom module.exports.joinRoom = joinRoom module.exports.inviteToRoom = inviteToRoom module.exports.leaveRoom = leaveRoom +module.exports.getEvent = getEvent module.exports.getAllState = getAllState +module.exports.getJoinedMembers = getJoinedMembers module.exports.sendState = sendState module.exports.sendEvent = sendEvent module.exports.profileSetDisplayname = profileSetDisplayname diff --git a/package.json b/package.json index 7fb8cc6..8604330 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,6 @@ "tap-dot": "github:cloudrac3r/tap-dot#223a4e67a6f7daf015506a12a7af74605f06c7f4" }, "scripts": { - "test": "cross-env FORCE_COLOR=true supertape --format tap test/test.js | tap-dot" + "test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap test/test.js | tap-dot" } } diff --git a/test/data.js b/test/data.js index d2c586a..7c8fadc 100644 --- a/test/data.js +++ b/test/data.js @@ -423,6 +423,434 @@ module.exports = { flags: 0, components: [] }, + skull_webp_attachment_with_content: { + type: 0, + tts: false, + timestamp: "2023-07-10T22:06:02.805000+00:00", + referenced_message: null, + pinned: false, + nonce: "1128084721398448128", + mentions: [], + mention_roles: [], + mention_everyone: false, + member: { + roles: [ + "112767366235959296", + "118924814567211009", + "199995902742626304", + "204427286542417920", + "222168467627835392", + "271173313575780353", + "392141548932038658", + "1040735082610167858", + "372954403902193689", + "1124134606514442300", + "585531096071012409" + ], + premium_since: "2022-04-20T21:11:14.016000+00:00", + pending: false, + nick: "Tap to add a nickname", + mute: false, + joined_at: "2022-04-20T20:16:02.828000+00:00", + flags: 0, + deaf: false, + communication_disabled_until: null, + avatar: "a_4ea72c7b058ad848c9d9d35479fac26e" + }, + id: "1128084748338741392", + flags: 0, + embeds: [], + edited_timestamp: null, + content: "Image", + components: [], + channel_id: "112760669178241024", + author: { + username: "extremity", + public_flags: 768, + id: "114147806469554185", + global_name: "Extremity", + discriminator: "0", + avatar_decoration: null, + avatar: "6628aaf6b27219c36e2d3b5cfd6d0ee6" + }, + attachments: [ + { + width: 1200, + url: "https://cdn.discordapp.com/attachments/112760669178241024/1128084747910918195/skull.webp", + size: 74290, + proxy_url: "https://media.discordapp.net/attachments/112760669178241024/1128084747910918195/skull.webp", + id: "1128084747910918195", + height: 628, + filename: "skull.webp", + content_type: "image/webp" + } + ], + guild_id: "112760669178241024" + }, + reply_to_skull_webp_attachment_with_content: { + type: 19, + tts: false, + timestamp: "2023-07-10T22:06:27.348000+00:00", + referenced_message: { + type: 0, + tts: false, + timestamp: "2023-07-10T22:06:02.805000+00:00", + pinned: false, + mentions: [], + mention_roles: [], + mention_everyone: false, + id: "1128084748338741392", + flags: 0, + embeds: [], + edited_timestamp: null, + content: "Image", + components: [], + channel_id: "112760669178241024", + author: { + username: "extremity", + public_flags: 768, + id: "114147806469554185", + global_name: "Extremity", + discriminator: "0", + avatar_decoration: null, + avatar: "6628aaf6b27219c36e2d3b5cfd6d0ee6" + }, + attachments: [ + { + width: 1200, + url: "https://cdn.discordapp.com/attachments/112760669178241024/1128084747910918195/skull.webp", + size: 74290, + proxy_url: "https://media.discordapp.net/attachments/112760669178241024/1128084747910918195/skull.webp", + id: "1128084747910918195", + height: 628, + filename: "skull.webp", + content_type: "image/webp" + } + ] + }, + pinned: false, + nonce: "1128084845403045888", + message_reference: { + message_id: "1128084748338741392", + guild_id: "112760669178241024", + channel_id: "112760669178241024" + }, + mentions: [ + { + username: "extremity", + public_flags: 768, + member: { + roles: [ + "112767366235959296", + "118924814567211009", + "199995902742626304", + "204427286542417920", + "222168467627835392", + "271173313575780353", + "392141548932038658", + "1040735082610167858", + "372954403902193689", + "1124134606514442300", + "585531096071012409" + ], + premium_since: "2022-04-20T21:11:14.016000+00:00", + pending: false, + nick: "Tap to add a nickname", + mute: false, + joined_at: "2022-04-20T20:16:02.828000+00:00", + flags: 0, + deaf: false, + communication_disabled_until: null, + avatar: "a_4ea72c7b058ad848c9d9d35479fac26e" + }, + id: "114147806469554185", + global_name: "Extremity", + discriminator: "0", + avatar_decoration: null, + avatar: "6628aaf6b27219c36e2d3b5cfd6d0ee6" + } + ], + mention_roles: [], + mention_everyone: false, + member: { + roles: [ + "112767366235959296", + "118924814567211009", + "199995902742626304", + "204427286542417920", + "222168467627835392", + "271173313575780353", + "392141548932038658", + "1040735082610167858", + "372954403902193689", + "1124134606514442300", + "585531096071012409" + ], + premium_since: "2022-04-20T21:11:14.016000+00:00", + pending: false, + nick: "Tap to add a nickname", + mute: false, + joined_at: "2022-04-20T20:16:02.828000+00:00", + flags: 0, + deaf: false, + communication_disabled_until: null, + avatar: "a_4ea72c7b058ad848c9d9d35479fac26e" + }, + id: "1128084851279536279", + flags: 0, + embeds: [], + edited_timestamp: null, + content: "Reply", + components: [], + channel_id: "112760669178241024", + author: { + username: "extremity", + public_flags: 768, + id: "114147806469554185", + global_name: "Extremity", + discriminator: "0", + avatar_decoration: null, + avatar: "6628aaf6b27219c36e2d3b5cfd6d0ee6" + }, + attachments: [ + { + width: 2048, + url: "https://cdn.discordapp.com/attachments/112760669178241024/1128084851023675515/RDT_20230704_0936184915846675925224905.jpg", + size: 85906, + proxy_url: "https://media.discordapp.net/attachments/112760669178241024/1128084851023675515/RDT_20230704_0936184915846675925224905.jpg", + id: "1128084851023675515", + height: 1536, + filename: "RDT_20230704_0936184915846675925224905.jpg", + content_type: "image/jpeg" + } + ], + guild_id: "112760669178241024" + }, + simple_reply_to_matrix_user: { + type: 19, + tts: false, + timestamp: "2023-07-11T00:19:04.358000+00:00", + referenced_message: { + webhook_id: "703458020193206272", + type: 0, + tts: false, + timestamp: "2023-07-11T00:18:52.856000+00:00", + pinned: false, + mentions: [], + mention_roles: [], + mention_everyone: false, + id: "1128118177155526666", + flags: 0, + embeds: [], + edited_timestamp: null, + content: "so can you reply to my webhook uwu", + components: [], + channel_id: "112760669178241024", + author: { + username: "cadence", + id: "703458020193206272", + discriminator: "0000", + bot: true, + avatar: "ea5413d310c85eb9edaa9db865e80155" + }, + attachments: [], + application_id: "684280192553844747" + }, + pinned: false, + nonce: "1128118222315323392", + message_reference: { + message_id: "1128118177155526666", + guild_id: "112760669178241024", + channel_id: "112760669178241024" + }, + mentions: [], + mention_roles: [], + mention_everyone: false, + member: { + roles: [ + "112767366235959296", "118924814567211009", + "204427286542417920", "199995902742626304", + "222168467627835392", "238028326281805825", + "259806643414499328", "265239342648131584", + "271173313575780353", "287733611912757249", + "225744901915148298", "305775031223320577", + "318243902521868288", "348651574924541953", + "349185088157777920", "378402925128712193", + "392141548932038658", "393912152173576203", + "482860581670486028", "495384759074160642", + "638988388740890635", "373336013109461013", + "530220455085473813", "454567553738473472", + "790724320824655873", "1123518980456452097", + "1040735082610167858", "695946570482450442", + "1123460940935991296", "849737964090556488" + ], + premium_since: null, + pending: false, + nick: null, + mute: false, + joined_at: "2015-11-11T09:55:40.321000+00:00", + flags: 0, + deaf: false, + communication_disabled_until: null, + avatar: null + }, + id: "1128118225398407228", + flags: 0, + embeds: [], + edited_timestamp: null, + content: "Reply", + components: [], + channel_id: "112760669178241024", + author: { + username: "kumaccino", + public_flags: 128, + id: "113340068197859328", + global_name: "kumaccino", + discriminator: "0", + avatar_decoration: null, + avatar: "b48302623a12bc7c59a71328f72ccb39" + }, + attachments: [], + guild_id: "112760669178241024" + }, + edit_of_reply_to_skull_webp_attachment_with_content: { + type: 19, + tts: false, + timestamp: "2023-07-10T22:06:27.348000+00:00", + referenced_message: { + type: 0, + tts: false, + timestamp: "2023-07-10T22:06:02.805000+00:00", + pinned: false, + mentions: [], + mention_roles: [], + mention_everyone: false, + id: "1128084748338741392", + flags: 0, + embeds: [], + edited_timestamp: null, + content: "Image", + components: [], + channel_id: "112760669178241024", + author: { + username: "extremity", + public_flags: 768, + id: "114147806469554185", + global_name: "Extremity", + discriminator: "0", + avatar_decoration: null, + avatar: "6628aaf6b27219c36e2d3b5cfd6d0ee6" + }, + attachments: [ + { + width: 1200, + url: "https://cdn.discordapp.com/attachments/112760669178241024/1128084747910918195/skull.webp", + size: 74290, + proxy_url: "https://media.discordapp.net/attachments/112760669178241024/1128084747910918195/skull.webp", + id: "1128084747910918195", + height: 628, + filename: "skull.webp", + content_type: "image/webp" + } + ] + }, + pinned: false, + message_reference: { + message_id: "1128084748338741392", + guild_id: "112760669178241024", + channel_id: "112760669178241024" + }, + mentions: [ + { + username: "extremity", + public_flags: 768, + member: { + roles: [ + "112767366235959296", + "118924814567211009", + "199995902742626304", + "204427286542417920", + "222168467627835392", + "271173313575780353", + "392141548932038658", + "1040735082610167858", + "372954403902193689", + "1124134606514442300", + "585531096071012409" + ], + premium_since: "2022-04-20T21:11:14.016000+00:00", + pending: false, + nick: "Tap to add a nickname", + mute: false, + joined_at: "2022-04-20T20:16:02.828000+00:00", + flags: 0, + deaf: false, + communication_disabled_until: null, + avatar: "a_4ea72c7b058ad848c9d9d35479fac26e" + }, + id: "114147806469554185", + global_name: "Extremity", + discriminator: "0", + avatar_decoration: null, + avatar: "6628aaf6b27219c36e2d3b5cfd6d0ee6" + } + ], + mention_roles: [], + mention_everyone: false, + member: { + roles: [ + "112767366235959296", + "118924814567211009", + "199995902742626304", + "204427286542417920", + "222168467627835392", + "271173313575780353", + "392141548932038658", + "1040735082610167858", + "372954403902193689", + "1124134606514442300", + "585531096071012409" + ], + premium_since: "2022-04-20T21:11:14.016000+00:00", + pending: false, + nick: "Tap to add a nickname", + mute: false, + joined_at: "2022-04-20T20:16:02.828000+00:00", + flags: 0, + deaf: false, + communication_disabled_until: null, + avatar: "a_4ea72c7b058ad848c9d9d35479fac26e" + }, + id: "1128084851279536279", + flags: 0, + embeds: [], + edited_timestamp: "2023-07-10T22:08:57.442417+00:00", + content: "Edit", + components: [], + channel_id: "112760669178241024", + author: { + username: "extremity", + public_flags: 768, + id: "114147806469554185", + global_name: "Extremity", + discriminator: "0", + avatar_decoration: null, + avatar: "6628aaf6b27219c36e2d3b5cfd6d0ee6" + }, + attachments: [ + { + width: 2048, + url: "https://cdn.discordapp.com/attachments/112760669178241024/1128084851023675515/RDT_20230704_0936184915846675925224905.jpg", + size: 85906, + proxy_url: "https://media.discordapp.net/attachments/112760669178241024/1128084851023675515/RDT_20230704_0936184915846675925224905.jpg", + id: "1128084851023675515", + height: 1536, + filename: "RDT_20230704_0936184915846675925224905.jpg", + content_type: "image/jpeg" + } + ], + guild_id: "112760669178241024" + }, sticker: { id: "1106366167788044450", type: 0, diff --git a/types.d.ts b/types.d.ts index 01ff6a1..3ed3975 100644 --- a/types.d.ts +++ b/types.d.ts @@ -81,6 +81,11 @@ namespace R { room_id: string } + export type RoomMember = { + avatar_url: string + display_name: string + } + export type FileUploaded = { content_uri: string } From 437f04682c0490b3fe94e679195f6466f2045abe Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 11 Jul 2023 17:27:40 +1200 Subject: [PATCH 36/99] support written @mentions (scenario 3) --- d2m/converters/message-to-event.js | 21 +++++++++++- d2m/converters/message-to-event.test.js | 43 +++++++++++++++++++++++++ test/data.js | 31 ++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 0e94a70..daa0a3c 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -8,6 +8,9 @@ const passthrough = require("../../passthrough") const { sync, db, discord } = passthrough /** @type {import("../../matrix/file")} */ const file = sync.require("../../matrix/file") +const reg = require("../../matrix/read-registration") + +const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex)) function getDiscordParseCallbacks(message, useHTML) { return { @@ -69,7 +72,7 @@ async function messageToEvent(message, guild, api) { function addMention(mxid) { if (!mentions.user_ids) mentions.user_ids = [] - mentions.user_ids.push(mxid) + if (!mentions.user_ids.includes(mxid)) mentions.user_ids.push(mxid) } // Mentions scenarios 1 and 2, part A. i.e. translate relevant message.mentions to m.mentions @@ -106,12 +109,28 @@ async function messageToEvent(message, guild, api) { discordCallback: getDiscordParseCallbacks(message, true) }, null, null) + // TODO: add a string return type to my discord-markdown library let body = markdown.toHTML(content, { discordCallback: getDiscordParseCallbacks(message, false), discordOnly: true, escapeHTML: false, }, null, null) + // Mentions scenario 3: scan the message content for written @mentions of matrix users + const matches = [...content.matchAll(/@([a-z0-9._]+)\b/gi)] + if (matches.length && matches.some(m => m[1].match(/[a-z]/i))) { + const writtenMentionsText = matches.map(m => m[1].toLowerCase()) + const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(message.channel_id) + const {joined} = await api.getJoinedMembers(roomID) + for (const [mxid, member] of Object.entries(joined)) { + if (!userRegex.some(rx => mxid.match(rx))) { + const localpart = mxid.match(/@([^:]*)/) + assert(localpart) + if (writtenMentionsText.includes(localpart[1].toLowerCase()) || writtenMentionsText.includes(member.display_name.toLowerCase())) addMention(mxid) + } + } + } + // Fallback body/formatted_body for replies if (repliedToEventId) { let repliedToDisplayName diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index 5fa16f8..58093ca 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -215,4 +215,47 @@ test("message2event: simple reply to matrix user", async t => { }]) }) +test("message2event: simple written @mention for matrix user", async t => { + const events = await messageToEvent(data.message.simple_written_at_mention_for_matrix, data.guild.general, { + async getJoinedMembers(roomID) { + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") + return new Promise(resolve => { + setTimeout(() => { + resolve({ + joined: { + "@cadence:cadence.moe": { + display_name: "cadence [they]", + avatar_url: "whatever" + }, + "@huckleton:cadence.moe": { + display_name: "huck", + avatar_url: "whatever" + }, + "@_ooye_botrac4r:cadence.moe": { + display_name: "botrac4r", + avatar_url: "whatever" + }, + "@_ooye_bot:cadence.moe": { + display_name: "Out Of Your Element", + avatar_url: "whatever" + } + } + }) + }) + }) + } + }) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.mentions": { + user_ids: [ + "@cadence:cadence.moe", + "@huckleton:cadence.moe" + ] + }, + msgtype: "m.text", + body: "@Cadence, tell me about @Phil, the creator of the Chin Trick, who has become ever more powerful under the mentorship of @botrac4r and @huck" + }]) +}) + // TODO: read "edits of replies" in the spec diff --git a/test/data.js b/test/data.js index 7c8fadc..a5ca95e 100644 --- a/test/data.js +++ b/test/data.js @@ -304,6 +304,37 @@ module.exports = { flags: 0, components: [] }, + simple_written_at_mention_for_matrix: { + id: "1126739682080858234", + type: 0, + content: "@Cadence, tell me about @Phil, the creator of the Chin Trick, who has become ever more powerful under the mentorship of @botrac4r and @huck", + channel_id: "112760669178241024", + author: { + id: "114147806469554185", + username: "extremity", + avatar: "6628aaf6b27219c36e2d3b5cfd6d0ee6", + discriminator: "0", + public_flags: 768, + flags: 768, + banner: null, + accent_color: null, + global_name: "Extremity", + avatar_decoration: null, + display_name: "Extremity", + banner_color: null + }, + attachments: [], + embeds: [], + mentions: [], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2023-07-07T05:01:14.019000+00:00", + edited_timestamp: null, + flags: 0, + components: [] + }, simple_reply: { id: "1126604870762369124", type: 19, From 011f9c5ecbf73580de9b9121a252a195811c27c6 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 11 Jul 2023 17:28:42 +1200 Subject: [PATCH 37/99] only need to do mentions scenario 2 part B now --- d2m/converters/message-to-event.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index daa0a3c..6705b6e 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -51,15 +51,15 @@ async function messageToEvent(message, guild, api) { /** @type {{room?: boolean, user_ids?: string[]}} We should consider the following scenarios for mentions: - 1. TODO A discord user rich-replies to a matrix user with a text post + 1. A discord user rich-replies to a matrix user with a text post + The matrix user needs to be m.mentioned in the text event + The matrix user needs to have their name/mxid/link in the text event (notification fallback) - So prepend their `@name:` to the start of the plaintext body - 2. TODO A discord user rich-replies to a matrix user with an image event only + 2. A discord user rich-replies to a matrix user with an image event only + The matrix user needs to be m.mentioned in the image event - + The matrix user needs to have their name/mxid in the image event's body field, alongside the filename (notification fallback) + + TODO The matrix user needs to have their name/mxid in the image event's body field, alongside the filename (notification fallback) - So append their name to the filename body, I guess!!! - 3. TODO A discord user `@`s a matrix user in the text body of their text box + 3. A discord user `@`s a matrix user in the text body of their text box + The matrix user needs to be m.mentioned in the text event + No change needed to the text event content: it already has their name - So make sure we don't do anything in this case. From f4bfe54850b86280c3e3d0b12108d353d6a7dc7d Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 12 Jul 2023 14:33:38 +1200 Subject: [PATCH 38/99] Include fallback text for replies to media --- d2m/converters/message-to-event.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 6705b6e..300d154 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -144,7 +144,9 @@ async function messageToEvent(message, guild, api) { repliedToDisplayName = message.referenced_message?.author.global_name || message.referenced_message?.author.username || "a Discord user" repliedToUserHtml = repliedToDisplayName } - const repliedToContent = message.referenced_message?.content || "[Replied-to message content wasn't provided by Discord]" + let repliedToContent = message.referenced_message?.content + if (repliedToContent == "") repliedToContent = "[Media]" + else if (!repliedToContent) repliedToContent = "[Replied-to message content wasn't provided by Discord]" const repliedToHtml = markdown.toHTML(repliedToContent, { discordCallback: getDiscordParseCallbacks(message, true) }, null, null) From 3a59d66626fac9f0a5b70f72a478ee8f0f3f77f0 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 13 Jul 2023 17:11:24 +1200 Subject: [PATCH 39/99] move namespace_prefix into a sub-object --- d2m/actions/register-user.js | 2 +- matrix/read-registration.test.js | 2 +- types.d.ts | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index 1d1eb3d..ef6045a 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -20,7 +20,7 @@ const userToMxid = sync.require("../converters/user-to-mxid") async function createSim(user) { // Choose sim name const simName = userToMxid.userToSimName(user) - const localpart = reg.namespace_prefix + simName + const localpart = reg.ooye.namespace_prefix + simName const mxid = "@" + localpart + ":cadence.moe" // Save chosen name in the database forever diff --git a/matrix/read-registration.test.js b/matrix/read-registration.test.js index c5b3ac8..d402cfb 100644 --- a/matrix/read-registration.test.js +++ b/matrix/read-registration.test.js @@ -2,7 +2,7 @@ const {test} = require("supertape") const reg = require("./read-registration") test("reg: has necessary parameters", t => { - const propertiesToCheck = ["sender_localpart", "id", "as_token", "namespace_prefix"] + const propertiesToCheck = ["sender_localpart", "id", "as_token", "ooye"] t.deepEqual( propertiesToCheck.filter(p => p in reg), propertiesToCheck diff --git a/types.d.ts b/types.d.ts index 3ed3975..32aa21f 100644 --- a/types.d.ts +++ b/types.d.ts @@ -4,7 +4,6 @@ export type AppServiceRegistrationConfig = { hs_token: string url: string sender_localpart: string - namespace_prefix: string namespaces: { users: { exclusive: boolean @@ -17,6 +16,9 @@ export type AppServiceRegistrationConfig = { } protocols: [string] rate_limited: boolean + ooye: { + namespace_prefix: string + } } export type WebhookCreds = { From f16900553a8135e7f6168f7c0be4ea5610512efc Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 13 Jul 2023 17:36:20 +1200 Subject: [PATCH 40/99] large files are now linked instead of uploaded --- d2m/converters/message-to-event.js | 23 ++++++++++++++++++++--- d2m/converters/message-to-event.test.js | 25 +++++++++++++++++++++++++ matrix/api.js | 2 +- package-lock.json | 12 +++++++++--- package.json | 1 + types.d.ts | 1 + 6 files changed, 57 insertions(+), 7 deletions(-) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 300d154..49a387a 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -2,6 +2,7 @@ const assert = require("assert").strict const markdown = require("discord-markdown") +const pb = require("prettier-bytes") const DiscordTypes = require("discord-api-types/v10") const passthrough = require("../../passthrough") @@ -184,8 +185,24 @@ async function messageToEvent(message, guild, api) { // Then attachments const attachmentEvents = await Promise.all(message.attachments.map(async attachment => { - // TODO: handle large files differently - link them instead of uploading - if (attachment.content_type?.startsWith("image/") && attachment.width && attachment.height) { + const emoji = + attachment.content_type?.startsWith("image/jp") ? "📸" + : attachment.content_type?.startsWith("image/") ? "🖼️" + : attachment.content_type?.startsWith("video/") ? "🎞️" + : attachment.content_type?.startsWith("text/") ? "📝" + : attachment.content_type?.startsWith("audio/") ? "🎶" + : "📄" + // for large files, always link them instead of uploading so I don't use up all the space in the content repo + if (attachment.size > reg.ooye.max_file_size) { + return { + $type: "m.room.message", + "m.mentions": mentions, + msgtype: "m.text", + body: `${emoji} Uploaded file: ${attachment.url} (${pb(attachment.size)})`, + format: "org.matrix.custom.html", + formatted_body: `${emoji} Uploaded file: ${attachment.filename} (${pb(attachment.size)})` + } + } else if (attachment.content_type?.startsWith("image/") && attachment.width && attachment.height) { return { $type: "m.room.message", "m.mentions": mentions, @@ -206,7 +223,7 @@ async function messageToEvent(message, guild, api) { $type: "m.room.message", "m.mentions": mentions, msgtype: "m.text", - body: "Unsupported attachment:\n" + JSON.stringify(attachment, null, 2) + body: `Unsupported attachment:\n${JSON.stringify(attachment, null, 2)}\n${attachment.url}` } } })) diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index 58093ca..17079e5 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -258,4 +258,29 @@ test("message2event: simple written @mention for matrix user", async t => { }]) }) +test("message2event: very large attachment is linked instead of being uploaded", async t => { + const events = await messageToEvent({ + content: "hey", + attachments: [{ + filename: "hey.jpg", + url: "https://discord.com/404/hey.jpg", + content_type: "application/i-made-it-up", + size: 100e6 + }] + }) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.mentions": {}, + msgtype: "m.text", + body: "hey" + }, { + $type: "m.room.message", + "m.mentions": {}, + msgtype: "m.text", + body: "📄 Uploaded file: https://discord.com/404/hey.jpg (100 MB)", + format: "org.matrix.custom.html", + formatted_body: '📄 Uploaded file: hey.jpg (100 MB)' + }]) +}) + // TODO: read "edits of replies" in the spec diff --git a/matrix/api.js b/matrix/api.js index ed9980b..7f9d74e 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -110,7 +110,7 @@ async function sendState(roomID, type, stateKey, content, mxid) { } async function sendEvent(roomID, type, content, mxid) { - console.log(`[api] event to ${roomID} as ${mxid || "default sim"}`) + 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) return root.event_id diff --git a/package-lock.json b/package-lock.json index 7dcde49..13b6799 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "matrix-js-sdk": "^24.1.0", "mixin-deep": "^2.0.1", "node-fetch": "^2.6.7", + "prettier-bytes": "^1.0.4", "snowtransfer": "^0.8.0", "try-to-catch": "^3.0.1" }, @@ -2099,6 +2100,11 @@ "node": ">=10" } }, + "node_modules/prettier-bytes": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prettier-bytes/-/prettier-bytes-1.0.4.tgz", + "integrity": "sha512-dLbWOa4xBn+qeWeIF60qRoB6Pk2jX5P3DIVgOQyMyvBpu931Q+8dXz8X0snJiFkQdohDDLnZQECjzsAj75hgZQ==" + }, "node_modules/pretty-format": { "version": "29.5.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", @@ -2318,9 +2324,9 @@ } }, "node_modules/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, diff --git a/package.json b/package.json index 8604330..951089b 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "matrix-js-sdk": "^24.1.0", "mixin-deep": "^2.0.1", "node-fetch": "^2.6.7", + "prettier-bytes": "^1.0.4", "snowtransfer": "^0.8.0", "try-to-catch": "^3.0.1" }, diff --git a/types.d.ts b/types.d.ts index 32aa21f..eeb4b75 100644 --- a/types.d.ts +++ b/types.d.ts @@ -18,6 +18,7 @@ export type AppServiceRegistrationConfig = { rate_limited: boolean ooye: { namespace_prefix: string + max_file_size: number } } From 96dd488e3935bc3a8a2eabca0546250dc6ac2fe3 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 28 Jul 2023 17:05:13 +1200 Subject: [PATCH 41/99] 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}`) + } + } +})() From f501718691b4f03f18be0d42b098b8a0300ea64d Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 11 Aug 2023 01:11:58 +1200 Subject: [PATCH 42/99] add code coverage --- .gitignore | 1 + package-lock.json | 506 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 4 +- 3 files changed, 510 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9c3ba55..4c982d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules config.js registration.yaml +coverage diff --git a/package-lock.json b/package-lock.json index 13b6799..e8b6aeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "devDependencies": { "@types/node": "^18.16.0", "@types/node-fetch": "^2.6.3", + "c8": "^8.0.1", "cross-env": "^7.0.3", "supertape": "^8.3.0", "tap-dot": "github:cloudrac3r/tap-dot#223a4e67a6f7daf015506a12a7af74605f06c7f4" @@ -41,6 +42,12 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, "node_modules/@cloudcmd/stub": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@cloudcmd/stub/-/stub-4.0.1.tgz", @@ -141,6 +148,15 @@ "node": ">=8" } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@jest/schemas": { "version": "29.4.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", @@ -153,6 +169,31 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", + "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@matrix-org/matrix-sdk-crypto-js": { "version": "0.1.0-alpha.8", "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.8.tgz", @@ -281,6 +322,12 @@ "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, "node_modules/@types/node": { "version": "18.16.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.5.tgz", @@ -625,6 +672,32 @@ "node": ">= 0.8" } }, + "node_modules/c8": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/c8/-/c8-8.0.1.tgz", + "integrity": "sha512-EINpopxZNH1mETuI0DzRA4MZpAUH+IFiRhnmFD3vFr3vdrgxqi3VfE3KL0AIL+zDq8rC9bZqwM/VDmmoe04y7w==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^2.0.0", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "rimraf": "^3.0.2", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -685,6 +758,41 @@ "node": ">=4" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cloudstorm": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.8.3.tgz", @@ -731,6 +839,12 @@ "node": ">= 0.8" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -750,6 +864,12 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, "node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -983,6 +1103,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1130,6 +1259,22 @@ "node": ">= 0.8" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -1139,6 +1284,19 @@ "is-callable": "^1.1.3" } }, + "node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/form-data": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", @@ -1203,6 +1361,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", @@ -1337,6 +1504,12 @@ "backtracker": "3.3.2" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -1674,6 +1847,42 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jest-diff": { "version": "29.5.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", @@ -1715,6 +1924,21 @@ "integrity": "sha512-QkuwuBMQ9BQHMUEkAtIA4INLrkmnnveqlFB1oFi09gbU0wBdZo6tTnyxNWMR84zHxBuwK7GLAwqN8nrvVxOLTA==", "dev": true }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/loglevel": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", @@ -1738,6 +1962,21 @@ "node": ">=10" } }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/matrix-appservice": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/matrix-appservice/-/matrix-appservice-2.0.0.tgz", @@ -2035,6 +2274,36 @@ "wrappy": "1" } }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-retry": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", @@ -2055,6 +2324,24 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2266,6 +2553,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -2291,6 +2587,63 @@ "node": ">= 4" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2418,6 +2771,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -2752,6 +3111,62 @@ "node": ">= 6" } }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -2856,6 +3271,20 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-to-istanbul": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -2944,6 +3373,44 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2955,11 +3422,38 @@ "integrity": "sha512-23LJhkIw940uTcDFyJZmNyO0z8lEINOTGCr4vR5YCG3urkdXwduRIhivBm9wKaVynLHYvxoHHYbKsDiafCLp6w==", "dev": true }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", @@ -2968,6 +3462,18 @@ "engines": { "node": ">=12" } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 951089b..dd6f55d 100644 --- a/package.json +++ b/package.json @@ -31,11 +31,13 @@ "devDependencies": { "@types/node": "^18.16.0", "@types/node-fetch": "^2.6.3", + "c8": "^8.0.1", "cross-env": "^7.0.3", "supertape": "^8.3.0", "tap-dot": "github:cloudrac3r/tap-dot#223a4e67a6f7daf015506a12a7af74605f06c7f4" }, "scripts": { - "test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap test/test.js | tap-dot" + "test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap test/test.js | tap-dot", + "cover": "c8 --skip-full -r html -r text supertape --no-check-assertions-count --format fail test/test.js" } } From cae591e5fd63a7afe66ac93f617a7ff53e5c430c Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 15 Aug 2023 17:20:31 +1200 Subject: [PATCH 43/99] script for capturing message update events --- d2m/actions/edit-message.js | 96 +---------------------- d2m/converters/edit-to-changes.js | 96 +++++++++++++++++++++++ d2m/discord-client.js | 6 +- scripts/capture-message-update-events.js | 51 ++++++++++++ scripts/events.db | Bin 0 -> 8192 bytes 5 files changed, 154 insertions(+), 95 deletions(-) create mode 100644 d2m/converters/edit-to-changes.js create mode 100644 scripts/capture-message-update-events.js create mode 100644 scripts/events.db diff --git a/d2m/actions/edit-message.js b/d2m/actions/edit-message.js index 662e124..933267c 100644 --- a/d2m/actions/edit-message.js +++ b/d2m/actions/edit-message.js @@ -1,94 +1,5 @@ -// @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! +async function editMessage() { + // Action time! // 1. Replace all the things. @@ -113,6 +24,5 @@ async function editMessage(message, guild) { } return eventIDs -} -module.exports.editMessage = editMessage +{eventsToReplace, eventsToRedact, eventsToSend} diff --git a/d2m/converters/edit-to-changes.js b/d2m/converters/edit-to-changes.js new file mode 100644 index 0000000..0dd084f --- /dev/null +++ b/d2m/converters/edit-to-changes.js @@ -0,0 +1,96 @@ +// @ts-check + +const assert = require("assert") + +const passthrough = require("../../passthrough") +const { discord, sync, db } = passthrough +/** @type {import("./message-to-event")} */ +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") +/** @type {import("../actions/create-room")} */ +const createRoom = sync.require("../actions/create-room") + +/** + * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message + * IMPORTANT: This may not have all the normal fields! The API documentation doesn't provide possible types, just says it's all optional! + * Since I don't have a spec, I will have to capture some real traffic and add it as test cases... I hope they don't change anything later... + * @param {import("discord-api-types/v10").APIGuild} guild + */ +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 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 + }) + + return {eventsToReplace, eventsToRedact, eventsToSend} +} + +module.exports.editMessage = editMessage diff --git a/d2m/discord-client.js b/d2m/discord-client.js index 91682bd..5e90d85 100644 --- a/d2m/discord-client.js +++ b/d2m/discord-client.js @@ -14,7 +14,7 @@ class DiscordClient { * @param {string} discordToken * @param {boolean} listen whether to set up the event listeners for OOYE to operate */ - constructor(discordToken, listen) { + constructor(discordToken, listen = true) { this.discordToken = discordToken this.snow = new SnowTransfer(discordToken) this.cloud = new CloudStorm(discordToken, { @@ -44,7 +44,9 @@ class DiscordClient { this.guilds = new Map() /** @type {Map>} */ this.guildChannelMap = new Map() - this.cloud.on("event", message => discordPackets.onPacket(this, message)) + if (listen) { + this.cloud.on("event", message => discordPackets.onPacket(this, message)) + } this.cloud.on("error", console.error) } } diff --git a/scripts/capture-message-update-events.js b/scripts/capture-message-update-events.js new file mode 100644 index 0000000..2ff9b49 --- /dev/null +++ b/scripts/capture-message-update-events.js @@ -0,0 +1,51 @@ +// @ts-check + +// **** +const interestingFields = ["author", "content", "edited_timestamp", "mentions", "attachments", "embeds", "type", "message_reference", "referenced_message", "sticker_items"] +// ***** + +function fieldToPresenceValue(field) { + if (field === undefined) return 0 + else if (field === null) return 1 + else if (Array.isArray(field) && field.length === 0) return 10 + else if (typeof field === "object" && Object.keys(field).length === 0) return 20 + else if (field === "") return 30 + else return 99 +} + +const sqlite = require("better-sqlite3") +const HeatSync = require("heatsync") + +const config = require("../config") +const passthrough = require("../passthrough") + +const sync = new HeatSync({watchFS: false}) + +Object.assign(passthrough, {config, sync}) + +const DiscordClient = require("../d2m/discord-client", false) + +const discord = new DiscordClient(config.discordToken, false) +passthrough.discord = discord + +;(async () => { + await discord.cloud.connect() + console.log("Discord gateway started") + + const f = event => onPacket(discord, event, () => discord.cloud.off("event", f)) + discord.cloud.on("event", f) +})() + +const events = new sqlite("scripts/events.db") +const sql = "INSERT INTO \"update\" (json, " + interestingFields.join(", ") + ") VALUES (" + "?".repeat(interestingFields.length + 1).split("").join(", ") + ")" +console.log(sql) +const prepared = events.prepare(sql) + +/** @param {DiscordClient} discord */ +function onPacket(discord, event, unsubscribe) { + if (event.t === "MESSAGE_UPDATE") { + const data = [JSON.stringify(event.d), ...interestingFields.map(f => fieldToPresenceValue(event.d[f]))] + console.log(data) + prepared.run(...data) + } +} diff --git a/scripts/events.db b/scripts/events.db new file mode 100644 index 0000000000000000000000000000000000000000..c8f5bad88858b0ddc7a3b54ec36577c94791c001 GIT binary patch literal 8192 zcmeH~-EJH;6vuZ16-iJk?ojXCxuJz5^0&vM3elE`R*G6^7m13XiD&HHOfoZG%}1lC z>J@O&TONa_;T?Dp&g^bBOW8y5$w>2Euo@l8K~zy9e7^aOeWJ%OG;PoO8z z6X*%_1bPBJf&T}AA8!sn`1sbX;m_xdNpjatt!WnD8xNly+#emF(ftR92Pl|#1?bN9 zR&ZMTGT0g&d^bW*p2Fwb!^2%@Hf?j_t6=N#lhMIf2haZ0m-@1CWwXB1S*Uq!++>BT z8&gczTNN-q^JTrh*EEeuPdb}x3|x^odre<+F?H*w1&?YoadG9wu5x91joGr>#)}c_ zr>mPRJ#$qI@fK@**}mJ=<{O*84|aZqum0%?^aOeWJ%OG;PoO98-yrbj%Z;61isAg( z+}wN>%&uP%-VGYKyhUS^*ACv7x4|f&n6f<_?h!G~i$-vKW2;!)~zn6<0oYu)=DSd+DkRU%Hi%a8ay+=v8oO9S}m5R7^_Bgch1=2@4Vu zYRW^xg;bOfj5RbeO$jH0hni8$LMg(TC1qS<&9R2@8XWJ&Gb*70(@ri26mb6=+V2%|NIHX`jqzb_Sh<{qSB5RAd z&bk|HRJM5zjjps=Iavl?!qqg0f~R@mQkbe}n=2@%J_EY3X}|zB1Md;MNA-xZNQj7$ zeX7*zW{h()0nV_a)|v5gh13^C3(J`99v|DR?rIJjw{Wk|FTFA^O=AFm=i}?$AcHN1 zSUHC1fuRZ+Kmg?6;MF3FtNm-?I$lC zI`<_qD6QJ8cDK=;7p}g0R34R&VB&jGQbOGlf$xeF-bs@P4Dtd08xK>1zVm$m3EKjT z%qpiD!t}(zS<2%%dyuDw-Q^VY$~2Lkq8F7OoRPM6RcQ)PO7Os+n)D1k@>T5s9+*wr zB+t_LYNKW|Dn&$hIJi`BRwNu0FrDN+F?l?nYOYVvM_Ofo#WYY=Mu7&MgJt4DgiX~# zikNF9wl4L+1~m1yhyqLam@A@rB3gAL4~Z@{c2<=y zGR0czZwslzxlTx+F=qrP%rb)0P`E@pYmBxDBytyGBE(v~nCTQ`K2K^hs50af%rzAP NWX=`uviRRq)gLj?i?aX# literal 0 HcmV?d00001 From b1ca71f37c2d8a20e534aac22c925e17d176e566 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 16 Aug 2023 17:03:05 +1200 Subject: [PATCH 44/99] 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 From 8f6bb86b927792b70f3cb49f2faf48ef647082e1 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 16 Aug 2023 20:44:38 +1200 Subject: [PATCH 45/99] record more update events --- d2m/actions/send-message.js | 2 +- d2m/converters/edit-to-changes.js | 47 +++++----- d2m/converters/edit-to-changes.test.js | 9 +- d2m/converters/message-to-event.js | 20 +++- d2m/converters/message-to-event.test.js | 116 +++++++++++++++--------- scripts/events.db | Bin 8192 -> 98304 bytes 6 files changed, 122 insertions(+), 72 deletions(-) diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index 258efcf..cf87d35 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -27,7 +27,7 @@ async function sendMessage(message, guild) { await registerUser.syncUser(message.author, message.member, message.guild_id, roomID) } - const events = await messageToEvent.messageToEvent(message, guild, api) + const events = await messageToEvent.messageToEvent(message, guild, {}, {api}) const eventIDs = [] let eventPart = 0 // 0 is primary, 1 is supporting for (const event of events) { diff --git a/d2m/converters/edit-to-changes.js b/d2m/converters/edit-to-changes.js index 87e769b..4afa3ce 100644 --- a/d2m/converters/edit-to-changes.js +++ b/d2m/converters/edit-to-changes.js @@ -29,7 +29,9 @@ async function editToChanges(message, guild) { // Figure out what we will be replacing them with - const newEvents = await messageToEvent.messageToEvent(message, guild, api) + const newFallbackContent = await messageToEvent.messageToEvent(message, guild, {includeEditFallbackStar: true}, {api}) + const newInnerContent = await messageToEvent.messageToEvent(message, guild, {includeReplyFallback: false}, {api}) + assert.ok(newFallbackContent.length === newInnerContent.length) // Match the new events to the old events @@ -47,21 +49,27 @@ async function editToChanges(message, guild) { 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. + function shift() { + newFallbackContent.shift() + newInnerContent.shift() + } + // For each old event... - outer: while (newEvents.length) { - const newe = newEvents[0] + outer: while (newFallbackContent.length) { + const newe = newFallbackContent[0] // Find a new event to pair it with... for (let i = 0; i < oldEventRows.length; i++) { const olde = oldEventRows[i] - if (olde.event_type === newe.$type && olde.event_subtype === (newe.msgtype || null)) { + if (olde.event_type === newe.$type && olde.event_subtype === (newe.msgtype ?? null)) { // The spec does allow subtypes to change, so I can change this condition later if I want to // Found one! // Set up the pairing eventsToReplace.push({ old: olde, - new: newe + newFallbackContent: newFallbackContent[0], + newInnerContent: newInnerContent[0] }) // These events have been handled now, so remove them from the source arrays - newEvents.shift() + shift() oldEventRows.splice(i, 1) // Go all the way back to the start of the next iteration of the outer loop continue outer @@ -69,7 +77,7 @@ async function editToChanges(message, guild) { } // 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() + shift() } // Anything remaining in oldEventRows is present in the old version only and should be redacted. eventsToRedact = oldEventRows @@ -92,7 +100,7 @@ async function editToChanges(message, guild) { // 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)})) + eventsToReplace = eventsToReplace.map(e => ({oldID: e.old.event_id, new: eventToReplacementEvent(e.old.event_id, e.newFallbackContent, e.newInnerContent)})) return {eventsToReplace, eventsToRedact, eventsToSend} } @@ -100,31 +108,26 @@ async function editToChanges(message, guild) { /** * @template T * @param {string} oldID - * @param {T} content + * @param {T} newFallbackContent + * @param {T} newInnerContent * @returns {import("../../types").Event.ReplacementContent} content */ -function eventToReplacementEvent(oldID, content) { - const newContent = { - ...content, +function eventToReplacementEvent(oldID, newFallbackContent, newInnerContent) { + const content = { + ...newFallbackContent, "m.mentions": {}, "m.new_content": { - ...content + ...newInnerContent }, "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"] + delete content["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 + delete content["m.new_content"]["m.relates_to"] + return content } module.exports.editToChanges = editToChanges diff --git a/d2m/converters/edit-to-changes.test.js b/d2m/converters/edit-to-changes.test.js index b3e6e0c..f6ecc8d 100644 --- a/d2m/converters/edit-to-changes.test.js +++ b/d2m/converters/edit-to-changes.test.js @@ -47,9 +47,13 @@ test("edit2changes: edit of reply to skull webp attachment with content", async oldID: "$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M", new: { $type: "m.room.message", - // TODO: read "edits of replies" in the spec!!! msgtype: "m.text", - body: "* Edit", + body: "> Extremity: Image\n\n* Edit", + format: "org.matrix.custom.html", + formatted_body: + '
In reply to Extremity' + + '
Image
' + + '* Edit', "m.mentions": {}, "m.new_content": { msgtype: "m.text", @@ -60,7 +64,6 @@ test("edit2changes: edit of reply to skull webp attachment with content", async 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 a2c4915..c128595 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -55,9 +55,12 @@ function getDiscordParseCallbacks(message, useHTML) { /** * @param {import("discord-api-types/v10").APIMessage} message * @param {import("discord-api-types/v10").APIGuild} guild - * @param {import("../../matrix/api")} api simple-as-nails dependency injection for the matrix API + * @param {{includeReplyFallback?: boolean, includeEditFallbackStar?: boolean}} options default values: + * - includeReplyFallback: true + * - includeEditFallbackStar: false + * @param {{api: import("../../matrix/api")}} di simple-as-nails dependency injection for the matrix API */ -async function messageToEvent(message, guild, api) { +async function messageToEvent(message, guild, options = {}, di) { const events = [] /** @@ -99,7 +102,7 @@ async function messageToEvent(message, guild, api) { } if (repliedToEventOriginallyFromMatrix) { // Need to figure out who sent that event... - const event = await api.getEvent(repliedToEventRoomId, repliedToEventId) + const event = await di.api.getEvent(repliedToEventRoomId, repliedToEventId) repliedToEventSenderMxid = event.sender // Need to add the sender to m.mentions addMention(repliedToEventSenderMxid) @@ -133,7 +136,7 @@ async function messageToEvent(message, guild, api) { if (matches.length && matches.some(m => m[1].match(/[a-z]/i))) { const writtenMentionsText = matches.map(m => m[1].toLowerCase()) const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(message.channel_id) - const {joined} = await api.getJoinedMembers(roomID) + const {joined} = await di.api.getJoinedMembers(roomID) for (const [mxid, member] of Object.entries(joined)) { if (!userRegex.some(rx => mxid.match(rx))) { const localpart = mxid.match(/@([^:]*)/) @@ -143,8 +146,15 @@ async function messageToEvent(message, guild, api) { } } + // Star * prefix for fallback edits + if (options.includeEditFallbackStar) { + body = "* " + body + html = "* " + html + } + // Fallback body/formatted_body for replies - if (repliedToEventId) { + // This branch is optional - do NOT change anything apart from the reply fallback, since it may not be run + if (repliedToEventId && options.includeReplyFallback !== false) { let repliedToDisplayName let repliedToUserHtml if (repliedToEventOriginallyFromMatrix && repliedToEventSenderMxid) { diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index 17079e5..4200afe 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -30,7 +30,7 @@ function mockGetEvent(t, roomID_in, eventID_in, outer) { } test("message2event: simple plaintext", async t => { - const events = await messageToEvent(data.message.simple_plaintext, data.guild.general) + const events = await messageToEvent(data.message.simple_plaintext, data.guild.general, {}) t.deepEqual(events, [{ $type: "m.room.message", "m.mentions": {}, @@ -40,7 +40,7 @@ test("message2event: simple plaintext", async t => { }) test("message2event: simple user mention", async t => { - const events = await messageToEvent(data.message.simple_user_mention, data.guild.general) + const events = await messageToEvent(data.message.simple_user_mention, data.guild.general, {}) t.deepEqual(events, [{ $type: "m.room.message", "m.mentions": {}, @@ -52,7 +52,7 @@ test("message2event: simple user mention", async t => { }) test("message2event: simple room mention", async t => { - const events = await messageToEvent(data.message.simple_room_mention, data.guild.general) + const events = await messageToEvent(data.message.simple_room_mention, data.guild.general, {}) t.deepEqual(events, [{ $type: "m.room.message", "m.mentions": {}, @@ -64,7 +64,7 @@ test("message2event: simple room mention", async t => { }) test("message2event: simple message link", async t => { - const events = await messageToEvent(data.message.simple_message_link, data.guild.general) + const events = await messageToEvent(data.message.simple_message_link, data.guild.general, {}) t.deepEqual(events, [{ $type: "m.room.message", "m.mentions": {}, @@ -76,7 +76,7 @@ test("message2event: simple message link", async t => { }) test("message2event: attachment with no content", async t => { - const events = await messageToEvent(data.message.attachment_no_content, data.guild.general) + const events = await messageToEvent(data.message.attachment_no_content, data.guild.general, {}) t.deepEqual(events, [{ $type: "m.room.message", "m.mentions": {}, @@ -94,7 +94,7 @@ test("message2event: attachment with no content", async t => { }) test("message2event: stickers", async t => { - const events = await messageToEvent(data.message.sticker, data.guild.general) + const events = await messageToEvent(data.message.sticker, data.guild.general, {}) t.deepEqual(events, [{ $type: "m.room.message", "m.mentions": {}, @@ -127,7 +127,7 @@ test("message2event: stickers", async t => { }) test("message2event: skull webp attachment with content", async t => { - const events = await messageToEvent(data.message.skull_webp_attachment_with_content, data.guild.general) + const events = await messageToEvent(data.message.skull_webp_attachment_with_content, data.guild.general, {}) t.deepEqual(events, [{ $type: "m.room.message", "m.mentions": {}, @@ -150,7 +150,7 @@ test("message2event: skull webp attachment with content", async t => { }) test("message2event: reply to skull webp attachment with content", async t => { - const events = await messageToEvent(data.message.reply_to_skull_webp_attachment_with_content, data.guild.general) + const events = await messageToEvent(data.message.reply_to_skull_webp_attachment_with_content, data.guild.general, {}) t.deepEqual(events, [{ $type: "m.room.message", "m.relates_to": { @@ -183,15 +183,17 @@ test("message2event: reply to skull webp attachment with content", async t => { }) test("message2event: simple reply to matrix user", async t => { - const events = await messageToEvent(data.message.simple_reply_to_matrix_user, data.guild.general, { - getEvent: mockGetEvent(t, "!kLRqKKUQXcibIMtOpl:cadence.moe", "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4", { - type: "m.room.message", - content: { - msgtype: "m.text", - body: "so can you reply to my webhook uwu" - }, - sender: "@cadence:cadence.moe" - }) + const events = await messageToEvent(data.message.simple_reply_to_matrix_user, data.guild.general, {}, { + api: { + getEvent: mockGetEvent(t, "!kLRqKKUQXcibIMtOpl:cadence.moe", "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4", { + type: "m.room.message", + content: { + msgtype: "m.text", + body: "so can you reply to my webhook uwu" + }, + sender: "@cadence:cadence.moe" + }) + } }) t.deepEqual(events, [{ $type: "m.room.message", @@ -215,34 +217,66 @@ test("message2event: simple reply to matrix user", async t => { }]) }) +test("message2event: simple reply to matrix user, reply fallbacks disabled", async t => { + const events = await messageToEvent(data.message.simple_reply_to_matrix_user, data.guild.general, {includeReplyFallback: false}, { + api: { + getEvent: mockGetEvent(t, "!kLRqKKUQXcibIMtOpl:cadence.moe", "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4", { + type: "m.room.message", + content: { + msgtype: "m.text", + body: "so can you reply to my webhook uwu" + }, + sender: "@cadence:cadence.moe" + }) + } + }) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.relates_to": { + "m.in_reply_to": { + event_id: "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4" + } + }, + "m.mentions": { + user_ids: [ + "@cadence:cadence.moe" + ] + }, + msgtype: "m.text", + body: "Reply" + }]) +}) + test("message2event: simple written @mention for matrix user", async t => { - const events = await messageToEvent(data.message.simple_written_at_mention_for_matrix, data.guild.general, { - async getJoinedMembers(roomID) { - t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") - return new Promise(resolve => { - setTimeout(() => { - resolve({ - joined: { - "@cadence:cadence.moe": { - display_name: "cadence [they]", - avatar_url: "whatever" - }, - "@huckleton:cadence.moe": { - display_name: "huck", - avatar_url: "whatever" - }, - "@_ooye_botrac4r:cadence.moe": { - display_name: "botrac4r", - avatar_url: "whatever" - }, - "@_ooye_bot:cadence.moe": { - display_name: "Out Of Your Element", - avatar_url: "whatever" + const events = await messageToEvent(data.message.simple_written_at_mention_for_matrix, data.guild.general, {}, { + api: { + async getJoinedMembers(roomID) { + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") + return new Promise(resolve => { + setTimeout(() => { + resolve({ + joined: { + "@cadence:cadence.moe": { + display_name: "cadence [they]", + avatar_url: "whatever" + }, + "@huckleton:cadence.moe": { + display_name: "huck", + avatar_url: "whatever" + }, + "@_ooye_botrac4r:cadence.moe": { + display_name: "botrac4r", + avatar_url: "whatever" + }, + "@_ooye_bot:cadence.moe": { + display_name: "Out Of Your Element", + avatar_url: "whatever" + } } - } + }) }) }) - }) + } } }) t.deepEqual(events, [{ diff --git a/scripts/events.db b/scripts/events.db index c8f5bad88858b0ddc7a3b54ec36577c94791c001..3356cbe2ba67631f1efdf34292de8256ab6ee024 100644 GIT binary patch literal 98304 zcmeI5S&$s}ecu6|Af*)~+p_G~j$6w}JcOO?`xuL)fW-n>0K2#r7rFGZXS!#nu`}Jn zId-ui8H=W*P?ji5N-DBdQY=+Dr;{p4wqixL>{QAR$x8~Xyd;(6MR~|e9-J4yCf`dVy|NH+tzQ6D9(uK28J5(x3v+lPQJy+fhAiwf%Y{`>HzkM@ihuKI^yJez_{wY;i$YFNMJt=h|zHF#EBTsO2w(i_NeS zHp95Q#bxWKgT>w**^g_rqw;dtT%^ABjIuob^uvt1j~@PpjJ{tw{QnOB&Efxh`2QUK z--myC_R$f-7_j%FmcR4J zLu3E&m4n^Sp+kpW9Z$zcQ9Ux=W^@%ReytVqdwsYT$8}XVCRBGqv*tCUXn94$DQJ$R zs_LgywWzA&N5&gb9EZXB-Rq;d_~J`P#(V!-Y$mmkHw+#MSHk965{CnKGLoC)#aGAs zSC4C&?%0lD+qz+SmZy6*FVr;G(@j@1E!)vGO;tS(8lJA1nq|74p{s^#TW)%#ZkV>} znX1SAp4PqJ&}|N=x@)VNZhDSqq?ekOZacQgQPt#?jyzO1Jexy~V;hEL*qYRrH#(*& zSG$hNVOy8?YpSU_hNZeXS82B6QUL1TXomHuQ(tUFGR2BzB%3@!LMlFJ*lF$aqATQdg7qb#>2GJ zcu8;VN&9tVvCAGkmhQTqxjuUO>t}KN(?vgAY^*htx*tbDr@TxJHQTUtUDGW`rJZg2 zIpzG|1N`1nG+5TAa8yQ$Aq&w)kKMuK)QhH8 z)YSqlrCM2&b7A2brHv4y$g;RkgRbl|Ls%7iYfWaWZyrd}oZK)nkQig3Z<(4A7@Ask zt+3>Uf$w`kiLD@XRn4+8$BW-+FwDElV%n&-YwBF?>6XPXHyu+dyt`5L#p4f*eZ4+7 zs_t~mctz7FuuI*OF;kcEkZ-T=vh9@_>>#2zu7Qk!M8jy?%M6wkCmlVe?ID=#%l4}H zt>rLaTTPNiO9>)2H>KH$m88>Fl8RDl`cd3SR>LM+65FibtW8W*lU1eaw-olUQpo;i zstUUR0!@p}v>E!rnry-|>|ibSt)Qq(u}!l9bZY4wY<*n0o^+mEk!_qESSdFn^D_I6DcyL-Ah9?Un#0li12mRq!(0-69zbH>zK-yCK3zPC0<)XtANeEp)u z*GE;peqn;IpZ^qJKle0WA9<3m50CS8`0w)dp{s~upt$Tm@G-s~|0rLN{TyG1 zKEl_d;)Z|Z1AIMvh_8np;p@SN`$4Y%=lcK4 zFX#GyuK%aHT51T*_5WP|&-MS@ynf%!>$(1)>;LaI<9M$B5BfcSruF~(W*is&|A7bp z?7*SF;w%5;Es(dsd!+?_=aENp*oc5Za@ffIhmDv;)dU7na6HE{ybR-e4jXwnhmG{{ zB=^=c_7(5pv2R`7n}gq+*R`VM3dq9N?wf;O_dMGI!Qz>ot-FqH=y<4f*8{$zdlr~D z-2%wA4Sp^I6bF(9BpBcbi)V;O%&>Uq-Q(Z7$f51PZ@OsbsyUCRTcsu4g5bgi;58eL z^Zf$9sg7+ME~q{m+>>KUMK@luj<4zCD94*oxymJ(07{B&7cC7peZjNL!D2^sfVcvC zT#bTub-d{4!1p>$fO+H9cDvCkPEMACxFArNq#1x%Dd1zA9DqJe0!ef=mp<{pExDi{ z9h+mOYP(<_0q^Msa4plFbZ#v>H{-Ht8c|6rG=LxqLNU3ywmAH{I#@n`@P%>M-nSQ4 z!)U1r4$M?_5L`hhNU?_C5~=TtClkT9&33`bFFn<-f3NOo#d{F97B$=q@Y=p13|(_{ z%eHLyY3f~0Y649Is>$pyY*vsi%3FYOLv6p$?_O{62ECC97>smrK>lN3Wb(d0{QS9ImX)cuqMsPh7gTICFXJf-#j$rtOzGdQz~b zw4q@z7;P7zplgAhbtg-VAf~u(_?liSJ7vrB9gPzeM=zI4hSwW6J>=kWr|y@_QJhe( zj2dMP+`D}B>#u{W-2rTh)Swnky#OW{KwGBN5Z03$5sQcmQkVJ2-?nSps{S14OiOtU zJn5&O$e35pr9)>M(4}E$pa%^mqo#uC0e#6G17m9gba@H5Z7K1afeeWuxTOvFj1k#| zT&k)A2pQ~w12)aHHCe)U1GzL@fv$}p<+g)dYQW`K@=x9Zf154vW%JP3@6GN_kRuQRXs-$;sNk3a?_FPA-W`Du zX>^2Px~^kco`A;%l!U}-SQg@+XX&XND2tM=nk*`WL0BgEXva&z+&XZ3)){t19ZVuB zS65Pch~>~giJNlJ@@PDhMVpcm@ZhR)5c$t@k*Qc1Ro7GP zRNA!(Mlo%qPYP~Y(vEUF+a_&lAud|H$khb$&PkUD5svE?Ev={BAupoE4H}cD7`&7# zfo}_3Uq>uQK%>kW2a$1Dgn7L*5_ffF&@5q_SGiPDlO}NpLD7kE;Q=lIE(RNjM7&rQ zu`JTevd=kmG(w?g((&{sd+MfG6|sS96DofYT>-#ZMb#?Et~NO2vm;p~4rBUeBW$*x zY$IIn2M!HF0XJT< zF5d4fZrDZ50zqG}X>`+GUl2B8TUM)R1`->xT3rK0+i7(OEm>1;bU@Tsec-M>0VB0q z*j5_E`?Qr>#0fCA?J99XplOv=pQA{|<%FmyC5nOD^1xO(jKd>JE8&W=A1i=pIV%OP z@r0_f8nyYVa#Ze0+Eu>GO}{blKtBk03n75ieOu@v(??>U45l};%VEjjj!e0x9!JZZ z+a%JM>mfxtp<5o>iRKYpfwIH3L-Ne{?A**54vt{|<%Oy6xSb4RgrzdFiem>_XjxX- z3T(aN8>O&fsX-}l_AJ9ZUjB;rXs-XG>F4@?8ezA-K;-)WofsT){l9B+$@?OgxA7yAE$2hSdO{Hcd;^H=`4ix&9e!4Hgm ztp#rN(4m9!_3A*dHrg5S!h}paXcMCP4eU}YQ4l5rhGsN{&4%XH)zt#=%k54nOpUGS z-IJXa_q1tWop!EY=ZXCV9MKH}?y#|R^Zl!SyIg&K<=NpUbmJn&yX6skdAlZ2$Wzs@ zyo|uBoa;uIT@3wN)Lv6wJb89ze(Kaq%7k*d(@yFUKQ4ytPBV?KzDgWf*sKwJP1)Mj zPQ4WS64kzLPiF<#T+FDjhLL_Lt>Q{FSii|u5QG)KQ)?G)G`6yj53JMrmUh~6m78<^ zvbH=obGh1ZtJU!I`DAAG;`GE+y>j%#`SNU~vOIP6^zymm(>*)w9h9oy2UsBJ12z#+M~wHPoNU#9Po2Ize{OaLeuSq9l2=O6Ex*|el``5-QdepyCpEMb9!y|hHLNw1 zOYJCLUQ6TY&nBorWf>U#xI>}>t06i}w_vD3s4t4&lxSk5(YbYtGA|L;o;gAoIHffr z7ehZCXL6@pONDC5QdCagh?Aw+!6CsogdKsv#SY{ROUe>Mkr$&aHT{_2#U`p&OUl%u z<=@HKkDKhV1tp4;q#hhUS10HRO2MJePWokIBnZ z^?2s;`K!z4FO-d2;pydTr)NqRPOWZqZg-YYtBtuq=cLR4`w|V@s$(@#Ug^knEH_v{BG!(S z#A7M(YehL&5&{UhR(g)_W~a9hE16@ZI%!gm#0c$+={T_JiY#$Hr5u_gmh2opIuytPlh;o zWW(}HB|@m5iW96m@$$z%{&51DXX3EY_9sszjfPxvq7%n!$C6Ts-~HkZK6d@Qly4!P zWo0T;zzOA&*q&vTx)l0p&Gv1nInlVKMi)6+fM7smo7gx_Km}LVVc(ogBK!;A!NY2!!)01aMwIO*r9W( z)L)owg*UKBW9SCSfqB|;b%7)37Itd9g9es<(-Q}-9K_I!)tm=HlVm_+lA8M+%*W!L zv@qej_yfc|jX{3YvY&05S1kwPCv58UDNa>Kyo;EwGtB*1ye$puK89r|Ik2?Lm6-el zNP!_6Q)|!Jkm(rMy$=B6J8Vssiw|F{>@*H9%Czif8u3$$CjJAlnd4XG2e)S$@iScr z2-tW+XbPGDhpp?#?Ww&UekvC~fQ^4d^`x1y?e&>X1bM(HWmSe1AFK2xgZ*~oQagfc zG;2cB_%n~`9aBG32qofPTi>ZDU?_$JqtTSk+oScJIdAC&+%ZTU8P@mDU>r}WW>^~8 zQlbj>?-ra%N+U`)2gJCFa6KvYq!M-I(blA_Z`kXT4%>-{aCLvVE9SK$v<>X@6n9&L zIi`nEv1&lp+W-O#Vkm~K=ExZ9X7kNJ4cdj}r*lLx%J zP<``T-)}AO*3Gf8uld=EM!I-2M-29xRBPEr(Lht!Yz4?xG-e%V2@Q48yegQcVL^CX z<$AJ((%f!M4nJTEJ>z^cYSopC2z>bDUObs9m-o)VF;JvCHE?VVL~FI$r3=C7PWa07 z^sy7?Iv1Li#l}?m^6K%GnHP@gE85hYHn%hvhn}0Qglt!;e!K6#fnz%h#(cGzbe5{h zJpS6{R6pTYtgIU_%+!F<2Iq-+V#t0$Vw2Pe&>xSYZ!@nK{vA{7~YmCFCHBnxai3v23q}i!&T-qpiEi0*VLsdKVh4_iCMMQa` zdn&cLJki=W#p7LI3)jTW$yC6(;io%)d7A$=T$B@CPi?MR| zTyg5WigUvht~e+m2W7NI(u%rBTEHea-1klG>G2JMP!SL!2wK}xfmRB@WVa$)R|4Je zz`XVvjAAn6g;-1sam&z;#=f%DSyNKeE>i#?H_EIj@?$^bttiF*LbjnRt;^ja0AMOVDP3)gQ9BV zB{I=DZD@g9F0~V2m9j8HfZ3z-T@Z$% zr~`)+WE^XSp)%pXy~|+8pmqjBmk9yPg5aUjo9MS5u_O>03$?UBo-$N=Fn};T^8qR7 zvBD6qV{ZY>OG&v^O?aSXk1-2G;|%Qt*}%M+C+o`j>FF~wb0>x0SeD&Zrz!G9D6WZZ zf^T~E0rs9WjL8D*W)4NMZ0rO3ht9*933IoYu|QVd$!0Sczpt?k6A$1OFv-xX8TNtK zUYlXtA?AbzN!?uOCL{La5JL1qzRhg8&FpN??Gi3`@&6AU{Lz6AeDU!wJ+jP?{PUh@ zfiJ2b8T7R^r;{E9L9@3#-}M_)aAo?uq?$3Euu=TK3Oq**Ez62lT*CX!Rc| z{Nu;`|C4wA0sl--zw_mV`1T*a^VxUay#1H__dVtIcW-}#e)|LE_7C`T`^Os^>&kMf ztF{(zEyjzDuD@)s%a5$z%ddYzzy8VXe@p-W_?_QY-g%3nyqO;U7kqy;EzY0c{@y!Z zU|{?%N51pUn+%XQ($b~H%)0wr=|)&?E7*iqk6`C%wITr8$>70DCyvg{9#Jk8P85z7 zW(u1?XyE2y3>U{s5-|5z|`>%~U51;L|0E2uZNOI3ug%dZbm)vVdPx&Y8 z=}SvBH9U=BTy&wH1M!l-7Wnm8nQv#f5b>)Jj)L0?$T_|qn6kNXdky6E@>{?5;jyoN zDtpNiqrAAr2zE3aSziX0tlmPgT@Ww9KQ6e!qHYv;z@WgjIfxfu+laj}D;RPkyU$j^ ztH^=q+C=v1B9uDnrhDDXEev6w0(Y6u1Aeg^(-em8^d?ts556U!%7mi+no^A+c`793kTJ;MSvW878=a0qLMhn)qfX9le4Z)VC{P$t^iJ zG~K`~S*}s>@oombIUM*aiEY|z1t1kvECb7Y58gcrUCIo$-D!PzEea8$a-O7YQAy(73Z3FY^{@&PEW1Q&0ect z)Mm3~wVg^f+{pVWt3};b71Kcq1ob6z0atJxN) zS9Tq!3$ffb-^GOu+%^d5I@+ZD zg2j`3Z(Lrk4qPoS;lnWA2WjkrJGZP|Sz4Q2nssJx8m(*Y)#~{HF3#Q;r$uvovjt-Co5&#k$%9VIy@XtWVz#qVkX*II}p;;6P+>EwEvP;K;O#pDB z_%*VfM_Jh|1+F{dd&+bPWwRw{WC%oO5GDpVg+PGv_T2+5ZN`gqE$CgyD^UC8b||>$ zA`Z|?tW?j!aE>X9cAXAI_!lwb7g~p;M6_&!| zq(tpc={Fc6UP_(e;@)FD18cy{!fB{V;a$AdbSTddCNL^2Ls1e*!u2H96a!W;dRv~k zYj;?$kIb}>dAcJThmCrT?+g8r*9k?4ya?SAa|GTq2s%imh}TgLX8Fy?n(A@8{7)1L zzQ~mOBGd1d$p*rEnZu&EQ7tqxP43U+!(gKmr>9_(f$icKOh;H2$491j6X-JPpCgPQ zxMmqe^RS>`skv-?1qz&?X^4eDC1lrS60Mf0n|)yC$tg(nb%g~9Y@ z28q8PbaHnljPQ2QbKqWiAZjb)9)6G1ktz2GI|>A)FvFN%8;2C#I3K}X`H z{$L7)ln2lObr3ERAcjYLGLi5)7TzHk{o5l0t}%GQI#RSR9AYxxBrf8)mivv zs9o5WDW)p4EHJ1@ZCUfAlkkG_2cnKHbqNla_PKB3woQQ~|T=1aYR{5mL6Zjn)XVI0L#ocu&p z)}CJ&rykq6_&p)u(XDw^@3Z|NOxzo}wVzy?%^J2P79Qd_n3@7G?_rNv!{r`|CgW<{v41NSIUHfsjffj=NrK`ih7BM9VxP4Ar6k@( zT$c^KADCh`eKatUy=BkO+y(rWut9e{KUqC8var+C+%U4Rm*96Ck-otaUp zpef_;EeE=hK=Zgc zoTpt_e|&jm($XfW2{^^Or16N8`MG53MD)t#=Ioi~c_&z%SiNzpQf|*&KN?uA=Jav< zT*X}t8)_d{vu!#0o*6L5$U1|bnV!LJ9&znTBm{vlCkivhad7b^zm-AN!KH9!AjIH= zS!*H0M6?_33&PeRsi;93xCUuQmmp(>V0CE53(D~%eiB$pql>EV1kezI1WkiLj`U7e z#Eql)OX5$1hEQBRJa*GX0&Gz_jrHRaRw;3kK1K40o5Z0ZZi|AreZ*U&G{U3-vSqkD zX|ixR+Y-tSu&B}s-TRamPl(HBIwsBpF2mzziERj98W;BGA*XESd>oKTG6(fn;-4S; z_R+xwboa}nB@C4UDK!hotoLMj%&>lDDiGO9O%{NA3lsVKhsqC&$Y$(M#!l0Z!`YU~ zKSu%W9goys|8f)%p-_9F|IblCJM;hJnUtsq3vm!4hAeS#d;Y(i|NkEG|37-*v6~M( zx@WZ~zwy1^0$+(gmdjnZMsm4p4;0F|+?C5+o1tafqUHyX-`RA2yJfjA-Ja@M8M4F=kQ%5Ms|InSnmpY;O_(PqsDQ z!7GRp!h_BO?jY#r4(Z-9TjA}8e>vZW^L@AvG8l-=>V;lyPj{O8|0gG9#wN`mWq9kY z5Q3r_L86T=+F1YwmVn&fV)Z-Kl8 z@)pQjAa8-Z1@acyu?60K{#VDoS-uNkmZ=pDs{qH={Q_p?bOm9`CRXpnQP~%x@FIGo zu#5$ZotU(*V%`>)pr+0TR#6Lw8u3dD4T+L+-5-4$Iy8pe@{%)19D{!ve7 zR|y{W>E0=df4^O|wFI7Ia`EjG@_%=gzH7~Y>fE(+z7dz4#p}+s*%yu(t7l$Ve&O^q zoNiZ+yBDXf%~U$IrPUQ@ZF(08Kt{NqCwHQ29!>y~K`wZQX+ZW^g53n7!INkgHMaom zDv1{F*bcu=003nO)T5YjAz^e4=sk1&fA9ML0|)-wfrGdC`dRwC(f+$*Z(h%KFhD*5 z=Qljyuu6|+ld)mB*5Rc!iFqOy4|HOz5Zn0|INc=^BV#SJZbBg}jtk2Zdbj)RSNPV% zuZ{ia`8`V{5GdIqk-%Dfza)|zDh^UUhl=O)@i~3G4k0~s?H1EP6$OHN~4xhFiCEpxf$BxQDEKA zE3*mQrnP7}RGxX}Qo}EY%E@X1L+VuG^F5#Re*E;a|46wM`XruHY-PF;wUQuI97PVF zhSH(Q^^fNo7$VV^-TPY0hF%&tu$)ccL9WRnwV>7F77Km^| z1c_{vw3wao1HXf3l61}{jiM#_9|v&d!}76Yua4Iw=SnLb#w9;k3R{y)lEizm9>r0i z+OF5051#F2Ide!s_R03Mc)UMZel{?)fMk1;8m#2`w&j#;-wkcjp;c@xG=fjI>QV4) z>`%0+WSyWCJ^9pcwxe>I&a0nL@Wp2qW8M#G>`WR6|cCH6` zM)p<>O3_7x54T)YCtpWvBD~q2&#JR z*iCoVoN?S2PAy+=CYEz;@!Hk!LQ_4ipSfX9e!@5re8M=s(a@jJ?KTH!Y<`vbwNr!l zyvn<33u@KZLC8c2vSFB)ub&m9(utL%lEz)v{N@r#+2+Dk<$BUwR_0~UY;@KUrPOJs zDMreqd~YQ+Qust9Z{v4}f)ca}qq#xS5S)=1o}Dz7H*P9pZ~9xf^gMDhlI(_rqjsA4 zXRk3srDoy>Wef*2R9BC?+i)SXtZF6OE0t}hO!S!-I3+yRmd|J|lRn6jD5{;R{<$l^ zKK8|5%bxV8(!*8X^`tNNCtP>pr}KVN#^I#uOlo@9^wFEVn^FPY*~}#m=g7>hua@hw z1f|PQ;MXVX%k4#?`NMW=GO8~PE=7%a>24Wjyx=a+6e&P;D6-ErCaQ%X`szo+Ju#Q#5h;GF{>`0C?dfAGH_{Ez&6 zZ-3rsKe?6PUnh4iWThtAb6v6$Go*$WiCvrVdU1vQ6@t{m?6&0DCE+!xt4Y2|jau27}TTs${l)_}~^g!$G(B8N|Ma$an zceEt#&#U_@2vtvkRKr7)gvsPxS50yhlOCDSRT^%mJ>zZj6Jy_;-7^$~JcGKVv^60u z-W6*gN#$B1{nUkZm}GfPzieivS=k=ytvpU%j*rrQcNj_s42eqW+*A}}CpPiaka=3HlNCw00Qquxp za{RyugD~_g$uI4MLEuzKS?-5=#VV&#oe=H_hPIj1!XCj+|4;~9&l=1-Jql=29Lur; zzVp@b{?(G9e3#wcjrg-nN$SlSP73#QfMg{tCs93VzkzTuX-GuwkS$Th67hy8Mi3XN zDxfY~;>r!3rsR+#a6-ukFPZ?2SCQ(J9rU^5RB6b>WAkN?`_O&yaa-0U) zKC3Xf?I~VLda*t|Vf&AQFp;&j=dq<8nd{EM@yhfp zwe3qGjxK7V!BOXPR1nkEQd*6bXs`~Gtsn?1ey7&H+XS<&+BtvY*yZ`)l-sG57F}!Z z!s7fZ)90P*?bQp~*=udPKAT)$oeSF+dRpThl&as(xPfVB#7^nIbZ^|Y&1uk{e04qP z%+rxF&xQ>+^HoUm_`}ZIH{dKaL%&V#S*$rxyhIOFNg7-ATZ(MaN=ec!*L|=Mm~1uz zItmN%eASPa#jD@Y?9kQ}SjO;Ym)w8F+dvDNsUIX!n#pKe@IZ>*+k z{ModG3W;<(wE%-wJxtq;7Hc3TGJeT}OGj+vsSvsvX4}Bx-5i7>Y<_QV&6>PCRgY&b zpTD|%{zBQf6`o$cc6z3C;neCzC!g-LlX~=IYoJ?{DLN$%YvUP8!bngAPufXI5dHsw z2ma>3V^{dfKY0thFIwQsFFif>`@eoSs0vL!QcekEB7x&EcO}px8bjpb!7t8EpmJ5OAi!4sl;h$~oGq)(`#s*+E;C z)BJxA9Qv0B9{Ubo`6q9IyanPO8rrP2&TrJmfn1v62t<}bv6 zmqG%2r-zg2JO+ReGs zYo>apQoVWd>ZxPyjT`Z$TBjPHZt6D|kDXW+vY|9WQ22#(7k;6x;koJy#p5zI=P59vf?KmmFH5ij)2 zl3qxaaQJ~SompqO35q2i>5}QhFw$`OJJ5&rqt0KSEsXu)Z|&Lrtrsn~sF86^+hybq z=*vE#L7%l?;LvUYIr5BacbRdG%pefgc+l#=`e4gY&U9t={EFMix_*E$`ULP?sSOm_(r*PNjVGqefFoY+lf@=?n~Hz^x|^$v78 zj_WXiWoGVXf6%rhqnlfux!O5Bck-2^^|@1LTh}_rTT|y7!I>q!U0=TbO7n!(nQOe# zHkzH?G>LB(?Q;rwJ5!$xgQ z5W^sUh5B#$W+QC2338oF*tL~QZMJNxoUuh@xu?@$4q@NiTd7`u{m9PZh&!P%a>&%k z(xDb0Js2E0_c2$W3$2L|Sa~F`a`ijS)psgH`blcbt%5Ssil1yNP`JTaC_&=V4KGX_ zOWFicD`~X2B1Did>-%v*IWOV>1QfG=6eP_Uf(yS^TT>da{z=%iU`2p|&qhI-m-E|4V21 zZu0*j(7Q#I{C^%CKG_C_u%6tAxQt*1R~8eO3yFi$pCh|Np8pU2r+sCUP5J*G_{@RF zZXUYz=xdMseSYSjyajgM0&o3>HTKmo+XO&%ML4q@6a^B02#MAmm;l@f)tT4yqNx@Q zy&!&_0T!$-b>Ifs4Ge^v$!d0^PWHJ;hE&*gu)Ml@^vb!?jmxKAIo&+gYRycQO6Sk7 z?-Ik1xdArdtp%|5d41~Ca{cDniAH1QO!K^IwAJ{;%8irf>t@xzwRCGGQLnU1?b_Tr z0qHJO@H;L3I>byB&Y1yaZ%B6SHC%4Mc<&f6Y?Zh_2uL+jm0A{3xsWFg^lzpz@4!gz zp;XySQIeM8;pC?KtjxppWp2s!Md-5ynX`?kizYv-}izN#DpOoVQnUL&a~gW z@v5<9?JY9YLi7kRo^4v90uI$4hB6jb3HL-Fu&Ml(=9}!=%l!-$=oAf87|+uC?|bd_ zt!odxEX1AA%tFZ~Jv&tUv@wOnmzFUMOgVR0drUthCZp{<+7|{t_1^mRGyDGShqXML zOrA}q4=KJQ$w;$=ACyGRmRfKj1RRiz?twL;%e=}Sf$a@5i=la4BbDhwD-9(flZgfB zPOTBev(xjJPF$WklV_88KV_4#ie?HhfN0a*f+co4{ol*8$=n0||A7Pl^HCi4{LaX9?4eQCxuO#o?_LX)V0MlH%gCbbQgh5 z5Cxl?HPu}|KZ~4xK>gg>KWv7xE337&IsZm!RXcw1>V?zwR$SJtlgB$(+pFP~#gf`M zd+hkB>aEK?5Ad$b)YmDIrT2-D@|!h#?1x^@!FTZBp}u)u^9UVl!!cg*Yb`uVY3ic3 zUvH#rK)NxZcFpt-yP`mt=-|P#UI{98s$E`a+B` zp6a*Sso$l85g)5R&P&qgDpgK!!IO0yGKo_2Z>@=EhSa1rA%w-$pYpS$&%BAnUnpN% z2~@98%0eG2Y^*+hg=QTp*-8_CS-?3A|4JGEKs$m4b~S2O@heowAG8+wO?(VXiQ-o% z5LYch6H{q+07mdUZk21R$tu8&T4lm7wUTCu!}tw^(-o(j)LeSqdD%Rg==*wmPjw2p|J*6jk6#7;N!Dr3AS!eakzm zQT>Q=g92u}LNE;rv7|prFYsZs>3}4W#)Uk#SJSk`s6bipBc#6~Ns{V&ZQI`G94PJj1!V;<*jHkrOs#K%*Qfb(J?PyV!iIQK!L9;6SwZgO0 z!f~T4jMMm#^C}Di`V?Zgz!xrc#(>l@eTUS0VZ2+TZU>=vrAgWBDyiGrOB2T z;xy$l9Zii==AUYa7jG$yJ3wl9KYb^Fn_jhg+{3H#zAhKAkPxSLG~oyq9WQ|)D3&of zUVM?^J*4xEN}=45sqc%gcB#46s3u!erE4i-oLm5=Lly>BrwZ<#n&=e(y`2qEGX%61><~g1(SuVw}vfl zvu141{JV4WQj#9>LHc8e!E}cvzNaEekKzgPUpGj^>6Aa)X2v5X$p%JX>-QM-C6duLZHBu2DTXXz?uDa}B&+Qd}!NNPYnmxnU^t z1up~)L3RqsS|Qdyc@Y5w1{r?v5qo4&@w9Fh6%sN?3D`|2lM1(sR7oRc5qZ8$QUgJ$ zE$LBY2GERT@WF`4kcD6$^GNThXnK@~dU}wRnbM?&=YRlGQU7QqIW0xaN|JG895)`c zWOAt^S%N4XmrBhY%#PIDGwD#%%gD_F&7ZDMsOE%*FTg2+2qYOi0HmSqmmP=odtEN* zF^Uc$3uakVk|uB6CiMoS54=b>B$bOn;(4v0(r1IJ^?Lm8zNB(_*DOJ-w0DbeV%({= z#k}N?MZO*xOPRBCi|Hk#I-4d<<~+`vNgz?kdBG?qZ42|26bUjCGp9m#6WKJCvIi}h zVkFlUIj#`*bf&pHHDfB#dUudeX#8{(-&|Wfoy0eH9*y>duFh*_(L$uv3MMHT?&WA? zwIdAD(kQ+Jlft5u~a1MD138~y^MoBAbuN^^X3sTy^r3h&S zkVqYIZ@U?nk36zr48|aG`RSV>``t}M^4oXNb-Y{!HD{p(nN1c&Ua__C06xB<(#e-Q{d~&wM!E^sj0*sbv7w4FT_X|7`YH6 zlqZy!yj7gwmGbY66hkw|#Xut%*c9hFHG5o^#`iL=)%@6$y%p%GkR}@0gZCLSDc&cslBwYa;Q8&bhsnK}ZW9 zhqUlby|}d{o}_iCm1yhe*z(9dSmG`F-df@}J%7)Z`00z6w2Q~4&a|h__~$CM*goMb z&NQoMYA5Dr>~qVri;bIWmoLW0XG%wRw8U>J)BaoH-G^veZKut~>D6UTueqh!lUI)2 zT)lP9I@!?U#Jn{h-B><$bj?kUolI_CJ-)NwXH%IrkG4@)#E?#i-^44bB(y+5a*ZvP zez6qGBHJL_CH@)7_1Q?oaS;$kpkOT?I+@zVn`{c{Sv(S{(Y~Hws7JX>FHLjGrN(AS z;-<99&8W>Pj~jsH9i@^3lKXC**>1NTo-gtv%(SfWEb{Kwi~QyZxQ9i4(-7Quck8C- RzZa(6kc%W!yx3`f{{Qk~$=Lt^ delta 59 zcmZo@U~6!gAkE6kz`(#XQNf;(bz{N;ejZ*Bm*X)5?<$VR8w;;W-ZNYYyh}%5On|m From 2973170e877aa3fecd1a5f2cc9a60420115ce4cb Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 17 Aug 2023 12:35:34 +1200 Subject: [PATCH 46/99] test for editing a new caption onto an image --- d2m/actions/edit-message.js | 2 +- d2m/converters/edit-to-changes.js | 9 +- d2m/converters/edit-to-changes.test.js | 45 ++++++- test/data.js | 156 +++++++++++++++++++++++++ 4 files changed, 202 insertions(+), 10 deletions(-) diff --git a/d2m/actions/edit-message.js b/d2m/actions/edit-message.js index 933267c..6823602 100644 --- a/d2m/actions/edit-message.js +++ b/d2m/actions/edit-message.js @@ -9,7 +9,7 @@ async function editMessage() { // 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? + let eventPart = 0 // TODO: what to do about eventPart when editing? probably just need to make sure that exactly 1 value of '0' remains in the database? for (const event of events) { const eventType = event.$type /** @type {Pick> & { $type?: string }} */ diff --git a/d2m/converters/edit-to-changes.js b/d2m/converters/edit-to-changes.js index 4afa3ce..f055c90 100644 --- a/d2m/converters/edit-to-changes.js +++ b/d2m/converters/edit-to-changes.js @@ -6,8 +6,6 @@ const passthrough = require("../../passthrough") const { discord, sync, db } = passthrough /** @type {import("./message-to-event")} */ 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("../actions/register-user") /** @type {import("../actions/create-room")} */ @@ -18,8 +16,9 @@ const createRoom = sync.require("../actions/create-room") * IMPORTANT: This may not have all the normal fields! The API documentation doesn't provide possible types, just says it's all optional! * Since I don't have a spec, I will have to capture some real traffic and add it as test cases... I hope they don't change anything later... * @param {import("discord-api-types/v10").APIGuild} guild + * @param {import("../../matrix/api")} api simple-as-nails dependency injection for the matrix API */ -async function editToChanges(message, guild) { +async function editToChanges(message, guild, api) { // Figure out what events we will be replacing const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(message.channel_id) @@ -76,7 +75,7 @@ async function editToChanges(message, guild) { } } // 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) + eventsToSend.push(newInnerContent[0]) shift() } // Anything remaining in oldEventRows is present in the old version only and should be redacted. @@ -102,7 +101,7 @@ async function editToChanges(message, guild) { eventsToRedact = eventsToRedact.map(e => e.event_id) eventsToReplace = eventsToReplace.map(e => ({oldID: e.old.event_id, new: eventToReplacementEvent(e.old.event_id, e.newFallbackContent, e.newInnerContent)})) - return {eventsToReplace, eventsToRedact, eventsToSend} + return {eventsToReplace, eventsToRedact, eventsToSend, senderMxid} } /** diff --git a/d2m/converters/edit-to-changes.test.js b/d2m/converters/edit-to-changes.test.js index f6ecc8d..8385cd0 100644 --- a/d2m/converters/edit-to-changes.test.js +++ b/d2m/converters/edit-to-changes.test.js @@ -1,12 +1,30 @@ -// @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) + const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.bot_response, data.guild.general, { + async getJoinedMembers(roomID) { + t.equal(roomID, "!uCtjHhfGlYbVnPVlkG:cadence.moe") + return new Promise(resolve => { + setTimeout(() => { + resolve({ + joined: { + "@cadence:cadence.moe": { + display_name: "cadence [they]", + avatar_url: "whatever" + }, + "@_ooye_botrac4r:cadence.moe": { + display_name: "botrac4r", + avatar_url: "whatever" + } + } + }) + }) + }) + } + }) t.deepEqual(eventsToRedact, []) t.deepEqual(eventsToSend, []) t.deepEqual(eventsToReplace, [{ @@ -39,8 +57,27 @@ test("edit2changes: bot response", async t => { }]) }) +test("edit2changes: remove caption from image", async t => { + const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.removed_caption_from_image, data.guild.general, {}) + t.deepEqual(eventsToRedact, ["$mtR8cJqM4fKno1bVsm8F4wUVqSntt2sq6jav1lyavuA"]) + t.deepEqual(eventsToSend, []) + t.deepEqual(eventsToReplace, []) +}) + +test("edit2changes: add caption back to that image", async t => { + const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.added_caption_to_image, data.guild.general, {}) + t.deepEqual(eventsToRedact, []) + t.deepEqual(eventsToSend, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "some text", + "m.mentions": {} + }]) + t.deepEqual(eventsToReplace, []) +}) + 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) + 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, [{ diff --git a/test/data.js b/test/data.js index e23d42f..bb2570b 100644 --- a/test/data.js +++ b/test/data.js @@ -867,6 +867,162 @@ module.exports = { tts: false, type: 0 }, + removed_caption_from_image: { + attachments: [ + { + content_type: "image/png", + filename: "piper_2.png", + height: 163, + id: "1141501302497615912", + proxy_url: "https://media.discordapp.net/attachments/112760669178241024/1141501302497615912/piper_2.png", + size: 43231, + url: "https://cdn.discordapp.com/attachments/112760669178241024/1141501302497615912/piper_2.png", + width: 188 + } + ], + author: { + avatar: "47db1be7ab77e1d812a4573177af0692", + avatar_decoration: null, + discriminator: "0", + global_name: "wing", + id: "112890272819507200", + public_flags: 0, + username: ".wing." + }, + channel_id: "112760669178241024", + components: [], + content: "", + edited_timestamp: "2023-08-16T22:38:43.075298+00:00", + embeds: [], + flags: 0, + guild_id: "112760669178241024", + id: "1141501302736695316", + member: { + avatar: null, + communication_disabled_until: null, + deaf: false, + flags: 0, + joined_at: "2015-11-08T12:25:38.461000+00:00", + mute: false, + nick: "windfucker", + pending: false, + premium_since: null, + roles: [ + "204427286542417920", + "118924814567211009", + "222168467627835392", + "265239342648131584", + "303273332248412160", + "303319030163439616", + "305775031223320577", + "318243902521868288", + "349185088157777920", + "378402925128712193", + "391076926573510656", + "230462991751970827", + "392141548932038658", + "397533096012152832", + "454567553738473472", + "482658335536185357", + "482860581670486028", + "495384759074160642", + "638988388740890635", + "764071315388629012", + "373336013109461013", + "872274377150980116", + "1034022405275910164", + "790724320824655873", + "1040735082610167858", + "1123730787653660742", + "1070177137367208036" + ] + }, + mention_everyone: false, + mention_roles: [], + mentions: [], + pinned: false, + timestamp: "2023-08-16T22:38:38.641000+00:00", + tts: false, + type: 0 + }, + added_caption_to_image: { + attachments: [ + { + content_type: "image/png", + filename: "piper_2.png", + height: 163, + id: "1141501302497615912", + proxy_url: "https://media.discordapp.net/attachments/112760669178241024/1141501302497615912/piper_2.png", + size: 43231, + url: "https://cdn.discordapp.com/attachments/112760669178241024/1141501302497615912/piper_2.png", + width: 188 + } + ], + author: { + avatar: "47db1be7ab77e1d812a4573177af0692", + avatar_decoration: null, + discriminator: "0", + global_name: "wing", + id: "112890272819507200", + public_flags: 0, + username: ".wing." + }, + channel_id: "112760669178241024", + components: [], + content: "some text", + edited_timestamp: "2023-08-17T00:13:18.620975+00:00", + embeds: [], + flags: 0, + guild_id: "112760669178241024", + id: "1141501302736695317", + member: { + avatar: null, + communication_disabled_until: null, + deaf: false, + flags: 0, + joined_at: "2015-11-08T12:25:38.461000+00:00", + mute: false, + nick: "windfucker", + pending: false, + premium_since: null, + roles: [ + "204427286542417920", + "118924814567211009", + "222168467627835392", + "265239342648131584", + "303273332248412160", + "303319030163439616", + "305775031223320577", + "318243902521868288", + "349185088157777920", + "378402925128712193", + "391076926573510656", + "230462991751970827", + "392141548932038658", + "397533096012152832", + "454567553738473472", + "482658335536185357", + "482860581670486028", + "495384759074160642", + "638988388740890635", + "764071315388629012", + "373336013109461013", + "872274377150980116", + "1034022405275910164", + "790724320824655873", + "1040735082610167858", + "1123730787653660742", + "1070177137367208036" + ] + }, + mention_everyone: false, + mention_roles: [], + mentions: [], + pinned: false, + timestamp: "2023-08-16T22:38:38.641000+00:00", + tts: false, + type: 0 + }, edit_of_reply_to_skull_webp_attachment_with_content: { type: 19, tts: false, From 040a2d253ffdbdec78afc6dd2008b1e629d9caf2 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 17 Aug 2023 16:41:28 +1200 Subject: [PATCH 47/99] write editMessage action, connected to dispatcher --- d2m/actions/edit-message.js | 70 ++++++++++++++++++-------- d2m/actions/register-user.js | 4 +- d2m/converters/edit-to-changes.js | 10 ++-- d2m/converters/edit-to-changes.test.js | 4 +- d2m/event-dispatcher.js | 22 ++++++++ matrix/api.js | 14 +++++- types.d.ts | 4 ++ 7 files changed, 95 insertions(+), 33 deletions(-) diff --git a/d2m/actions/edit-message.js b/d2m/actions/edit-message.js index 6823602..9a329b6 100644 --- a/d2m/actions/edit-message.js +++ b/d2m/actions/edit-message.js @@ -1,28 +1,54 @@ -async function editMessage() { - // Action time! +// @ts-check + +const passthrough = require("../../passthrough") +const { sync, db } = passthrough +/** @type {import("../converters/edit-to-changes")} */ +const editToChanges = sync.require("../converters/edit-to-changes") +/** @type {import("../../matrix/api")} */ +const api = sync.require("../../matrix/api") + +/** + * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message + * @param {import("discord-api-types/v10").APIGuild} guild + */ +async function editMessage(message, guild) { + console.log(`*** applying edit for message ${message.id} in channel ${message.channel_id}`) + const {roomID, eventsToRedact, eventsToReplace, eventsToSend, senderMxid} = await editToChanges.editToChanges(message, guild, api) + console.log("making these changes:", {eventsToRedact, eventsToReplace, eventsToSend}) // 1. Replace all the things. + for (const {oldID, newContent} of eventsToReplace) { + const eventType = newContent.$type + /** @type {Pick> & { $type?: string }} */ + const newContentWithoutType = {...newContent} + delete newContentWithoutType.$type - - // 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 '0' 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) + await api.sendEvent(roomID, eventType, newContentWithoutType, senderMxid) + // Ensure the database is up to date. + // The columns are event_id, event_type, event_subtype, message_id, channel_id, part, source. Only event_subtype could potentially be changed by a replacement event. + const subtype = newContentWithoutType.msgtype ?? null + db.prepare("UPDATE event_message SET event_subtype = ? WHERE event_id = ?").run(subtype, oldID) } - return eventIDs + // 2. Redact all the things. + // Not redacting as the last action because the last action is likely to be shown in the room preview in clients, and we don't want it to look like somebody actually deleted a message. + for (const eventID of eventsToRedact) { + await api.redactEvent(roomID, eventID, senderMxid) + // TODO: I should almost certainly remove the redacted event from our database now, shouldn't I? I mean, it's literally not there any more... you can't do anything else with it... + // TODO: If I just redacted part = 0, I should update one of the other events to make it the new part = 0, right? + // TODO: Consider whether this code could be reused between edited messages and deleted messages. + } -{eventsToReplace, eventsToRedact, eventsToSend} + // 3. Send all the things. + for (const content of eventsToSend) { + const eventType = content.$type + /** @type {Pick> & { $type?: string }} */ + const contentWithoutType = {...content} + delete contentWithoutType.$type + + const eventID = await api.sendEvent(roomID, eventType, contentWithoutType, senderMxid) + db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, channel_id, part, source) VALUES (?, ?, ?, ?, ?, 1, 1)").run(eventID, eventType, content.msgtype || null, message.id, message.channel_id) // part 1 = supporting; source 1 = discord + } +} + +module.exports.editMessage = editMessage diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index beb24bd..1455360 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -43,7 +43,7 @@ async function createSim(user) { * Ensure a sim is registered for the user. * If there is already a sim, use that one. If there isn't one yet, register a new sim. * @param {import("discord-api-types/v10").APIUser} user - * @returns mxid + * @returns {Promise} mxid */ async function ensureSim(user) { let mxid = null @@ -60,7 +60,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 + * @returns {Promise} mxid */ async function ensureSimJoined(user, roomID) { // Ensure room ID is really an ID, not an alias diff --git a/d2m/converters/edit-to-changes.js b/d2m/converters/edit-to-changes.js index f055c90..4e6892d 100644 --- a/d2m/converters/edit-to-changes.js +++ b/d2m/converters/edit-to-changes.js @@ -82,7 +82,7 @@ async function editToChanges(message, guild, api) { 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.) + // (Example: 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. (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. @@ -99,9 +99,9 @@ async function editToChanges(message, guild, api) { // 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.newFallbackContent, e.newInnerContent)})) + eventsToReplace = eventsToReplace.map(e => ({oldID: e.old.event_id, newContent: makeReplacementEventContent(e.old.event_id, e.newFallbackContent, e.newInnerContent)})) - return {eventsToReplace, eventsToRedact, eventsToSend, senderMxid} + return {roomID, eventsToReplace, eventsToRedact, eventsToSend, senderMxid} } /** @@ -111,7 +111,7 @@ async function editToChanges(message, guild, api) { * @param {T} newInnerContent * @returns {import("../../types").Event.ReplacementContent} content */ -function eventToReplacementEvent(oldID, newFallbackContent, newInnerContent) { +function makeReplacementEventContent(oldID, newFallbackContent, newInnerContent) { const content = { ...newFallbackContent, "m.mentions": {}, @@ -130,4 +130,4 @@ function eventToReplacementEvent(oldID, newFallbackContent, newInnerContent) { } module.exports.editToChanges = editToChanges -module.exports.eventToReplacementEvent = eventToReplacementEvent +module.exports.makeReplacementEventContent = makeReplacementEventContent diff --git a/d2m/converters/edit-to-changes.test.js b/d2m/converters/edit-to-changes.test.js index 8385cd0..bb3f3ec 100644 --- a/d2m/converters/edit-to-changes.test.js +++ b/d2m/converters/edit-to-changes.test.js @@ -29,7 +29,7 @@ test("edit2changes: bot response", async t => { t.deepEqual(eventsToSend, []) t.deepEqual(eventsToReplace, [{ oldID: "$fdD9OZ55xg3EAsfvLZza5tMhtjUO91Wg3Otuo96TplY", - new: { + newContent: { $type: "m.room.message", msgtype: "m.text", body: "* :ae_botrac4r: @cadence asked ``­``, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.", @@ -82,7 +82,7 @@ test("edit2changes: edit of reply to skull webp attachment with content", async t.deepEqual(eventsToSend, []) t.deepEqual(eventsToReplace, [{ oldID: "$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M", - new: { + newContent: { $type: "m.room.message", msgtype: "m.text", body: "> Extremity: Image\n\n* Edit", diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 1686b5f..4bb94ff 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -3,6 +3,9 @@ const {sync, db} = require("../passthrough") /** @type {import("./actions/send-message")}) */ const sendMessage = sync.require("./actions/send-message") +/** @type {import("./actions/edit-message")}) */ +const editMessage = sync.require("./actions/edit-message") + /** @type {import("./actions/add-reaction")}) */ const addReaction = sync.require("./actions/add-reaction") @@ -29,6 +32,25 @@ module.exports = { sendMessage.sendMessage(message, guild) }, + /** + * @param {import("./discord-client")} client + * @param {import("discord-api-types/v10").GatewayMessageUpdateDispatchData} message + */ + onMessageUpdate(client, data) { + // Based on looking at data they've sent me over the gateway, this is the best way to check for meaningful changes. + // If the message content is a string then it includes all interesting fields and is meaningful. + if (typeof data.content === "string") { + /** @type {import("discord-api-types/v10").GatewayMessageCreateDispatchData} */ + const message = data + /** @type {import("discord-api-types/v10").APIGuildChannel} */ + const channel = client.channels.get(message.channel_id) + if (!channel.guild_id) return // Nothing we can do in direct messages. + const guild = client.guilds.get(channel.guild_id) + if (message.guild_id !== "112760669178241024" && message.guild_id !== "497159726455455754") return // TODO: activate on other servers (requires the space creation flow to be done first) + editMessage.editMessage(message, guild) + } + }, + /** * @param {import("./discord-client")} client * @param {import("discord-api-types/v10").GatewayMessageReactionAddDispatchData} data diff --git a/matrix/api.js b/matrix/api.js index 9111909..ef3e199 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -14,7 +14,7 @@ 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 {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 */ @@ -119,7 +119,7 @@ async function sendState(roomID, type, stateKey, content, mxid) { * @param {string} roomID * @param {string} type * @param {any} content - * @param {string} [mxid] + * @param {string?} [mxid] * @param {number} [timestamp] timestamp of the newly created event, in unix milliseconds */ async function sendEvent(roomID, type, content, mxid, timestamp) { @@ -129,6 +129,15 @@ async function sendEvent(roomID, type, content, mxid, timestamp) { return root.event_id } +/** + * @returns {Promise} room ID + */ +async function redactEvent(roomID, eventID, mxid) { + /** @type {Ty.R.EventRedacted} */ + const root = await mreq.mreq("PUT", path(`/client/v3/rooms/${roomID}/redact/${eventID}/${makeTxnId.makeTxnId()}`, mxid)) + return root.event_id +} + async function profileSetDisplayname(mxid, displayname) { await mreq.mreq("PUT", path(`/client/v3/profile/${mxid}/displayname`, mxid), { displayname @@ -152,5 +161,6 @@ module.exports.getAllState = getAllState module.exports.getJoinedMembers = getJoinedMembers module.exports.sendState = sendState module.exports.sendEvent = sendEvent +module.exports.redactEvent = redactEvent module.exports.profileSetDisplayname = profileSetDisplayname module.exports.profileSetAvatarUrl = profileSetAvatarUrl diff --git a/types.d.ts b/types.d.ts index 76d3bd1..b9f7ed6 100644 --- a/types.d.ts +++ b/types.d.ts @@ -112,4 +112,8 @@ namespace R { export type EventSent = { event_id: string } + + export type EventRedacted = { + event_id: string + } } From f5ef881cd000aa5840ac0c0320c0a2669667cd50 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 17 Aug 2023 18:14:44 +1200 Subject: [PATCH 48/99] update discord-markdown to remove html tags --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6be3b42..c6b6004 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "better-sqlite3": "^8.3.0", "cloudstorm": "^0.8.0", - "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#df495b152fdc48fb22284ecda9a988e6df61bf99", + "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#9799e4f79912d07f89a030e479e82fcc1e75bc81", "heatsync": "^2.4.1", "js-yaml": "^4.1.0", "matrix-appservice": "^2.0.0", @@ -351,9 +351,9 @@ "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "node_modules/@types/react": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.6.tgz", - "integrity": "sha512-wRZClXn//zxCFW+ye/D2qY65UsYP1Fpex2YXorHc8awoNamkMZSvBxwxdYVInsHOZZd2Ppq8isnSzJL5Mpf8OA==", + "version": "18.2.20", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.20.tgz", + "integrity": "sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -1051,7 +1051,7 @@ }, "node_modules/discord-markdown": { "version": "2.4.1", - "resolved": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#df495b152fdc48fb22284ecda9a988e6df61bf99", + "resolved": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#9799e4f79912d07f89a030e479e82fcc1e75bc81", "license": "MIT", "dependencies": { "simple-markdown": "^0.7.2" diff --git a/package.json b/package.json index 2437aba..6557500 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "dependencies": { "better-sqlite3": "^8.3.0", "cloudstorm": "^0.8.0", - "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#df495b152fdc48fb22284ecda9a988e6df61bf99", + "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#9799e4f79912d07f89a030e479e82fcc1e75bc81", "heatsync": "^2.4.1", "js-yaml": "^4.1.0", "matrix-appservice": "^2.0.0", From e00eb15f35b36e796f3fa02d3f8ac889cad61094 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 17 Aug 2023 18:17:53 +1200 Subject: [PATCH 49/99] support up to 1 space in written mentions --- d2m/converters/message-to-event.js | 4 ++-- d2m/converters/message-to-event.test.js | 5 ++++- scripts/events.db | Bin 98304 -> 192512 bytes 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index c128595..29730d4 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -131,8 +131,8 @@ async function messageToEvent(message, guild, options = {}, di) { escapeHTML: false, }, null, null) - // Mentions scenario 3: scan the message content for written @mentions of matrix users - const matches = [...content.matchAll(/@([a-z0-9._]+)\b/gi)] + // Mentions scenario 3: scan the message content for written @mentions of matrix users. Allows for up to one space between @ and mention. + const matches = [...content.matchAll(/@ ?([a-z0-9._]+)\b/gi)] if (matches.length && matches.some(m => m[1].match(/[a-z]/i))) { const writtenMentionsText = matches.map(m => m[1].toLowerCase()) const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(message.channel_id) diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index 4200afe..df94196 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -247,10 +247,12 @@ test("message2event: simple reply to matrix user, reply fallbacks disabled", asy }]) }) -test("message2event: simple written @mention for matrix user", async t => { +test("message2event: simple written @mentions for matrix users", async t => { + let called = 0 const events = await messageToEvent(data.message.simple_written_at_mention_for_matrix, data.guild.general, {}, { api: { async getJoinedMembers(roomID) { + called++ t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") return new Promise(resolve => { setTimeout(() => { @@ -290,6 +292,7 @@ test("message2event: simple written @mention for matrix user", async t => { msgtype: "m.text", body: "@Cadence, tell me about @Phil, the creator of the Chin Trick, who has become ever more powerful under the mentorship of @botrac4r and @huck" }]) + t.equal(called, 1, "should only look up the member list once") }) test("message2event: very large attachment is linked instead of being uploaded", async t => { diff --git a/scripts/events.db b/scripts/events.db index 3356cbe2ba67631f1efdf34292de8256ab6ee024..d3b817d2a036453e0de94479f85f48b5b73d0bb7 100644 GIT binary patch delta 20626 zcmeHv3ve6feJ58GVTv>aOCl**w(KSRk|+|~_by;dGG7uYQ4%Fl)QdK>02aWC02Txm zq)5l19lzoz_C)#;VVtzh!_Fmk&Q5f0;wDZ!&Z9{??e*-LPJ8WKX5z+kZ6>`Wp1a;; zoTR?re;1%gOLgqHNha4M_L=~8zx^Kn@BjGyfB)~Edjj`-uk}OMG?g0~8hX$2JGkR=v2rGoeZ4_O@t1FMnk(o{h@6k zBP53+p)H{sFHG~?v-hw0L~sEOL{8z-lg6bxh09hGm##Z-*>VDx%|~&$WdfI*594yv zI4(CH#N~zqxP-@W>D-44KZ?usyK%X07cQH2;&SZ(F4y$ovT-{uS8u~*!)>^%*Kz4k z8yfC={QBn}zn(M4RyQ?X)3AC^%QemErsIv*T$oqwU?2BI$}K8HS-nql(e{ zR*$e3za_BiPxrI&KUlXG5BZO3vMvaM6y3u5{_i+D_y_G*z4<4`&GkP!_}wmUwW!1d zJ*HqBnx^X2>GfO(Yx&N#?3ur6UMq@{t_hkJ714b~6eQ)@+dpe?=f2R?v|_Sh#pH$Q zgDV0b>)amv(hck@X}!9?z%@VjHwo^p)(|yo#FS`6!>cv9da}UHsQmsCpR!Z5E_3{; zO4;R;PTpHjTB(v{=InXXwVeXpkuO_0EYA-1$z7e*C)3=PD(`Twwlp?G8d@?dzta#o zJIx0k?&u4iTjQ1G)DERX3n^H zH?~-H`aN7nwRbz$#O~C#AAmqZu3RT$p&O->Cwa4w;=8)~w{&&!Cr|pnnnmoolPA0R z5k8r9oU#?e9eqphxcGeMb3b0sB{}w@u`+OG)9u0Y*Vj6%#H46UHX=q;TI}%36IV5c zWSVa&Dx%jama@90nbU8_l;UmE#>;Y;AcyY zYw=l3aoMdTr!bMb*|qUk17|vR1kd}E)=G_vG0BLCBK!K2Hs-9I{rD^G)oox*MU1L4J20?;om`8bAYz)RX@aDwf+Q-6#C~z&YS#DFHg@kH<7XX{Ekz|2 za;%Gz7?s%z_iteT=b?7iwy}e?|FDC7c(QG+BFMTLjS6@JwvHkj2K&U9@Pg+$Skv=u z%*5^AIMcCK)6k4)D6$-l>Z+{k20Q!I`nK+{ft3|i1OJZ-XhsqA>YA;b(CW^)mQ_$p zK@L|to4D)R)E-gn5#@0~iYcC0Dw2Hez9ZF_wsOswZ4Hepk}4ZeSE@JL3;x8&(Fk^R zRMajr+mPKN7T(ubd6MR=Ib-oqG?;m_kjaszNjb?WtHf`%%0(+_Ln+Pi1liC ziziubX^vD{!Ey1NjTIZjGhwTg_BGcWnuYw?X;O21(k$@ijDt;NvzPA-uaP8CMZX16 z!Xz3b`9CRaXt_B4>dQBCSKlOxiV8u(a_WXELfy!an@rA0n7MdOAG_Ey*SE2I;y3Y1 zN=zkmIcg}0WFl=wEj*o0sw$>PN-HUaIh$@hCp2BL^2ZIWnUz1jF#Y|Nfe%&&f{*R@ z0dh-DB?03G z8&)Pwm^91Q0y^K1=P`CHb;iVRWX7^~VX91WvZUwy(yk^f2wTD8F-cAu3c;5x>U7b` zL98Ho#ft0F3{{H!tnFsKcY2Rt2xvKH=h4Ll&zeHG^OXWd(Z8$EAy#JF=r=j*Qt~j z@C(Viiv=k2l|lm6+@wibY@ld4#hewHEKC+ga8n-5mavCN4?a3jw3I(|*c2MJpBYv?z9S0eX}@y{moE-u%6%v>)#1|JAI_YO+bXZHY9$op^h3PXZn;t0b44$dJa+2!^L50dtwu-L6C@Ttk{qS{c zGPy32w^Ftlfz&vq2$sIr?vE=&bI13&hlRerzA>jaX=mVpnFWg+75I&}EtvbHp`*na zQH4<2^=4SAnc=*i4kW>4OUYS8e6EcJ|>kAGj|*6nv=HixJ?^NLWZ1Q9*pXRo#l#T8%Gd~l4d(U7hHn68}y@6U6 z-m>+~lJye}O}hK;JJ`xsZuCa*NN_Ot*upzC79rnQC`LpPb@&$So)_8{e!I4!yztw# z73GEBuC16`pvddDXDhlFJX-X$%eEpF(S(X70+;-6IKSe zz)KB*mo7|yI}kY6vn%+-j+)q$7aCLbh$vOZIyoh9s^U6%o7}aw>L)t6b!^829qiQy z+u40^gx`0fgAJW&XN^;BtnF*WHrcU;oIq?Igr6#$d-!7HN;Bb4*0VMFb?nf_-r!F@ z{=u*CyS?tb*LR=S`R=pexz7|YMr1su_FoeQLIJ;8>XC$T>`q0FDRobo9r)aa%?KZL zS&GVvE((gS8PEyKz0TTe7G2L|(o|CEw5}(ks+vxVTGC8dN;HWGB5f*S>ba}BxIg1o zD+;MRRTdRlQ&slbH*YzXvdYPlT_kq}Rw5qn>N*DVUbb9v8Ld3LH25weLjD3AA2Z`b zy1L?V{={u&ENd5w&WuxXtJZLvf9_U}JF6dxbiJ9H>s|DPHcg}A83_u7SuYnAI?$KzsLifeIEiVtJoEcqTk z44QKn@4NVy7(ZfJQ}lV{;+OQ-cotAd{6Ga^sD-ASbbPF2&BS9WmNu$kez3xlhye)O zCkrP}o}6>x2IlZb_-;1#LT5;<3!p9_jMG@CZG+&iMvUY`6oDWZx~6IhtnYUIw%a{0 zVA*PJuZnkbyV)PL|FHV=UalFTd9)$$mz>ZLJv+TMaId>Nc;7L0b{^=1su{WgqywRc zp-SQEV`(nRgzMLasH3U@v8AX^G1!6LgX{~h@PRW|j|9)JuWc0sy+TY?BbxZ^^zp|V zgJ@1gbGihvGOFSX7rjmN??D&~V<;(?iPmPx6NXO>pV@Z1akUUBCrjr1Tz%hmu}$&s z2hNGZ!6$ZkZ@v<9qypxVA~2xU1D%}w`(5erD*}F!A zj~^ib4}c{$l4`_cEuzR#_C|WN_7!loXat)VFx6oe`tYjNiVA2FK&Jv_gTVd5FdXZ# z7uu_TmE^9vWj{sP{tl}nt%PFOB`*@hzDvv@I-g^o*cJ97t^MYFg^-V=W0qamh$4bw zI~h;s%uG2PlT@ucY^R{rq3;D~B)~z6s_Ri*ycEXhLrd18IT7?|S~BIdX2_-&ZC%At_f zE*EZbfg4cNZ4=4|cr?9Wmto8hEf*1+&++9V!AE$|b`j+|rLq|8gWBTJd{&;Z!WoEP^)RTg=K?Iv({+bX2!Goo|4G3Uk1>}dP7)z7B6 zt)x1elgTqfdxGbki)veqDQZL&YRF>Ovow(q9jqPD9z}p16=1wcayql5!i|`yMg#@_ z(=RiX+xbxp-^bg^u8E;yBngX#;VJUTf~Vw7J_CpWU>Z;fpc+6va)jRytU+_ORNem| z@V5=kkFNOJ3)6qGqUnsXFL=KCy+$tDDnaY3F+q8A-$Q_|5PKk&0$v!^q8?Ou#J^h< zBNA!5x&yQnn6+;~9{|u~F~7hGZ9>6mIRTnjNm?3o7xPOa!QX|cbOlh<(vE&V@Tjvl z_=h`cyOiEYU5sd&j$InQe%49=g`SFg+e8=SH`~<(*4s)GERpjjN`U1iYTJ0JWhP|2 z+uOnmlapOQMhZZj$YcUo12O}o+bk5U9Dd{RZ;?g=am7*;T%b#d9Cj~(TM10G2XA2= zJjdKm)6iO=v6AEDA#Q}R(|CbJ(YLb@sd6`u*fdGV=uOxhJQllzm8(6M0ajK>b<@K)(7Z>!p0>Ptv z8JWT$65_s(MR{v$h>u{W^BT+HKg(s~cH={DJ3IEBBTv~J+zkxFM187;S)Qv zXdvz<6HOqer8t4l-kn~&NUP~#jWA&;w3czOg%dL^U{!>uY+Pohe~rRv7LKPb${A$( z*CvXu2Dp_68&)2?Fg?67P~Cbk_>BI$zi!c(Aj6Q!g7zC$DrRBf-K?9ZH?UimX%2p~ z=b-uav1}vVX z%E<>B4+YuOqM49^bbu`aSZ>iw1d*>53Dc3lrBFP)BLpKsyb?Ie5*AFo2!+?uW+V#0 zv)lxnLU@^G9tktg@8^+w?g1T#v_Eea@t2v*M)>{k4qdPF;z9QzC*4eE9Vv4S|m!4F>o%dpP)M z-ypXxqF=veW=)7984K6omD`k)cR=<0Wu=U8<`lh$y>jZNBx>8_^0eN#t+ z5AC94d{EJmYzOTUEHzzW|Lqso)l%`Tp9z zPtEs;s(88dJa|>_v>kT#)@{S@_$hk(Tcz*M9y=0z+)vJ22~Y_HE`Um!B*10yNS9u^ z@>{3ck-luJhHS25&81|)XFhN>vX&iJ2}m%3#Gt5>Y#`?b653N6*pA5#cEdw$tn?>7 z5s`(5u4e!J>b2|%io-m@-k&FzX;C z`CX7EVBr+B10nY)2p%p_wodaNdH~1U1uK9suUSAEfD8~}mtY2KNLR`(Co5$lk@04N z;BjI=AsPpA21jd_0s|10O3R(8S)!x?qa}hNxjXzg*kK;5%rB%$hz8I;8YD6;IaVMG zF0!S3x|J6kBy7T-YaUr0gCu5@-;(zdf+c|$pfl7xFw4>G<`JTn;fETJ&VLEeX~JR` zz+Xi7$+FXP#gdgN01yOM8_4mD=_7}JCjjtKpo3umh4DSWTQHymp@e1219FM*2Pnd8 z5bYOUHKGPT|tX-IKKrK#r zJ}G&mG@TxE7GNe(oV?j3^c{E^@~gm`%$(OHf59jc^M53-bb%PnARW~3%Ed^moClXP$`nbRk#fb4{e^q^a8)2M>ka?}=(L>-irpKf<7YOp9 z7_d@6FEHt@$CmT%q}j}QtK;|Fdj~O>m%MUhaS8B}GAMvKu;&+pPy&DnRHBalH;j)0l>HVuTHqG9YG>I?OeDhBASo(TE*4v-Ch`)MbSK>K2#`Kt{rha11nef7fh zGhE>Q6(@oZReV|(0Msb{1^kL+gY40|p1w7Fn+~cMF`i{A3_-zx)+-`24Rl(Kg4K(P z0c6p&UTfZCz=4(_8+w|^3=w=$Ew12hZo-qGdwu*?;a#-_{@;4Az{0!VO^257!_X^s zKK9eCkP%J}@b$Y>+hYzm+KKdvrbf(C0R{LL=io zH6rOzR9M7s#xw{jO%g~2cmkw#&)CA?qSgXDbKuajpi<#P$j-wYIDG%%Lx+y>ot;!I z@pd1-_fKy*ALKS}&Fw!tZQ9PzvDxFt#Z0+0HhTEGpU03iiUcH;1SUAc|xwC=}p*$gKCLB75eU=Czm3 zr&k0LIJ5a!@O&NL#>jepX-)-75mXi0D`;L%Iy` zN;wi}9!gdgh^Vp10WSc+H*-`M)IgUs@ShYo zqn8l1hU(4Ku>#il8J_f5;1O;z_*iCfl4Y<53{?Ff=X(khfPy3>I8+oniJ(ovMMV9` zH%}3>&2tUAs`sJLg#FEl^=t+5dGDKAw-%GFAeD}qBe>JLU`V>>6ZV1f7DheDYwvGo z0qdW%a#gbYP!5F7vB9w1|ZmNlOzc2)%EqGvP12a`m6o#>I7`7sV zC!|Wz%Ow)99G%rGIc`fJg7;-2URlO1=qW0bXogT*BcNAQ&a<2#M|an!oLBTzLQGh? znb37hOc|nNLWpDx%S;Oz7_qHCx~2*BXUiD9?OXgl!?}65rqB)2@!lj zKPlRWrNmvJB}r6qb~%cL$LAxEUC$-SX_AsU-pq`r+N| z3$M2^XQHij$t~>BiH$>J_WZmF{{jvv<*tX|gj)ROy-sQiKZfd6Hw!)kQpjWQ#=%SN z;m3fIJDCcSu<%&NLBB1bT#PCfHn9I$3NLN`+!2MUcDEiImZuX^&t6j)IWlm}9ja)1 z@~6kL#%O=pD(u!q4Rd<1uTrU`?7i$?lO4;OXlI{4a(rRf%36^~CyM%f87={JDH7iIRX z0xYn99~nqYVa6_)Ieux6(aSMs$^sF%b242Ra2y0_mFx}-i{er$qM9w_9E3XPIK2k@ z1yK!!auj@1v*`*7&b)FwL|1m&>U=MM3@oqC_j;4_rdH?y<{TT@ytnVbo<4UjmvM4C zcWUav^pU}_i9SeEO*PuI0ySEOE92n{;?Q{Ev6PlQs$_wS8e4nfi?Cv|-le11M zM%kP{KG>gBMm;fXx!6f|T`Jc{i2|x>^a|-{!WS37^Z#;QC(*C5fib@l9FAWlALAzr zq|s0bRDEDI*BL5qlQ9pXe1rcV*0-?p=4fT$=+W|E#o1kgl}V*4BYEfOfs0d)#h@Y$OUCCH8s$(Q=HaL&F|i&l-QmLW9i37Ca&o^fm$2w z4*pQqp07+*Q0!cjzC%P@a!XW4xOr@#Cu(@J>22$!O}sHN26t^{zCXKDb;s1x2M^h% zyYEyn-*;pvbvnOmym)vvzVFcK!u-V1gHyDlmp|R%?dZXp_{R9U!p~b7Gg6rE1;GFu z_}=+!awvDKbo>Uy9i_6(0rTSEs(hN-X^BC#I?Fl?{M0{QCrJP48| zZF7DU#6<`9$zb;2dlHpH2=lRFjx0E(Txzla;-7(E_L?aW-oTzgjVFB`Kq3-6vQcar zw2CwaeR&YgPC59D=IAwhF@0J33|vme=R+p;@nnHaIsJUNfU^#;`tZJ0di$W2s-Wr* z5<;}){&xMie(s!P|-cw&#QRgjYK^)P=Z35~&v_=?=OC0|Jx9Jzn^ z>JaJ=lUaOL!HmV{A?TPnd<5-N=Ak>urbBZh_a%bl#O^s}Y&kwPub-NjK74FqSQmB| z_asljlq`|EV;C_n$*M-8*S_Y>-bJ`tXyw9JlGn4M@rw;D{f%F|F#Tj>;E|Se@Ugl7 zz@E0+4YU@~6wX~hh=Eu~S1%JgdS(P!NUD$iz0bYO*MA#6H@^xF_nBL*;Q14a4w{PX zEK>ODpAgW;zIp#e6Ho!?#dCG?_GzflEuPNs>y6oau5Din zNC@mm)qvki>~>H+D8d+-%2nL?qM60*%PH~5nbbq~OVA$NygY==n>M@fzhe9^1DgD@ z{*_m*cCY+&5{Oq;=`YIQ= z_ij6Q-;`JGgED^^WZEb~ALO{yFnj)uo7m5P-d_FQ5rmij`_CQ-oC#-x=eMx0%UTdS z+C%z4vsL9<1Rzs^74SMFWDu+o8CTu4cps6nkx{$c&WAR(2{i&SQieka7m~QiDUa4( zM%p3m?x28f30{Q5Fyu_|{GKIUf$;-fRr0#B8UO+yM1=cD>j9_fd$gOqw6o=?-x+8k zDMF`WD3?2o1GH9Dp{G)i0F(EV0cFrq$xy)qCTRi9o;*(co5f{lCKV!g6T1EMLT*iV74f)h8wteC#B`cs4ZY~#ImIn{e7xD6o{ z^}k55A)=zwWVGs6Te!atJm40BXFOMDHKY!_XUsI95*z~Z(l2`0ORukG8(+U_i+r=Y zP0?>oZbJZUq|$QIG7~@=5*qLaF$p9^it1@mHYGOx`X9c7yQzF!9v$A5NGnHA?K*yX zcYk^y8IAAPN5_Vf(|eC->aiYoV6Sm#Ks%6#J9Okw3P%nB^_F@B9p`x5Bxx?4L8Rl4 zB>wni{i{`E&(6+9Q22ldr_BE1^^NScpFJOVa3ml6;4H2GYOE>>7_q8G1T=oaF>gVt z7D|IoCxHdm=^PVmFN~Tn8-Q9snjspRcj`zlTeu_KruP8!6U1@<_z=L-I49{Mc!ZiY z9HoPUH8j*?d({K#j`~}Ho&pO6Saul=v{Ot!SNB=0?)W1s&ygbc7exF|*xJb6?>kdVb%QP5hRf{eRPp(+eGtl(pT%SwLaKx#-c{u0m;GXlhuSD<4To2ZqFf=|czR zjwa$og(Q*$@M0@92}`C5kzcWI3$@N+mFfT*&UU%v+ymqVaRQh(&>~?ANMj=HM`P|a zD3vbs?lns|E+anT#t#?chjR!#mYfw!37I9u7fdt$!FbPpYx zDebfNjPFnGEWdenHT50M?j;?f8EvVZM}{e7|Fyhn^S?2TxV0XRlW3tcZF$ggQ(p8@e)D4hJC~O( zFIke>iY%CfW|76fX*UIv!XLm7;9t_ID8N8mco-;f&Y()m$$R%vxicMl22+?q5a!)T zYm5ddNUD9)PX#+DHKUNNCy5VaBMA~fhwMxNg%~(52Fdq@o^l@azD4E$()bw%i3chP znz73`oCn`fyVmI}WsfmXbS7WPxi&#Vz`uIv%oQ(3UMuFI-||$!RT~AR=&8gkX?6DT z6GRW7P|-v$4m!C7I?2sP%YYLgp@F(2&-cP>a#m069m(1Q01|RYF#D9~6wYC?@F<|8 t1oT8QM**96-kV>_9Q55EDo-V(A tx;$fU01pdm6a$|(mo!HjYt+U Date: Thu, 17 Aug 2023 18:27:13 +1200 Subject: [PATCH 50/99] actually call the function lol --- d2m/discord-packets.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index 3786393..b8c0af6 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -67,6 +67,8 @@ const utils = { } else if (message.t === "MESSAGE_CREATE") { eventDispatcher.onMessageCreate(client, message.d) + } else if (message.t === "MESSAGE_UPDATE") { + eventDispatcher.onMessageUpdate(client, message.d) } else if (message.t === "MESSAGE_REACTION_ADD") { eventDispatcher.onReactionAdd(client, message.d) From e3737997ec266ed40703a012b6766e8745168abb Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 17 Aug 2023 19:03:09 +1200 Subject: [PATCH 51/99] test that edits by webhook work --- d2m/converters/edit-to-changes.js | 9 +- d2m/converters/edit-to-changes.test.js | 205 ++++++++++++++----------- d2m/event-dispatcher.js | 7 + scripts/events.db | Bin 192512 -> 196608 bytes test/data.js | 27 ++++ 5 files changed, 158 insertions(+), 90 deletions(-) diff --git a/d2m/converters/edit-to-changes.js b/d2m/converters/edit-to-changes.js index 4e6892d..3f4b2d2 100644 --- a/d2m/converters/edit-to-changes.js +++ b/d2m/converters/edit-to-changes.js @@ -22,7 +22,14 @@ async function editToChanges(message, guild, api) { // Figure out what events we will be replacing 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 {string?} */ + let senderMxid = db.prepare("SELECT mxid FROM sim WHERE discord_id = ?").pluck().get(message.author.id) ?? null + if (senderMxid) { + const senderIsInRoom = db.prepare("SELECT * FROM sim_member WHERE room_id = ? and mxid = ?").get(roomID, senderMxid) + if (!senderIsInRoom) { + senderMxid = null // just send as ooye bot + } + } /** @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) diff --git a/d2m/converters/edit-to-changes.test.js b/d2m/converters/edit-to-changes.test.js index bb3f3ec..674cb15 100644 --- a/d2m/converters/edit-to-changes.test.js +++ b/d2m/converters/edit-to-changes.test.js @@ -3,104 +3,131 @@ const {editToChanges} = require("./edit-to-changes") const data = require("../../test/data") const Ty = require("../../types") +test("edit2changes: edit by webhook", async t => { + const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.edit_by_webhook, data.guild.general, {}) + t.deepEqual(eventsToRedact, []) + t.deepEqual(eventsToSend, []) + t.deepEqual(eventsToReplace, [{ + oldID: "$zXSlyI78DQqQwwfPUSzZ1b-nXzbUrCDljJgnGDdoI10", + newContent: { + $type: "m.room.message", + msgtype: "m.text", + body: "* test 2", + "m.mentions": {}, + "m.new_content": { + // *** Replaced With: *** + msgtype: "m.text", + body: "test 2", + "m.mentions": {} + }, + "m.relates_to": { + rel_type: "m.replace", + event_id: "$zXSlyI78DQqQwwfPUSzZ1b-nXzbUrCDljJgnGDdoI10" + } + } + }]) + t.equal(senderMxid, null) +}) + test("edit2changes: bot response", async t => { - const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.bot_response, data.guild.general, { - async getJoinedMembers(roomID) { - t.equal(roomID, "!uCtjHhfGlYbVnPVlkG:cadence.moe") - return new Promise(resolve => { - setTimeout(() => { - resolve({ - joined: { - "@cadence:cadence.moe": { - display_name: "cadence [they]", - avatar_url: "whatever" - }, - "@_ooye_botrac4r:cadence.moe": { - display_name: "botrac4r", - avatar_url: "whatever" - } - } - }) - }) - }) - } - }) - t.deepEqual(eventsToRedact, []) - t.deepEqual(eventsToSend, []) - t.deepEqual(eventsToReplace, [{ - oldID: "$fdD9OZ55xg3EAsfvLZza5tMhtjUO91Wg3Otuo96TplY", - newContent: { - $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" - } - } - }]) + const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.bot_response, data.guild.general, { + async getJoinedMembers(roomID) { + t.equal(roomID, "!uCtjHhfGlYbVnPVlkG:cadence.moe") + return new Promise(resolve => { + setTimeout(() => { + resolve({ + joined: { + "@cadence:cadence.moe": { + display_name: "cadence [they]", + avatar_url: "whatever" + }, + "@_ooye_botrac4r:cadence.moe": { + display_name: "botrac4r", + avatar_url: "whatever" + } + } + }) + }) + }) + } + }) + t.deepEqual(eventsToRedact, []) + t.deepEqual(eventsToSend, []) + t.deepEqual(eventsToReplace, [{ + oldID: "$fdD9OZ55xg3EAsfvLZza5tMhtjUO91Wg3Otuo96TplY", + newContent: { + $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" + } + } + }]) + t.equal(senderMxid, "@_ooye_bojack_horseman:cadence.moe") }) test("edit2changes: remove caption from image", async t => { - const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.removed_caption_from_image, data.guild.general, {}) - t.deepEqual(eventsToRedact, ["$mtR8cJqM4fKno1bVsm8F4wUVqSntt2sq6jav1lyavuA"]) - t.deepEqual(eventsToSend, []) - t.deepEqual(eventsToReplace, []) + const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.removed_caption_from_image, data.guild.general, {}) + t.deepEqual(eventsToRedact, ["$mtR8cJqM4fKno1bVsm8F4wUVqSntt2sq6jav1lyavuA"]) + t.deepEqual(eventsToSend, []) + t.deepEqual(eventsToReplace, []) }) test("edit2changes: add caption back to that image", async t => { - const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.added_caption_to_image, data.guild.general, {}) - t.deepEqual(eventsToRedact, []) - t.deepEqual(eventsToSend, [{ - $type: "m.room.message", - msgtype: "m.text", - body: "some text", - "m.mentions": {} - }]) - t.deepEqual(eventsToReplace, []) + const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.added_caption_to_image, data.guild.general, {}) + t.deepEqual(eventsToRedact, []) + t.deepEqual(eventsToSend, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "some text", + "m.mentions": {} + }]) + t.deepEqual(eventsToReplace, []) }) 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, {}) + 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", - newContent: { - $type: "m.room.message", - msgtype: "m.text", - body: "> Extremity: Image\n\n* Edit", - format: "org.matrix.custom.html", - formatted_body: - '
In reply to Extremity' - + '
Image
' - + '* Edit', - "m.mentions": {}, - "m.new_content": { - msgtype: "m.text", - body: "Edit", - "m.mentions": {} - }, - "m.relates_to": { - rel_type: "m.replace", - event_id: "$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M" - } - } - }]) + t.deepEqual(eventsToSend, []) + t.deepEqual(eventsToReplace, [{ + oldID: "$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M", + newContent: { + $type: "m.room.message", + msgtype: "m.text", + body: "> Extremity: Image\n\n* Edit", + format: "org.matrix.custom.html", + formatted_body: + '
In reply to Extremity' + + '
Image
' + + '* Edit', + "m.mentions": {}, + "m.new_content": { + msgtype: "m.text", + body: "Edit", + "m.mentions": {} + }, + "m.relates_to": { + rel_type: "m.replace", + event_id: "$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M" + } + } + }]) }) diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 4bb94ff..1527b28 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -37,6 +37,13 @@ module.exports = { * @param {import("discord-api-types/v10").GatewayMessageUpdateDispatchData} message */ onMessageUpdate(client, data) { + if (data.webhook_id) { + const row = db.prepare("SELECT webhook_id FROM webhook WHERE webhook_id = ?").pluck().get(message.webhook_id) + if (row) { + // The update was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it. + return + } + } // Based on looking at data they've sent me over the gateway, this is the best way to check for meaningful changes. // If the message content is a string then it includes all interesting fields and is meaningful. if (typeof data.content === "string") { diff --git a/scripts/events.db b/scripts/events.db index d3b817d2a036453e0de94479f85f48b5b73d0bb7..436e06e8bba877bf32990534de28e2b7c98531fe 100644 GIT binary patch delta 342 zcmZXNJxjw-7)EnTQ>7+NEGj~!sikY1_a^sK1Sc1_il7dPkhMr7W+{k~7Id^+@eVEy z4!T(!1P7sOCx3uL9sD20!AUsN;XFL)bS_P;>x!8eh5^Ic21-Up56*1U@iJk6vfRC# z<|G`at4%u5Ll|Vzpb9XYK6giY+oJd)gG$25C|#=0Z9a*KtY|W zW{K|rTMJ}HE8tQig$_&E4VPI0j7SLd_z^MpuM0^FGah2y{YDEay~(R-)kJ}cS88P< zIc7MdAVWeKa4vNGhL-ho7cG@J{QGbF&HQoF>8N{uv+_Hpk_U`Xn!zclpG8qrEEc05 DRt;He delta 58 zcmZo@;AwckJwckan1O*of1-jtWAVm>CHx#Z{3<{J2K~uA0udVv6&N?SC@$b_R*`R4 Nk!RelBF}W^0szXq5D)+W diff --git a/test/data.js b/test/data.js index bb2570b..7e3fdae 100644 --- a/test/data.js +++ b/test/data.js @@ -788,6 +788,33 @@ module.exports = { } }, message_update: { + edit_by_webhook: { + application_id: "684280192553844747", + attachments: [], + author: { + avatar: null, + bot: true, + discriminator: "0000", + id: "700285844094845050", + username: "cadence [they]" + }, + channel_id: "497161350934560778", + components: [], + content: "test 2", + edited_timestamp: "2023-08-17T06:29:34.167314+00:00", + embeds: [], + flags: 0, + guild_id: "497159726455455754", + id: "1141619794500649020", + mention_everyone: false, + mention_roles: [], + mentions: [], + pinned: false, + timestamp: "2023-08-17T06:29:29.279000+00:00", + tts: false, + type: 0, + webhook_id: "700285844094845050" + }, bot_response: { attachments: [], author: { From 6de13338a86fb59ae49dccd414b008feb71397d1 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 18 Aug 2023 01:22:14 +1200 Subject: [PATCH 52/99] add error handler and message deleter --- d2m/actions/delete-message.js | 29 ++++++++++++++++++ d2m/discord-packets.js | 22 ++++++++++---- d2m/event-dispatcher.js | 57 +++++++++++++++++++++++++++++++++-- 3 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 d2m/actions/delete-message.js diff --git a/d2m/actions/delete-message.js b/d2m/actions/delete-message.js new file mode 100644 index 0000000..261c8f9 --- /dev/null +++ b/d2m/actions/delete-message.js @@ -0,0 +1,29 @@ +// @ts-check + +const passthrough = require("../../passthrough") +const { sync, db } = passthrough +/** @type {import("../converters/edit-to-changes")} */ +const editToChanges = sync.require("../converters/edit-to-changes") +/** @type {import("../../matrix/api")} */ +const api = sync.require("../../matrix/api") + +/** + * @param {import("discord-api-types/v10").GatewayMessageDeleteDispatchData} data + */ +async function deleteMessage(data) { + /** @type {string?} */ + const roomID = db.prepare("SELECT channel_id FROM channel_room WHERE channel_id = ?").pluck().get(data.channel_id) + if (!roomID) return + + /** @type {string[]} */ + const eventsToRedact = db.prepare("SELECT event_id FROM event_message WHERE message_id = ?").pluck().all(data.id) + + for (const eventID of eventsToRedact) { + // Unfortuately, we can't specify a sender to do the redaction as, unless we find out that info via the audit logs + await api.redactEvent(roomID, eventID) + db.prepare("DELETE from event_message WHERE event_id = ?").run(eventID) + // TODO: Consider whether this code could be reused between edited messages and deleted messages. + } +} + +module.exports.deleteMessage = deleteMessage diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index b8c0af6..6ae1c22 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -16,6 +16,7 @@ const utils = { /** @type {typeof import("./event-dispatcher")} */ const eventDispatcher = sync.require("./event-dispatcher") + // Client internals, keep track of the state we need if (message.t === "READY") { if (client.ready) return client.ready = true @@ -62,16 +63,25 @@ const utils = { } } } + } + // Event dispatcher for OOYE bridge operations + try { + if (message.t === "MESSAGE_CREATE") { + eventDispatcher.onMessageCreate(client, message.d) - } else if (message.t === "MESSAGE_CREATE") { - eventDispatcher.onMessageCreate(client, message.d) + } else if (message.t === "MESSAGE_UPDATE") { + eventDispatcher.onMessageUpdate(client, message.d) - } else if (message.t === "MESSAGE_UPDATE") { - eventDispatcher.onMessageUpdate(client, message.d) + } else if (message.t === "MESSAGE_DELETE") { + eventDispatcher.onMessageDelete(client, message.d) - } else if (message.t === "MESSAGE_REACTION_ADD") { - eventDispatcher.onReactionAdd(client, message.d) + } else if (message.t === "MESSAGE_REACTION_ADD") { + eventDispatcher.onReactionAdd(client, message.d) + } + } catch (e) { + // Let OOYE try to handle errors too + eventDispatcher.onError(client, e, message) } } } diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 1527b28..fde228d 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -1,17 +1,61 @@ const assert = require("assert").strict +const util = require("util") const {sync, db} = require("../passthrough") /** @type {import("./actions/send-message")}) */ const sendMessage = sync.require("./actions/send-message") /** @type {import("./actions/edit-message")}) */ const editMessage = sync.require("./actions/edit-message") - +/** @type {import("./actions/delete-message")}) */ +const deleteMessage = sync.require("./actions/delete-message") /** @type {import("./actions/add-reaction")}) */ const addReaction = sync.require("./actions/add-reaction") +/** @type {import("../matrix/api")}) */ +const api = sync.require("../matrix/api") + +let lastReportedEvent = 0 // Grab Discord events we care about for the bridge, check them, and pass them on module.exports = { + /** + * @param {import("./discord-client")} client + * @param {Error} e + * @param {import("cloudstorm").IGatewayMessage} gatewayMessage + */ + onError(client, e, gatewayMessage) { + console.error("hit event-dispatcher's error handler with this exception:") + console.error(e) // TODO: also log errors into a file or into the database, maybe use a library for this? or just wing it? definitely need to be able to store the formatted event body to load back in later + console.error(`while handling this ${gatewayMessage.t} gateway event:`) + console.dir(gatewayMessage.d) + + if (Date.now() - lastReportedEvent > 5000) { + lastReportedEvent = Date.now() + const channelID = gatewayMessage.d.channel_id + if (channelID) { + const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(channelID) + let stackLines = e.stack.split("\n") + let cloudstormLine = stackLines.findIndex(l => l.includes("/node_modules/cloudstorm/")) + if (cloudstormLine !== -1) { + stackLines = stackLines.slice(0, cloudstormLine - 2) + } + api.sendEvent(roomID, "m.room.message", { + msgtype: "m.text", + body: "\u26a0 Bridged event from Discord not delivered. See formatted content for full details.", + format: "org.matrix.custom.html", + formatted_body: "\u26a0 Bridged event from Discord not delivered" + + `
Gateway event: ${gatewayMessage.t}` + + `
${stackLines.join("\n")}
` + + `
Original payload` + + `
${util.inspect(gatewayMessage.d, false, 4, false)}
`, + "m.mentions": { + user_ids: ["@cadence:cadence.moe"] + } + }) + } + } + }, + /** * @param {import("./discord-client")} client * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message @@ -38,7 +82,7 @@ module.exports = { */ onMessageUpdate(client, data) { if (data.webhook_id) { - const row = db.prepare("SELECT webhook_id FROM webhook WHERE webhook_id = ?").pluck().get(message.webhook_id) + const row = db.prepare("SELECT webhook_id FROM webhook WHERE webhook_id = ?").pluck().get(data.webhook_id) if (row) { // The update was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it. return @@ -67,5 +111,14 @@ module.exports = { if (data.emoji.id !== null) return // TODO: image emoji reactions console.log(data) addReaction.addReaction(data) + }, + + /** + * @param {import("./discord-client")} client + * @param {import("discord-api-types/v10").GatewayMessageDeleteDispatchData} data + */ + onMessageDelete(client, data) { + console.log(data) + deleteMessage.deleteMessage(data) } } From 2d14e843127c1479e4c1246b74d65222b7508202 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 18 Aug 2023 01:22:33 +1200 Subject: [PATCH 53/99] update discord-markdown to escape less stuff --- d2m/converters/message-to-event.test.js | 10 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- test/data.js | 31 +++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index df94196..86942a7 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -39,6 +39,16 @@ test("message2event: simple plaintext", async t => { }]) }) +test("message2event: simple plaintext with quotes", async t => { + const events = await messageToEvent(data.message.simple_plaintext_with_quotes, data.guild.general, {}) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.mentions": {}, + msgtype: "m.text", + body: `then he said, "you and her aren't allowed in here!"` + }]) +}) + test("message2event: simple user mention", async t => { const events = await messageToEvent(data.message.simple_user_mention, data.guild.general, {}) t.deepEqual(events, [{ diff --git a/package-lock.json b/package-lock.json index c6b6004..875e329 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "better-sqlite3": "^8.3.0", "cloudstorm": "^0.8.0", - "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#9799e4f79912d07f89a030e479e82fcc1e75bc81", + "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#440130ef343c8183a81c7c09809731484aa3a182", "heatsync": "^2.4.1", "js-yaml": "^4.1.0", "matrix-appservice": "^2.0.0", @@ -1051,7 +1051,7 @@ }, "node_modules/discord-markdown": { "version": "2.4.1", - "resolved": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#9799e4f79912d07f89a030e479e82fcc1e75bc81", + "resolved": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#440130ef343c8183a81c7c09809731484aa3a182", "license": "MIT", "dependencies": { "simple-markdown": "^0.7.2" diff --git a/package.json b/package.json index 6557500..bc0a0db 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "dependencies": { "better-sqlite3": "^8.3.0", "cloudstorm": "^0.8.0", - "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#9799e4f79912d07f89a030e479e82fcc1e75bc81", + "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#440130ef343c8183a81c7c09809731484aa3a182", "heatsync": "^2.4.1", "js-yaml": "^4.1.0", "matrix-appservice": "^2.0.0", diff --git a/test/data.js b/test/data.js index 7e3fdae..a1d3ece 100644 --- a/test/data.js +++ b/test/data.js @@ -170,6 +170,37 @@ module.exports = { flags: 0, components: [] }, + simple_plaintext_with_quotes: { + id: "1126733830494093454", + type: 0, + content: `then he said, "you and her aren't allowed in here!"`, + channel_id: "112760669178241024", + author: { + id: "111604486476181504", + username: "kyuugryphon", + avatar: "e4ce31267ca524d19be80e684d4cafa1", + discriminator: "0", + public_flags: 0, + flags: 0, + banner: null, + accent_color: null, + global_name: "KyuuGryphon", + avatar_decoration: null, + display_name: "KyuuGryphon", + banner_color: null + }, + attachments: [], + embeds: [], + mentions: [], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2023-07-07T04:37:58.892000+00:00", + edited_timestamp: null, + flags: 0, + components: [] + }, simple_user_mention: { id: "1126739682080858234", type: 0, From 6246dc3a7f7ddbea0bb75b2c1912912dbbe238f7 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 18 Aug 2023 01:23:53 +1200 Subject: [PATCH 54/99] remove redactions from database in edit flow --- d2m/actions/edit-message.js | 7 +++++-- scripts/events.db | Bin 196608 -> 208896 bytes 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/d2m/actions/edit-message.js b/d2m/actions/edit-message.js index 9a329b6..1c1b90e 100644 --- a/d2m/actions/edit-message.js +++ b/d2m/actions/edit-message.js @@ -14,7 +14,8 @@ const api = sync.require("../../matrix/api") async function editMessage(message, guild) { console.log(`*** applying edit for message ${message.id} in channel ${message.channel_id}`) const {roomID, eventsToRedact, eventsToReplace, eventsToSend, senderMxid} = await editToChanges.editToChanges(message, guild, api) - console.log("making these changes:", {eventsToRedact, eventsToReplace, eventsToSend}) + console.log("making these changes:") + console.dir({eventsToRedact, eventsToReplace, eventsToSend}, {depth: null}) // 1. Replace all the things. for (const {oldID, newContent} of eventsToReplace) { @@ -34,7 +35,9 @@ async function editMessage(message, guild) { // Not redacting as the last action because the last action is likely to be shown in the room preview in clients, and we don't want it to look like somebody actually deleted a message. for (const eventID of eventsToRedact) { await api.redactEvent(roomID, eventID, senderMxid) - // TODO: I should almost certainly remove the redacted event from our database now, shouldn't I? I mean, it's literally not there any more... you can't do anything else with it... + // TODO: Reconsider whether it's the right thing to do to delete it from our database? I mean, it's literally not there any more... you can't do anything else with it... + // and you definitely want to mark it in *some* way to prevent duplicate redactions... + db.prepare("DELETE from event_message WHERE event_id = ?").run(eventID) // TODO: If I just redacted part = 0, I should update one of the other events to make it the new part = 0, right? // TODO: Consider whether this code could be reused between edited messages and deleted messages. } diff --git a/scripts/events.db b/scripts/events.db index 436e06e8bba877bf32990534de28e2b7c98531fe..045e1471b97b6274aebcb9b2f457c3bc3ab5b898 100644 GIT binary patch delta 2896 zcmeHJTWlQF8TO3h3rW^%L(<#`K3=JDTiLU7zpSW|7zD9!$}Lt25=qa_p4~n1?40$S znVqb&mYou{K}9OjRGPFe2m%j0^uemCyz~Vm1Ollg%2VG`+B{Y&s`}EB(*M|-U{K2g zmHO0|-JNs!FW>)t-|YJO?)6){UpX|iJ}@v~TtP>r^^G6AQ5;o^Cf9(-sQ;6qJcoSgje~{^RhaW66J!#;lm0Vi>Ax*#9Ptu{}-v zHPaNf`i3;R;i&nI%krKtHluIL@-Q!4e&HTwe-TI+70pvLL$^)uUWwVQhlZqW)m@CE z{!p&p{!~6RI9`B(?igGcxbNN3-wt;M7dDn39vuGB^o7F9N3-S5-lFYlhHAK;0Y!PL zo87qc;J0u{1IlB?XXO?S8@TKNDr)%&MtKq?vrkY?sNYPIJmAvdiT*PBeEAROrTy72 z|Ng*!+G;49!4w{sTck$&p|TsjQl@_Q;2K$3n0wL*IW}8o=bo|g)2HT_PCs|*ynlT5 znRCy()`{x*#R$*pJTS}6IBxUue+pTeDK!agG~-eQ*t9xRYDcU?Ya}XFE|fytBBe^{ zct{tZuM+Im2=^np){ZF)VdqqnARe=*izr99%90pi1ihoQ$v8niYqgVr(+&aFg*6iT zlyKNzB8bL_w%UwyS`7%Qvj{b53&jjA5z>Yr=U8HNYcWOWJJ~#fV_PiCEku5!^r#p~p><=M9CIJ)PkmY#idXXAE)&=jc~h7YN*;pCvg<9^hB40I0sto%e6nr z`|K~rfx23Rm#ZYkf*|E84nq?0avh%ySc56;uyM4b-(%hBxbbLs&U|(`Rv(2DU8nK= z#iselyp|qcnydPYb0?ea=f1bHG{^k%H;uc@||GN zV^mE-AOof~z!x>KAWS17I0nMhfnzm-rxc{1M_8*xfo&`l@O*x?wE6-Jq$wH?&e9ri zTGI?uGc8*;HOKU9)i$A!MnZ%9rJM5Mp}2d`y)CitX{O`Yx@MO0SFcIJy?MUwNmE1a z`NGx3>}|syb55yl#d0gErMQ-Led0~&^+6c3qk(%Enri6a=dZmX+Y@&WT9#sZp5uD? z#8r9kT8oA?kPShs4)UOJQ|Mq?a4;Y-e76JK1v(3Hv~Z2Ux&fq2A`Y|#Rtq^ZL^#J? zB-$sK#~3z&>zm>bs)fjcwSEAy1r3Al;8Dn<8etcd3+RHv`#6c4Nr>A(;|7VT4;ldj z2>l>|riVAF(kS2SC3O+EXpUdsP5Di^Z9zWdHUz?u>3krowg$kqER=+@!76G3HjH(1Ih@e?vHfSki@R|fT zSSXg}0YLZjP? z=b2899|D^6dOoXyB}Kg+I*I}?pJ;P3A~Q7TV^JFI10ei2gz6D^Rsd-s|0M^E>&G_vMZ3$+xDn zPub|OG_q%4WY5O(P8c&?EnGAHKjyCmX~jr qmuK8AFVFP!0tXY%DF)tyJf}7m?&D!@EGe3Pk%L)s`?EhxQp^As2O1^- From e08262388bc40ce697680380bb6d1c4fd7fc3197 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 18 Aug 2023 16:58:46 +1200 Subject: [PATCH 55/99] bug fix where errors weren't being sent --- d2m/actions/delete-message.js | 5 +---- d2m/actions/edit-message.js | 3 --- d2m/actions/register-user.js | 7 +++++++ d2m/actions/send-message.js | 3 +-- d2m/discord-packets.js | 10 +++++----- d2m/event-dispatcher.js | 18 +++++++++--------- matrix/api.js | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/d2m/actions/delete-message.js b/d2m/actions/delete-message.js index 261c8f9..c9b43ee 100644 --- a/d2m/actions/delete-message.js +++ b/d2m/actions/delete-message.js @@ -2,8 +2,6 @@ const passthrough = require("../../passthrough") const { sync, db } = passthrough -/** @type {import("../converters/edit-to-changes")} */ -const editToChanges = sync.require("../converters/edit-to-changes") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") @@ -12,7 +10,7 @@ const api = sync.require("../../matrix/api") */ async function deleteMessage(data) { /** @type {string?} */ - const roomID = db.prepare("SELECT channel_id FROM channel_room WHERE channel_id = ?").pluck().get(data.channel_id) + const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(data.channel_id) if (!roomID) return /** @type {string[]} */ @@ -22,7 +20,6 @@ async function deleteMessage(data) { // Unfortuately, we can't specify a sender to do the redaction as, unless we find out that info via the audit logs await api.redactEvent(roomID, eventID) db.prepare("DELETE from event_message WHERE event_id = ?").run(eventID) - // TODO: Consider whether this code could be reused between edited messages and deleted messages. } } diff --git a/d2m/actions/edit-message.js b/d2m/actions/edit-message.js index 1c1b90e..8e8c838 100644 --- a/d2m/actions/edit-message.js +++ b/d2m/actions/edit-message.js @@ -35,11 +35,8 @@ async function editMessage(message, guild) { // Not redacting as the last action because the last action is likely to be shown in the room preview in clients, and we don't want it to look like somebody actually deleted a message. for (const eventID of eventsToRedact) { await api.redactEvent(roomID, eventID, senderMxid) - // TODO: Reconsider whether it's the right thing to do to delete it from our database? I mean, it's literally not there any more... you can't do anything else with it... - // and you definitely want to mark it in *some* way to prevent duplicate redactions... db.prepare("DELETE from event_message WHERE event_id = ?").run(eventID) // TODO: If I just redacted part = 0, I should update one of the other events to make it the new part = 0, right? - // TODO: Consider whether this code could be reused between edited messages and deleted messages. } // 3. Send all the things. diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index 1455360..19c3a6d 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -115,8 +115,14 @@ function calculateProfileEventContentHash(content) { } /** + * Sync profile data for a sim user. This function follows the following process: + * 1. Join the sim to the room if needed + * 2. Make an object of what the new room member state content would be, including uploading the profile picture if it hasn't been done before + * 3. Compare against the previously known state content, which is helpfully stored in the database + * 4. If the state content has changes, send it to Matrix and update it in the database for next time * @param {import("discord-api-types/v10").APIUser} user * @param {Omit} member + * @returns {Promise} mxid of the updated sim */ async function syncUser(user, member, guildID, roomID) { const mxid = await ensureSimJoined(user, roomID) @@ -128,6 +134,7 @@ async function syncUser(user, member, guildID, roomID) { await api.sendState(roomID, "m.room.member", mxid, content, mxid) db.prepare("UPDATE sim_member SET profile_event_content_hash = ? WHERE room_id = ? AND mxid = ?").run(profileEventContentHash, roomID, mxid) } + return mxid } async function syncAllUsersInRoom(roomID) { diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index cf87d35..2132905 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -23,8 +23,7 @@ async function sendMessage(message, guild) { let senderMxid = null if (!message.webhook_id) { assert(message.member) - senderMxid = await registerUser.ensureSimJoined(message.author, roomID) - await registerUser.syncUser(message.author, message.member, message.guild_id, roomID) + senderMxid = await registerUser.syncUser(message.author, message.member, message.guild_id, roomID) } const events = await messageToEvent.messageToEvent(message, guild, {}, {api}) diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index 6ae1c22..c0ba1a6 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -11,7 +11,7 @@ const utils = { * @param {import("./discord-client")} client * @param {import("cloudstorm").IGatewayMessage} message */ - onPacket(client, message) { + async onPacket(client, message) { // requiring this later so that the client is already constructed by the time event-dispatcher is loaded /** @type {typeof import("./event-dispatcher")} */ const eventDispatcher = sync.require("./event-dispatcher") @@ -68,16 +68,16 @@ const utils = { // Event dispatcher for OOYE bridge operations try { if (message.t === "MESSAGE_CREATE") { - eventDispatcher.onMessageCreate(client, message.d) + await eventDispatcher.onMessageCreate(client, message.d) } else if (message.t === "MESSAGE_UPDATE") { - eventDispatcher.onMessageUpdate(client, message.d) + await eventDispatcher.onMessageUpdate(client, message.d) } else if (message.t === "MESSAGE_DELETE") { - eventDispatcher.onMessageDelete(client, message.d) + await eventDispatcher.onMessageDelete(client, message.d) } else if (message.t === "MESSAGE_REACTION_ADD") { - eventDispatcher.onReactionAdd(client, message.d) + await eventDispatcher.onReactionAdd(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 fde228d..8e64591 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -27,7 +27,7 @@ module.exports = { console.error("hit event-dispatcher's error handler with this exception:") console.error(e) // TODO: also log errors into a file or into the database, maybe use a library for this? or just wing it? definitely need to be able to store the formatted event body to load back in later console.error(`while handling this ${gatewayMessage.t} gateway event:`) - console.dir(gatewayMessage.d) + console.dir(gatewayMessage.d, {depth: null}) if (Date.now() - lastReportedEvent > 5000) { lastReportedEvent = Date.now() @@ -60,7 +60,7 @@ module.exports = { * @param {import("./discord-client")} client * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message */ - onMessageCreate(client, message) { + async onMessageCreate(client, message) { if (message.webhook_id) { const row = db.prepare("SELECT webhook_id FROM webhook WHERE webhook_id = ?").pluck().get(message.webhook_id) if (row) { @@ -73,14 +73,14 @@ module.exports = { if (!channel.guild_id) return // Nothing we can do in direct messages. const guild = client.guilds.get(channel.guild_id) if (message.guild_id !== "112760669178241024" && message.guild_id !== "497159726455455754") return // TODO: activate on other servers (requires the space creation flow to be done first) - sendMessage.sendMessage(message, guild) + await sendMessage.sendMessage(message, guild) }, /** * @param {import("./discord-client")} client * @param {import("discord-api-types/v10").GatewayMessageUpdateDispatchData} message */ - onMessageUpdate(client, data) { + async onMessageUpdate(client, data) { if (data.webhook_id) { const row = db.prepare("SELECT webhook_id FROM webhook WHERE webhook_id = ?").pluck().get(data.webhook_id) if (row) { @@ -98,7 +98,7 @@ module.exports = { if (!channel.guild_id) return // Nothing we can do in direct messages. const guild = client.guilds.get(channel.guild_id) if (message.guild_id !== "112760669178241024" && message.guild_id !== "497159726455455754") return // TODO: activate on other servers (requires the space creation flow to be done first) - editMessage.editMessage(message, guild) + await editMessage.editMessage(message, guild) } }, @@ -106,19 +106,19 @@ module.exports = { * @param {import("./discord-client")} client * @param {import("discord-api-types/v10").GatewayMessageReactionAddDispatchData} data */ - onReactionAdd(client, data) { + async onReactionAdd(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. if (data.emoji.id !== null) return // TODO: image emoji reactions console.log(data) - addReaction.addReaction(data) + await addReaction.addReaction(data) }, /** * @param {import("./discord-client")} client * @param {import("discord-api-types/v10").GatewayMessageDeleteDispatchData} data */ - onMessageDelete(client, data) { + async onMessageDelete(client, data) { console.log(data) - deleteMessage.deleteMessage(data) + await deleteMessage.deleteMessage(data) } } diff --git a/matrix/api.js b/matrix/api.js index ef3e199..81d8a16 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -134,7 +134,7 @@ async function sendEvent(roomID, type, content, mxid, timestamp) { */ async function redactEvent(roomID, eventID, mxid) { /** @type {Ty.R.EventRedacted} */ - const root = await mreq.mreq("PUT", path(`/client/v3/rooms/${roomID}/redact/${eventID}/${makeTxnId.makeTxnId()}`, mxid)) + const root = await mreq.mreq("PUT", path(`/client/v3/rooms/${roomID}/redact/${eventID}/${makeTxnId.makeTxnId()}`, mxid), {}) return root.event_id } From 36adc30990d47efd8a0cba111e9711c1a17d37a3 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 18 Aug 2023 17:00:40 +1200 Subject: [PATCH 56/99] only include necessary data for testing --- db/data-for-test.sql | 94 +++++++++++++++++++++++++++++++++++++++++++ matrix/file.js | 21 ++++++---- scripts/events.db | Bin 208896 -> 249856 bytes test/test.js | 8 +++- 4 files changed, 114 insertions(+), 9 deletions(-) create mode 100644 db/data-for-test.sql diff --git a/db/data-for-test.sql b/db/data-for-test.sql new file mode 100644 index 0000000..fa04562 --- /dev/null +++ b/db/data-for-test.sql @@ -0,0 +1,94 @@ +BEGIN TRANSACTION; +CREATE TABLE IF NOT EXISTS "guild_space" ( + "guild_id" TEXT NOT NULL UNIQUE, + "space_id" TEXT NOT NULL UNIQUE, + PRIMARY KEY("guild_id") +); +CREATE TABLE IF NOT EXISTS "file" ( + "discord_url" TEXT NOT NULL UNIQUE, + "mxc_url" TEXT NOT NULL UNIQUE, + PRIMARY KEY("discord_url") +); +CREATE TABLE IF NOT EXISTS "sim" ( + "discord_id" TEXT NOT NULL UNIQUE, + "sim_name" TEXT NOT NULL UNIQUE, + "localpart" TEXT NOT NULL UNIQUE, + "mxid" TEXT NOT NULL UNIQUE, + PRIMARY KEY("discord_id") +); +CREATE TABLE IF NOT EXISTS "sim_member" ( + "mxid" TEXT NOT NULL, + "room_id" TEXT NOT NULL, + "profile_event_content_hash" BLOB, + PRIMARY KEY("mxid","room_id") +); +CREATE TABLE IF NOT EXISTS "webhook" ( + "channel_id" TEXT NOT NULL UNIQUE, + "webhook_id" TEXT NOT NULL UNIQUE, + "webhook_token" TEXT NOT NULL, + PRIMARY KEY("channel_id") +); +CREATE TABLE IF NOT EXISTS "channel_room" ( + "channel_id" TEXT NOT NULL UNIQUE, + "room_id" TEXT NOT NULL UNIQUE, + "name" TEXT, + "nick" TEXT, + PRIMARY KEY("channel_id") +); +CREATE TABLE IF NOT EXISTS "event_message" ( + "event_id" TEXT NOT NULL, + "event_type" TEXT, + "event_subtype" TEXT, + "message_id" TEXT NOT NULL, + "channel_id" TEXT, + "part" INTEGER NOT NULL, + "source" INTEGER NOT NULL, + PRIMARY KEY("event_id","message_id") +); +COMMIT; + + + +BEGIN TRANSACTION; + +INSERT INTO guild_space (guild_id, space_id) VALUES +('112760669178241024', '!jjWAGMeQdNrVZSSfvz:cadence.moe'); + +INSERT INTO channel_room (channel_id, room_id, name, nick) VALUES +('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main'), +('497161350934560778', '!edUxjVdzgUvXDUIQCK:cadence.moe', 'amanda-spam', NULL), +('160197704226439168', '!uCtjHhfGlYbVnPVlkG:cadence.moe', 'the-stanley-parable-channel', 'bots'); + +INSERT INTO sim (discord_id, sim_name, localpart, mxid) VALUES +('0', 'bot', '_ooye_bot', '@_ooye_bot:cadence.moe'), +('820865262526005258', 'crunch_god', '_ooye_crunch_god', '@_ooye_crunch_god:cadence.moe'), +('771520384671416320', 'bojack_horseman', '_ooye_bojack_horseman', '@_ooye_bojack_horseman:cadence.moe'), +('112890272819507200', '.wing.', '_ooye_.wing.', '@_ooye_.wing.:cadence.moe'), +('114147806469554185', 'extremity', '_ooye_extremity', '@_ooye_extremity:cadence.moe'); + +INSERT INTO sim_member (mxid, room_id, profile_event_content_hash) VALUES +('@_ooye_bojack_horseman:cadence.moe', '!uCtjHhfGlYbVnPVlkG:cadence.moe', NULL); + +INSERT INTO event_message (event_id, event_type, event_subtype, message_id, channel_id, part, source) VALUES +('$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg', 'm.room.message', 'm.text', '1126786462646550579', '112760669178241024', 0, 1), +('$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4', 'm.room.message', 'm.text', '1128118177155526666', '112760669178241024', 0, 0), +('$zXSlyI78DQqQwwfPUSzZ1b-nXzbUrCDljJgnGDdoI10', 'm.room.message', 'm.text', '1141619794500649020', '497161350934560778', 0, 1), +('$fdD9OZ55xg3EAsfvLZza5tMhtjUO91Wg3Otuo96TplY', 'm.room.message', 'm.text', '1141206225632112650', '160197704226439168', 0, 1), +('$mtR8cJqM4fKno1bVsm8F4wUVqSntt2sq6jav1lyavuA', 'm.room.message', 'm.text', '1141501302736695316', '112760669178241024', 0, 1), +('$51f4yqHinwnSbPEQ9dCgoyy4qiIJSX0QYYVUnvwyTCI', 'm.room.message', 'm.image', '1141501302736695316', '112760669178241024', 1, 1), +('$51f4yqHinwnSbPEQ9dCgoyy4qiIJSX0QYYVUnvwyTCJ', 'm.room.message', 'm.image', '1141501302736695317', '112760669178241024', 0, 1), +('$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M', 'm.room.message', 'm.text', '1128084851279536279', '112760669178241024', 0, 1), +('$YUJFa5j0ZJe7PUvD2DykRt9g51RoadUEYmuJLdSEbJ0', 'm.room.message', 'm.image', '1128084851279536279', '112760669178241024', 1, 1), +('$oLyUTyZ_7e_SUzGNWZKz880ll9amLZvXGbArJCKai2Q', 'm.room.message', 'm.text', '1128084748338741392', '112760669178241024', 0, 1); + +INSERT INTO file (discord_url, mxc_url) VALUES +('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'), +('https://cdn.discordapp.com/attachments/122155380120748034/1106366167486038016/image.png', 'mxc://cadence.moe/ZDCNYnkPszxGKgObUIFmvjus'), +('https://cdn.discordapp.com/stickers/1106323941183717586.png', 'mxc://cadence.moe/UuUaLwXhkxFRwwWCXipDlBHn'), +('https://cdn.discordapp.com/attachments/112760669178241024/1128084747910918195/skull.webp', 'mxc://cadence.moe/sDxWmDErBhYBxtDcJQgBETes'), +('https://cdn.discordapp.com/attachments/112760669178241024/1141501302497615912/piper_2.png', 'mxc://cadence.moe/KQYdXKRcHWjDYDLPkTOOWOjA'), +('https://cdn.discordapp.com/attachments/112760669178241024/1128084851023675515/RDT_20230704_0936184915846675925224905.jpg', 'mxc://cadence.moe/WlAbFSiNRIHPDEwKdyPeGywa'), +('https://cdn.discordapp.com/guilds/112760669178241024/users/134826546694193153/avatars/38dd359aa12bcd52dd3164126c587f8c.png?size=1024', 'mxc://cadence.moe/rfemHmAtcprjLEiPiEuzPhpl'), +('https://cdn.discordapp.com/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024', 'mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF'); + +COMMIT; diff --git a/matrix/file.js b/matrix/file.js index 64cd492..965ec1c 100644 --- a/matrix/file.js +++ b/matrix/file.js @@ -40,15 +40,8 @@ async function uploadDiscordFileToMxc(path) { // Download from Discord const promise = fetch(url, {}).then(/** @param {import("node-fetch").Response} res */ async res => { - const body = res.body - // Upload to Matrix - /** @type {import("../types").R.FileUploaded} */ - const root = await mreq.mreq("POST", "/media/v3/upload", body, { - headers: { - "Content-Type": res.headers.get("content-type") - } - }) + const root = await module.exports._actuallyUploadDiscordFileToMxc(url, res) // Store relationship in database db.prepare("INSERT INTO file (discord_url, mxc_url) VALUES (?, ?)").run(url, root.content_uri) @@ -61,6 +54,17 @@ async function uploadDiscordFileToMxc(path) { return promise } +async function _actuallyUploadDiscordFileToMxc(url, res) { + const body = res.body + /** @type {import("../types").R.FileUploaded} */ + const root = await mreq.mreq("POST", "/media/v3/upload", body, { + headers: { + "Content-Type": res.headers.get("content-type") + } + }) + return root +} + function guildIcon(guild) { return `/icons/${guild.id}/${guild.icon}.png?size=${IMAGE_SIZE}` } @@ -102,3 +106,4 @@ module.exports.emoji = emoji module.exports.stickerFormat = stickerFormat module.exports.sticker = sticker module.exports.uploadDiscordFileToMxc = uploadDiscordFileToMxc +module.exports._actuallyUploadDiscordFileToMxc = _actuallyUploadDiscordFileToMxc diff --git a/scripts/events.db b/scripts/events.db index 045e1471b97b6274aebcb9b2f457c3bc3ab5b898..86957d8684d132e60a394c3305b37bd63cad3e1c 100644 GIT binary patch delta 11883 zcmeHNdvqMtdDo0&$+EqYWlOR#I3BHESoZGD`?0|f$&zfzk|kM^B{_!m?9S}&NV_w; zJG0tdB}Oc6fW{8s40s@cBsSy}f)nCU>Zak8&?E;=Pt)SkoHR+3#t}{$P9ULqoR$LV zcV{J8euTJzlYfeBt+g|E?)~Q8@BY5u_xtW`KU#9zXKU}he!)X!Wo28B6Wg(e&K!EQ zc44sQ+i;<5tG#6Dq59gzwF_!~SM%RBzp8n)=BG91YrbFe?K9~L?5RJi>-T&Rn%z`f z37;PKwo?*BcVkK0h*+l04WB-*@BueeMZz5HZrQ>lT)fsk6NbB%&I;)YN;8YqQx{z zPM}++&&pH65+)0E7`G zTG0SROG+j7b_|*X3_!R85HkZ1XaE8YK)?WmWe{PNfB^`<-|w8yV=Hihx6_X{w9kJp zU9|$alrLiF78nr7rV@s%m~*dZ8dh8l7tDEYC-7vR5lOqr++dSiS4|CM+1=5x&d?Aq z?@xCQPKf>S5W9b5aBQD?AVtL`Vc*`)J|iLzhuA>evQlOsqUiqj#0LLZDpmuN7FV@c z-0}v&Ea%yZ@)c7VlNg*?Iq%^uUPUz{87*aLh7NPvE$7UvW(A3X(#vf$iFGvFLzr?x zMYGm1I6}na3H9;`Kj@WpLe?^9EfOlMsg{%@1^+>>_wdbIypg!9>uMsTDc+#-+62y5 zeYE~o&&O6^_Kt9Ut-uJRC~*``a}>ui$h{6h5Akf+^{keptSfSxW=4#R64-AfBC>8= z^oEQUiKB0tl|at%p{q8(nb@o52-A`?gsdp4;{D4LKilcdn6xlEuq-YPOex?dJjh;$F;+rM;K?v>zLs9K2W~! z>@6$Gzf*aZ^q0Sgomh%5sKQQ_Eu6qkok^d-sy^OV#5|u`U_Zz3HNqfG1vw^2iGEUK z?fz5R*&W09Yxq)@5;>X{B!QDCi6$lI8{wG%4~<`U19m3fy3$MIhIT zsYJ3sq@pPzt6N%v=p-i8j20~rhVI^o8@fuE23(IB$ca&7_gWaBpp%Fre}>C&6LvbZ zh(l}5a<9#Fq*t7d6tAqU0E3o9s*Z} z&N4*8h-qM~YQ+t7Q9+%DlClDCrV<&|R4vzP*)(&o3s4tRFs`DN8tX<59)P_Hy}|ag zEKQH0_Lojh$p!%HR2HBY05BqhdlH?^#6j>uG@0lt3~T^2>x8Ns*;xFbmjHxFq3M7- zVHNcpLNl~FDTBp1xd3*Qo(s8#cWz7)V_EyfTh=;nJ&$j4YGU{*XU7ZpLTB?B-n$^A z!1i!%J%BUzV53xdPi3*p^RWFjMgXs%#2^V?LHY%ee5(J{<5-O#u?#N=v?zd2^Y-gs zziFl!6(nhBCfT(gyrG=qY3Jmp@s5>(#E1;ZksQZzA}a;}gkq{crQ3DGE1jAnxYwC_ z0b9O>VyWNPG6vgZ7BMN3Bo8~=_h9>-A0ERRYfH}nQt`A%i=uP<4xC&jTZ9aO#8ks_ zA_35tRPC1E4mv+LhOM~#fmw7pd-P76$Cj4dnhuhJpCUPibkZxZMtjloH{cZIJb5>E zp)^Pta>qqxBnF z5^BCL!4@V1fSgv=y!!L@@h8@;5C%yxNHal_^Yb(V6StWpgCvPU>&g=q<;%NMVbgEr zv}DYMK{aoMtX#-74!kLw4A1pV3nB~9MN$+KNM&IOV*%e}OdsUU{(las{3r`-3tjO6PUhhUoIGZq-`c4CUmv=X0_2#S2Hdd^GGYfg0!8~fP4$vquH$k&zZ z6%K?CbdQYh>7r9|XcxCH>*xz>^9<;pSkl3@N<^<#UsxWKl-ar6Tf`; z{(FcUcg(fggr=?kQs?+%jW~RG%ZKM}psBe*-f;uX^4b|DvhHr0nk+p4`_R z*9t>I=f3f1ey`5?LQHFj(OP!(Uv)u#U+44PpW{XQ{OIzf62*!FD@uUcEXi`LQ+x?u zU!E$KJ6~(ZR#Je?GzT9dOG$#jBA1#+481%_AzY#n)N_E*0I}!)6T8lN@o{`{9b$$o zKs)R-k)$b+w(IWS=iJ+Y5tVo1Mcngo=eZ(|z>ozpDF^~Z3LI;{_}1$6^I4v65~zUB z$ixDQ`)fcmAyqH~ob$uV@*8%SC`Al{Z6K$HQACF=rmGP*VSp>95gCV@5J(3Gx``We zrn!muyR$?m>V#O!q2CqXOh)0t-jtD2^;kwu#b@{+1aQ?dT?`D<;c$eH3MvCTN@N*H zmPlD3RWTB#6-gx7a8#mYQ3;E1jUstY6l8`MSr!`b(Wp4{3Rim@m|1Y)d?93i?<3c_ ziHNeBZ?kAR*KL|SIno|&$%TBa*~ox=U^Efy=!qVf2=#`9kSy;Wo$-SA)bdT&Y`Sx^ zg{_;Kp{!S=C%d&RvxF^WXpojEi00_Po$>@)F<{%k!O+6Fx6v<%tGKHfH4=)N*-~Ox zCDK>oM=qb6U1>;z2y4ns4`pI_U?5n6K#Rz^3~AOMpj86$wnW@8EkAS#Tq}`Px6emO zfg=U!^6bY;0dwGh&;Yp*MTwC(2A*+=wVNGzc^=SwwZw7Y!z{#TRs^c;fsDNzpD3#s zFVEoy&yh-Q$N5AjEC-72T9xWMzp zs@bf72J#1Z=VwU%?3vGFE#;m|$c;lyfYndDf-ifY;Ji<8ToC=ktmu7$^FG0OF9?qF z*}L#2l(e}ZMe}}|67HzB8b0)pLqUxwotoAs)C7~8@UILN?0t(IY zBvR54Mc=ebl`}&rl#N8)P0#rx2oU`U_ymBGnFzaU-8YS33rfBxs zD5#ZGc4bC-T3RBJ0}|wEJEo*IMjT~wa%wO&RA?KX?Bw=`CPjUJf6KslS0){fw00Rj zeZ0`Yw7et#yrkzD%6~Car&-_%SIfUD{=^B`)Voi>MvW}WxIm8yCqPd?3Q*Jly(R$xS27K)!xh&grwd8D7(2GoRW!M{c z%(w2ElCWzW?6M?m>-=%v(fO~IhuJ6JShM!(*;cwGloCfeTRKxbFU%_b@&4X?zDF`d zMM?~*@Y2#J+q(DW#zVt9HO@%I`o+oCkfH4D?X^sXbq+lZ8SJH<*ewH%IKw+d z57yUo;VV!5-Sqtmzl`Sk6@>D!N@y1{bi&FAq3M}Hn`s>~M`d+wBH=86L65-CWfC6HgyTyf5A zS^WB&K#QR{9`XsEg2aJ_IzoVgbX_8mI?sbx!w3vTf>b3bg)yjX@uWF1k&^t7pe5~d zA6bEv*rq-2rWbgbS?Cuj{xA}M12@0R4rhvst|WOy(aLL|e`vXVJi4~(uHNDz&+*!s za0~kf3Rj>^@SJqforcwDCckIW;%L)ZvIG+sUHoo?J2iZ0prhwzqDPL!49g_;Wz9Iz zsk>6C^R1`w<+c&2T|B>!bI;%`%HlUunJB$q%MEpRAK*iiyA7*ttZy*WKL(YWLia>R z!jgwa7+;dhcgI|3x}=xl)3`CUPm7Efpcs}gA%itxgPCq@DL}}Pp+1U;325eE7Zej> z5Pghnh5$XpMCox9N^mktxk2}r2{V<@ET5^u4W;Rzeo$`#c_aZlI8lJo!I)7%xw$+( z7qCEhX(Fatp|q0CxTO`SQ=rgx5mS&6V1Yi~M07@pf(!*2i%5e!gKCrJ7QmoVV;4p` z2{{492NOXK6hT0Hhsq<$^`X9j%EeFvMj(TVDh0w_RMcyjfI&9G_&F_+a7{D7o>a=n zAY10N1)12>>+9%jA`a@aircv1i)wDlF{$dhrbdlC+&!od0j^I%5X5R{q|JlqH_GD zvdT^6|AfDECjCvkqL?q%+E2c@vKC}EPGCUnhixX2kj$?tU*~MwRKCKld+PzbSmX4b z!>?!sWUvck&pvk!e;KQ?2Z|ogUo3W`gg8h-H4a#<=m%lK>2JcA@{FEvPCkG)I$zJ> z+nmqE@Oo$Z`}k(tH(u|YegbQ7>eoT7;>#avaL9jvdle62jY|=vLe&vOI`|Jbfqi^@ zr;R<+U=JQ_upez}upj#CX8RjoX>^88V-0rS%MJF{M1$>VZ*=ZI4IH}dXV7aizS*(9 zi!Ha0wr_O4bP`)_zuI1pSvJ<;b#^?4Rn|zZv3!t`{JbbRum23->z}`ccdg?E&;((4 z9wHr#j>N@S!hoVoXa@FveB#T_@$X?bHo6rkZ&1%B5?j2bA}dJs&iA@7;_T99>;_JfQ!VT8lWnN z4juA3C-1_ErE?1;`6&=-B>GNT~UEmA5Z^J)kky1WuBv<68O^$3sR~i@Zg&)>&Vmi z-ez`#wG}dj4Uw&s5K*I1Mio_+qj(Z@DusiyAy5^NNP&&;&c9CM4{y5W_RcvITkF={ z*#TNoqPs$poNOi2X>*Su3w*NR8&8e2bmmiWiudKAv=GhZ+`790b$6(H(LPe}QS5q} zx6lQFhuhU4aPIlo?j6AP;S_itS9m>2M`qPk?=|IfN)9k?5S0UcyTs7rzq zm&)z-y+Qjv&CXCa-e9*KZE$u1 zdf4Ci=?c5*KkA*KW{7s?vXzc^4B$iuY^Z!0z~$u;;3M~)1{fGv6#xYARQF{kC!vbI ztHn7#jyEkMB_GKkB!F`SECZu)4z?8!0Wk733H$?&EbveOaOxIfTV-L_*i|By(0TB^`u@>0`dxUUq`YztQa13^0Dy0^ja~rVhHpAHz5}*Y>K|Y03 z5^mY_Qh1qr0AUPwb&QEbLEM$1YD59Lz*B;#N(`I^A(evE0GmhJ;lR_xm;aJ{XQ zxe=e5-JfmM+eYO?I^Lg8?`=!$`E4frpE9D`w?6fJ=p#+Ly^kpd$Dj|F*L0G`S_b(^t(v*x3?x1RBl zJaQ+R4AO7_f%U@>$eo7m;Ui6>DLJJXskjCeHdF&mWy5g9Efh`2G1D6a(#wJ(K6A2$ z0r!CL1C<&$CI?66Bv;zMBnb6EZ@s17>@%-!>btt1u!2lUVUh)biI>76&8rl{D>AD@ zS(=l=VI*nJyzv~XM-_V0Yd0-x=m*#a-vZ?vA`gpF9uy2=3TOrnE(mbu2LdFV0-C$X z!T|!Fhl&$$dbo%lhZYP6?*@?!aRQhuvZ6I3E6(|Dd`-RRqeyqgV3N5YNBY6T+zr?) z!McfX2u_5=fE74osC($h%@5E$NE&Dc7&GfU_Za3zRlE3`5OY`wEjh~1!TR%@bN<(OeNi{zuX-r8Bp8xYk8 z(w`}SeJGhx!L$sVUb2Yo+qV;)$~ZAP8BIcF18N#1FK}RRGVEqLNu;Eq6QOQ zS8ly^_FbCa`4CgpeKo}up2x7+7kZyfUlW@?eGrfR>De@LB2dKKbO$Io4M_w#P3Q!+ zVGDfkicp8}>IK!0lvO`+CjDS_)v?jyb)E-TAfNEKeoF&Vihf>lif>?_f9g|X50=+J zlm^ZWL Date: Sat, 19 Aug 2023 18:37:34 +1200 Subject: [PATCH 57/99] send a slightly more appropriate display name --- m2d/converters/event-to-message.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index 817ffff..74a45ce 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -16,11 +16,19 @@ function eventToMessage(event) { /** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]})[]} */ const messages = [] + let displayName = event.sender + let avatarURL = undefined + const match = event.sender.match(/^@(.*?):/) + if (match) { + displayName = match[1] + // TODO: get the media repo domain and the avatar url from the matrix member event + } + if (event.content.msgtype === "m.text") { messages.push({ content: event.content.body, - username: event.sender.replace(/^@/, ""), - avatar_url: undefined, // TODO: provide the URL to the avatar from the homeserver's content repo + username: displayName, + avatar_url: avatarURL }) } From 0fc8e68f15c35d490ec88013f8af75afaf5f33f2 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 19 Aug 2023 18:39:23 +1200 Subject: [PATCH 58/99] support threads --- d2m/actions/create-room.js | 68 +++++++++++++++++++++++----------- d2m/actions/create-space.js | 12 +++--- d2m/discord-packets.js | 6 +++ d2m/event-dispatcher.js | 61 ++++++++++++++++-------------- db/data-for-test.sql | 8 ++-- m2d/actions/channel-webhook.js | 5 ++- m2d/actions/send-event.js | 10 ++++- notes.md | 7 ++++ test/data.js | 58 +++++++++++++++++++++++++++++ 9 files changed, 172 insertions(+), 63 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 0fd0646..6d64d58 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -21,8 +21,8 @@ async function roomToKState(roomID) { } /** - * @params {string} roomID - * @params {any} kstate + * @param {string} roomID + * @param {any} kstate */ function applyKStateDiffToRoom(roomID, kstate) { const events = ks.kstateToState(kstate) @@ -51,7 +51,7 @@ function convertNameAndTopic(channel, guild, customName) { } /** - * @param {DiscordTypes.APIGuildTextChannel} channel + * @param {DiscordTypes.APIGuildTextChannel | DiscordTypes.APIThreadChannel} channel * @param {DiscordTypes.APIGuild} guild */ async function channelToKState(channel, guild) { @@ -98,21 +98,27 @@ async function channelToKState(channel, guild) { * @returns {Promise} room ID */ async function createRoom(channel, guild, spaceID, kstate) { + const [convertedName, convertedTopic] = convertNameAndTopic(channel, guild, null) const roomID = await api.createRoom({ - name: channel.name, - topic: channel.topic || undefined, + name: convertedName, + topic: convertedTopic, preset: "private_chat", visibility: "private", invite: ["@cadence:cadence.moe"], // TODO initial_state: ks.kstateToState(kstate) }) - db.prepare("INSERT INTO channel_room (channel_id, room_id) VALUES (?, ?)").run(channel.id, roomID) + let threadParent = null + if (channel.type === DiscordTypes.ChannelType.PublicThread) { + /** @type {DiscordTypes.APIThreadChannel} */ // @ts-ignore + const thread = channel + threadParent = thread.parent_id + } + + db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent) VALUES (?, ?, ?, NULL, ?)").run(channel.id, roomID, channel.name, threadParent) // Put the newly created child into the space - await api.sendState(spaceID, "m.space.child", roomID, { // TODO: should I deduplicate with the equivalent code from syncRoom? - via: ["cadence.moe"] // TODO: use the proper server - }) + _syncSpaceMember(channel, spaceID, roomID) return roomID } @@ -156,14 +162,15 @@ async function _syncRoom(channelID, shouldActuallySync) { assert.ok(channel) const guild = channelToGuild(channel) - /** @type {string?} */ - const existing = db.prepare("SELECT room_id from channel_room WHERE channel_id = ?").pluck().get(channel.id) + /** @type {{room_id: string, thread_parent: string?}} */ + const existing = db.prepare("SELECT room_id, thread_parent from channel_room WHERE channel_id = ?").get(channelID) + if (!existing) { const {spaceID, channelKState} = await channelToKState(channel, guild) return createRoom(channel, guild, spaceID, channelKState) } else { if (!shouldActuallySync) { - return existing // only need to ensure room exists, and it does. return the room ID + return existing.room_id // only need to ensure room exists, and it does. return the room ID } console.log(`[room sync] to matrix: ${channel.name}`) @@ -171,24 +178,41 @@ async function _syncRoom(channelID, shouldActuallySync) { const {spaceID, channelKState} = await channelToKState(channel, guild) // sync channel state to room - const roomKState = await roomToKState(existing) + const roomKState = await roomToKState(existing.room_id) const roomDiff = ks.diffKState(roomKState, channelKState) - const roomApply = applyKStateDiffToRoom(existing, roomDiff) + const roomApply = applyKStateDiffToRoom(existing.room_id, roomDiff) // sync room as space member - const spaceKState = await roomToKState(spaceID) - const spaceDiff = ks.diffKState(spaceKState, { - [`m.space.child/${existing}`]: { - via: ["cadence.moe"] // TODO: use the proper server - } - }) - const spaceApply = applyKStateDiffToRoom(spaceID, spaceDiff) + const spaceApply = _syncSpaceMember(channel, spaceID, existing.room_id) await Promise.all([roomApply, spaceApply]) - return existing + return existing.room_id } } +/** + * @param {DiscordTypes.APIGuildTextChannel} channel + * @param {string} spaceID + * @param {string} roomID + * @returns {Promise} + */ +async function _syncSpaceMember(channel, spaceID, roomID) { + const spaceKState = await roomToKState(spaceID) + let spaceEventContent = {} + if ( + channel.type !== DiscordTypes.ChannelType.PrivateThread // private threads do not belong in the space (don't offer people something they can't join) + || channel["thread_metadata"]?.archived // archived threads do not belong in the space (don't offer people conversations that are no longer relevant) + ) { + spaceEventContent = { + via: ["cadence.moe"] // TODO: use the proper server + } + } + const spaceDiff = ks.diffKState(spaceKState, { + [`m.space.child/${roomID}`]: spaceEventContent + }) + return applyKStateDiffToRoom(spaceID, spaceDiff) +} + function ensureRoom(channelID) { return _syncRoom(channelID, false) } diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index e3b6da7..02c2dcf 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -1,5 +1,6 @@ // @ts-check +const assert = require("assert") const passthrough = require("../../passthrough") const { sync, db } = passthrough /** @type {import("../../matrix/api")} */ @@ -9,13 +10,14 @@ const api = sync.require("../../matrix/api") * @param {import("discord-api-types/v10").RESTGetAPIGuildResult} guild */ async function createSpace(guild) { + assert(guild.name) const roomID = await api.createRoom({ name: guild.name, - preset: "private_chat", + preset: "private_chat", // cannot join space unless invited visibility: "private", power_level_content_override: { - events_default: 100, - invite: 50 + events_default: 100, // space can only be managed by bridge + invite: 0 // any existing member can invite others }, invite: ["@cadence:cadence.moe"], // TODO topic: guild.description || undefined, @@ -27,13 +29,13 @@ async function createSpace(guild) { type: "m.room.guest_access", state_key: "", content: { - guest_access: "can_join" + guest_access: "can_join" // guests can join space if other conditions are met } }, { type: "m.room.history_visibility", content: { - history_visibility: "invited" + history_visibility: "invited" // any events sent after user was invited are visible } } ] diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index c0ba1a6..79138b2 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -35,6 +35,12 @@ const utils = { arr.push(channel.id) client.channels.set(channel.id, channel) } + for (const thread of message.d.threads || []) { + // @ts-ignore + thread.guild_id = message.d.id + arr.push(thread.id) + client.channels.set(thread.id, thread) + } } else if (message.t === "GUILD_DELETE") { diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 8e64591..9387199 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -15,6 +15,10 @@ const api = sync.require("../matrix/api") let lastReportedEvent = 0 +function isGuildAllowed(guildID) { + return ["112760669178241024", "497159726455455754", "1100319549670301727"].includes(guildID) +} + // Grab Discord events we care about for the bridge, check them, and pass them on module.exports = { @@ -29,31 +33,34 @@ module.exports = { console.error(`while handling this ${gatewayMessage.t} gateway event:`) console.dir(gatewayMessage.d, {depth: null}) - if (Date.now() - lastReportedEvent > 5000) { - lastReportedEvent = Date.now() - const channelID = gatewayMessage.d.channel_id - if (channelID) { - const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(channelID) - let stackLines = e.stack.split("\n") - let cloudstormLine = stackLines.findIndex(l => l.includes("/node_modules/cloudstorm/")) - if (cloudstormLine !== -1) { - stackLines = stackLines.slice(0, cloudstormLine - 2) - } - api.sendEvent(roomID, "m.room.message", { - msgtype: "m.text", - body: "\u26a0 Bridged event from Discord not delivered. See formatted content for full details.", - format: "org.matrix.custom.html", - formatted_body: "\u26a0 Bridged event from Discord not delivered" - + `
Gateway event: ${gatewayMessage.t}` - + `
${stackLines.join("\n")}
` - + `
Original payload` - + `
${util.inspect(gatewayMessage.d, false, 4, false)}
`, - "m.mentions": { - user_ids: ["@cadence:cadence.moe"] - } - }) - } + if (Date.now() - lastReportedEvent < 5000) return + lastReportedEvent = Date.now() + + const channelID = gatewayMessage.d.channel_id + if (!channelID) return + const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(channelID) + if (!roomID) return + + let stackLines = e.stack.split("\n") + let cloudstormLine = stackLines.findIndex(l => l.includes("/node_modules/cloudstorm/")) + if (cloudstormLine !== -1) { + stackLines = stackLines.slice(0, cloudstormLine - 2) } + api.sendEvent(roomID, "m.room.message", { + msgtype: "m.text", + body: "\u26a0 Bridged event from Discord not delivered. See formatted content for full details.", + format: "org.matrix.custom.html", + formatted_body: "\u26a0 Bridged event from Discord not delivered" + + `
Gateway event: ${gatewayMessage.t}` + + `
${e.toString()}` + + `
Error trace` + + `
${stackLines.join("\n")}
` + + `
Original payload` + + `
${util.inspect(gatewayMessage.d, false, 4, false)}
`, + "m.mentions": { + user_ids: ["@cadence:cadence.moe"] + } + }) }, /** @@ -72,7 +79,7 @@ module.exports = { const channel = client.channels.get(message.channel_id) if (!channel.guild_id) return // Nothing we can do in direct messages. const guild = client.guilds.get(channel.guild_id) - if (message.guild_id !== "112760669178241024" && message.guild_id !== "497159726455455754") return // TODO: activate on other servers (requires the space creation flow to be done first) + if (!isGuildAllowed(guild.id)) return await sendMessage.sendMessage(message, guild) }, @@ -97,7 +104,7 @@ module.exports = { const channel = client.channels.get(message.channel_id) if (!channel.guild_id) return // Nothing we can do in direct messages. const guild = client.guilds.get(channel.guild_id) - if (message.guild_id !== "112760669178241024" && message.guild_id !== "497159726455455754") return // TODO: activate on other servers (requires the space creation flow to be done first) + if (!isGuildAllowed(guild.id)) return await editMessage.editMessage(message, guild) } }, @@ -109,7 +116,6 @@ module.exports = { async onReactionAdd(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. if (data.emoji.id !== null) return // TODO: image emoji reactions - console.log(data) await addReaction.addReaction(data) }, @@ -118,7 +124,6 @@ module.exports = { * @param {import("discord-api-types/v10").GatewayMessageDeleteDispatchData} data */ async onMessageDelete(client, data) { - console.log(data) await deleteMessage.deleteMessage(data) } } diff --git a/db/data-for-test.sql b/db/data-for-test.sql index fa04562..aa82c91 100644 --- a/db/data-for-test.sql +++ b/db/data-for-test.sql @@ -54,10 +54,10 @@ BEGIN TRANSACTION; INSERT INTO guild_space (guild_id, space_id) VALUES ('112760669178241024', '!jjWAGMeQdNrVZSSfvz:cadence.moe'); -INSERT INTO channel_room (channel_id, room_id, name, nick) VALUES -('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main'), -('497161350934560778', '!edUxjVdzgUvXDUIQCK:cadence.moe', 'amanda-spam', NULL), -('160197704226439168', '!uCtjHhfGlYbVnPVlkG:cadence.moe', 'the-stanley-parable-channel', 'bots'); +INSERT INTO channel_room (channel_id, room_id, name, nick, is_thread) VALUES +('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main', NULL, 0), +('497161350934560778', '!edUxjVdzgUvXDUIQCK:cadence.moe', 'amanda-spam', NULL, 0), +('160197704226439168', '!uCtjHhfGlYbVnPVlkG:cadence.moe', 'the-stanley-parable-channel', 'bots', 0); INSERT INTO sim (discord_id, sim_name, localpart, mxid) VALUES ('0', 'bot', '_ooye_bot', '@_ooye_bot:cadence.moe'), diff --git a/m2d/actions/channel-webhook.js b/m2d/actions/channel-webhook.js index b62057b..f5fd9a9 100644 --- a/m2d/actions/channel-webhook.js +++ b/m2d/actions/channel-webhook.js @@ -50,10 +50,11 @@ async function withWebhook(channelID, callback) { /** * @param {string} channelID * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]}} data + * @param {string} [threadID] */ -async function sendMessageWithWebhook(channelID, data) { +async function sendMessageWithWebhook(channelID, data, threadID) { const result = await withWebhook(channelID, async webhook => { - return discord.snow.webhook.executeWebhook(webhook.id, webhook.token, data, {wait: true, disableEveryone: true}) + return discord.snow.webhook.executeWebhook(webhook.id, webhook.token, data, {wait: true, thread_id: threadID, disableEveryone: true}) }) return result } diff --git a/m2d/actions/send-event.js b/m2d/actions/send-event.js index 3f49fa4..88ba0fd 100644 --- a/m2d/actions/send-event.js +++ b/m2d/actions/send-event.js @@ -13,7 +13,13 @@ const eventToMessage = sync.require("../converters/event-to-message") /** @param {import("../../types").Event.Outer} event */ async function sendEvent(event) { // TODO: we just assume the bridge has already been created - const channelID = db.prepare("SELECT channel_id FROM channel_room WHERE room_id = ?").pluck().get(event.room_id) + const row = db.prepare("SELECT channel_id, thread_parent FROM channel_room WHERE room_id = ?").get(event.room_id) + let channelID = row.channel_id + let threadID = undefined + if (row.thread_parent) { + threadID = channelID + channelID = row.thread_parent // it's the thread's parent... get with the times... + } // no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it @@ -24,7 +30,7 @@ async function sendEvent(event) { const messageResponses = [] let eventPart = 0 // 0 is primary, 1 is supporting for (const message of messages) { - const messageResponse = await channelWebhook.sendMessageWithWebhook(channelID, message) + const messageResponse = await channelWebhook.sendMessageWithWebhook(channelID, message, threadID) 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? diff --git a/notes.md b/notes.md index ec2b9bb..1dcbcd7 100644 --- a/notes.md +++ b/notes.md @@ -9,6 +9,13 @@ A database will be used to store the discord id to matrix event id mapping. Tabl There needs to be a way to easily manually trigger something later. For example, it should be easy to manually retry sending a message, or check all members for changes, etc. +## Current manual process for setting up a server + +1. Call createSpace.createSpace(discord.guilds.get(GUILD_ID)) +2. Call createRoom.createAllForGuild(GUILD_ID) +3. Edit source code of event-dispatcher.js isGuildAllowed() and add the guild ID to the list +4. If developing, make sure SSH port forward is activated, then wait for events to sync over! + ## Transforming content 1. Upload attachments to mxc if they are small enough. diff --git a/test/data.js b/test/data.js index a1d3ece..b579a24 100644 --- a/test/data.js +++ b/test/data.js @@ -816,6 +816,64 @@ module.exports = { format_type: 1, name: "pomu puff" }] + }, + message_in_thread: { + type: 0, + tts: false, + timestamp: "2023-08-19T01:55:02.063000+00:00", + referenced_message: null, + position: 942, + pinned: false, + nonce: "1142275498206822400", + mentions: [], + mention_roles: [], + mention_everyone: false, + member: { + roles: [ + "112767366235959296", "118924814567211009", + "204427286542417920", "199995902742626304", + "222168467627835392", "238028326281805825", + "259806643414499328", "265239342648131584", + "271173313575780353", "287733611912757249", + "225744901915148298", "305775031223320577", + "318243902521868288", "348651574924541953", + "349185088157777920", "378402925128712193", + "392141548932038658", "393912152173576203", + "482860581670486028", "495384759074160642", + "638988388740890635", "373336013109461013", + "530220455085473813", "454567553738473472", + "790724320824655873", "1123518980456452097", + "1040735082610167858", "695946570482450442", + "1123460940935991296", "849737964090556488" + ], + premium_since: null, + pending: false, + nick: null, + mute: false, + joined_at: "2015-11-11T09:55:40.321000+00:00", + flags: 0, + deaf: false, + communication_disabled_until: null, + avatar: null + }, + id: "1142275501721911467", + flags: 0, + embeds: [], + edited_timestamp: null, + content: "don't mind me, posting something for cadence", + components: [], + channel_id: "910283343378120754", + author: { + username: "kumaccino", + public_flags: 128, + id: "113340068197859328", + global_name: "kumaccino", + discriminator: "0", + avatar_decoration_data: null, + avatar: "b48302623a12bc7c59a71328f72ccb39" + }, + attachments: [], + guild_id: "112760669178241024" } }, message_update: { From 3f187e81077a4c0032248b3046ea73e044d6f8df Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 19 Aug 2023 18:40:01 +1200 Subject: [PATCH 59/99] see how this different reactions format goes... --- d2m/actions/add-reaction.js | 2 +- d2m/actions/edit-message.js | 3 --- m2d/actions/add-reaction.js | 8 +++++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/d2m/actions/add-reaction.js b/d2m/actions/add-reaction.js index 49afca9..b46af59 100644 --- a/d2m/actions/add-reaction.js +++ b/d2m/actions/add-reaction.js @@ -22,7 +22,7 @@ async function addReaction(data) { assert.equal(typeof parentID, "string") const roomID = await createRoom.ensureRoom(data.channel_id) const senderMxid = await registerUser.ensureSimJoined(user, roomID) - const eventID = api.sendEvent(roomID, "m.reaction", { + const eventID = await api.sendEvent(roomID, "m.reaction", { "m.relates_to": { rel_type: "m.annotation", event_id: parentID, diff --git a/d2m/actions/edit-message.js b/d2m/actions/edit-message.js index 8e8c838..fa152cf 100644 --- a/d2m/actions/edit-message.js +++ b/d2m/actions/edit-message.js @@ -12,10 +12,7 @@ const api = sync.require("../../matrix/api") * @param {import("discord-api-types/v10").APIGuild} guild */ async function editMessage(message, guild) { - console.log(`*** applying edit for message ${message.id} in channel ${message.channel_id}`) const {roomID, eventsToRedact, eventsToReplace, eventsToSend, senderMxid} = await editToChanges.editToChanges(message, guild, api) - console.log("making these changes:") - console.dir({eventsToRedact, eventsToReplace, eventsToSend}, {depth: null}) // 1. Replace all the things. for (const {oldID, newContent} of eventsToReplace) { diff --git a/m2d/actions/add-reaction.js b/m2d/actions/add-reaction.js index d259ddb..68828dd 100644 --- a/m2d/actions/add-reaction.js +++ b/m2d/actions/add-reaction.js @@ -18,10 +18,12 @@ async function addReaction(event) { // no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it let emoji = event.content["m.relates_to"].key // TODO: handle custom text or emoji reactions - emoji = encodeURIComponent(emoji) - emoji = emoji.replace(/%EF%B8%8F/g, "") + let encoded = encodeURIComponent(emoji) + let encodedTrimmed = encoded.replace(/%EF%B8%8F/g, "") - return discord.snow.channel.createReaction(channelID, messageID, emoji) + console.log("add reaction from matrix:", emoji, encoded, encodedTrimmed) + + return discord.snow.channel.createReaction(channelID, messageID, encoded) } module.exports.addReaction = addReaction From 4635200bcb8fa4a9ca58618887d138892b90df0c Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 19 Aug 2023 18:40:37 +1200 Subject: [PATCH 60/99] only use nickname as displayname, not nick+user --- d2m/actions/register-user.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index 19c3a6d..a33cecc 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -85,7 +85,8 @@ async function ensureSimJoined(user, roomID) { */ async function memberToStateContent(user, member, guildID) { let displayname = user.username - if (member.nick && member.nick !== displayname) displayname = member.nick + " | " + displayname // prepend nick if present + // if (member.nick && member.nick !== displayname) displayname = member.nick + " | " + displayname // prepend nick if present + if (member.nick) displayname = member.nick const content = { displayname, From 0f20dcab6d0e856553f9c4e7fd9d419becfb4627 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 19 Aug 2023 18:40:46 +1200 Subject: [PATCH 61/99] guard for errors on matrix event dispatcher --- m2d/event-dispatcher.js | 48 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/m2d/event-dispatcher.js b/m2d/event-dispatcher.js index 82ebd75..44eba85 100644 --- a/m2d/event-dispatcher.js +++ b/m2d/event-dispatcher.js @@ -1,9 +1,10 @@ // @ts-check -/** +/* * Grab Matrix events we care about, check them, and bridge them. */ +const util = require("util") const Ty = require("../types") const {sync, as} = require("../passthrough") @@ -13,21 +14,58 @@ const sendEvent = sync.require("./actions/send-event") const addReaction = sync.require("./actions/add-reaction") /** @type {import("./converters/utils")} */ const utils = sync.require("./converters/utils") +/** @type {import("../matrix/api")}) */ +const api = sync.require("../matrix/api") -sync.addTemporaryListener(as, "type:m.room.message", +let lastReportedEvent = 0 + +function guard(type, fn) { + return async function(event, ...args) { + try { + return await fn(event, ...args) + } catch (e) { + console.error("hit event-dispatcher's error handler with this exception:") + console.error(e) // TODO: also log errors into a file or into the database, maybe use a library for this? or just wing it? + console.error(`while handling this ${type} gateway event:`) + console.dir(event, {depth: null}) + + if (Date.now() - lastReportedEvent < 5000) return + lastReportedEvent = Date.now() + + let stackLines = e.stack.split("\n") + api.sendEvent(event.room_id, "m.room.message", { + msgtype: "m.text", + body: "\u26a0 Matrix event not delivered to Discord. See formatted content for full details.", + format: "org.matrix.custom.html", + formatted_body: "\u26a0 Matrix event not delivered to Discord" + + `
Event type: ${type}` + + `
${e.toString()}` + + `
Error trace` + + `
${stackLines.join("\n")}
` + + `
Original payload` + + `
${util.inspect(event, false, 4, false)}
`, + "m.mentions": { + user_ids: ["@cadence:cadence.moe"] + } + }) + } + } +} + +sync.addTemporaryListener(as, "type:m.room.message", guard("m.room.message", /** * @param {Ty.Event.Outer} event it is a m.room.message because that's what this listener is filtering for */ async event => { if (utils.eventSenderIsFromDiscord(event.sender)) return const messageResponses = await sendEvent.sendEvent(event) -}) +})) -sync.addTemporaryListener(as, "type:m.reaction", +sync.addTemporaryListener(as, "type:m.reaction", guard("m.reaction", /** * @param {Ty.Event.Outer} event it is a m.reaction because that's what this listener is filtering for */ async event => { if (utils.eventSenderIsFromDiscord(event.sender)) return await addReaction.addReaction(event) -}) +})) From 343675950467e660ae0d5e52dbd6d9dd4d082bfa Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 19 Aug 2023 22:54:23 +1200 Subject: [PATCH 62/99] catch up on missed d->m messages when logging in --- d2m/actions/create-room.js | 28 +++++ d2m/actions/register-user.test.js | 2 +- d2m/actions/send-message.js | 9 +- .../message-to-event.embeds.test.js | 40 +++++++ d2m/converters/message-to-event.js | 25 ++++- d2m/converters/message-to-event.test.js | 12 ++- d2m/converters/user-to-mxid.test.js | 6 +- d2m/discord-packets.js | 1 + d2m/event-dispatcher.js | 38 ++++++- db/data-for-test.sql | 9 +- m2d/actions/channel-webhook.js | 3 +- m2d/converters/event-to-message.js | 6 ++ m2d/converters/event-to-message.test.js | 2 +- stdin.js | 1 + test/data.js | 101 +++++++++++++++++- test/test.js | 1 + 16 files changed, 268 insertions(+), 16 deletions(-) create mode 100644 d2m/converters/message-to-event.embeds.test.js diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 6d64d58..0178347 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -190,6 +190,33 @@ async function _syncRoom(channelID, shouldActuallySync) { } } +async function _unbridgeRoom(channelID) { + /** @ts-ignore @type {DiscordTypes.APIGuildChannel} */ + const channel = discord.channels.get(channelID) + assert.ok(channel) + const roomID = db.prepare("SELECT room_id from channel_room WHERE channel_id = ?").pluck().get(channelID) + assert.ok(roomID) + const spaceID = db.prepare("SELECT space_id FROM guild_space WHERE guild_id = ?").pluck().get(channel.guild_id) + assert.ok(spaceID) + + // remove room from being a space member + await api.sendState(spaceID, "m.space.child", roomID, {}) + + // send a notification in the room + await api.sendEvent(roomID, "m.room.message", { + msgtype: "m.notice", + body: "⚠️ This room was removed from the bridge." + }) + + // leave room + await api.leaveRoom(roomID) + + // delete room from database + const {changes} = db.prepare("DELETE FROM channel_room WHERE room_id = ? AND channel_id = ?").run(roomID, channelID) + assert.equal(changes, 1) +} + + /** * @param {DiscordTypes.APIGuildTextChannel} channel * @param {string} spaceID @@ -237,3 +264,4 @@ module.exports.syncRoom = syncRoom module.exports.createAllForGuild = createAllForGuild module.exports.channelToKState = channelToKState module.exports._convertNameAndTopic = convertNameAndTopic +module.exports._unbridgeRoom = _unbridgeRoom diff --git a/d2m/actions/register-user.test.js b/d2m/actions/register-user.test.js index 0afce50..34470ba 100644 --- a/d2m/actions/register-user.test.js +++ b/d2m/actions/register-user.test.js @@ -8,7 +8,7 @@ test("member2state: general", async t => { await _memberToStateContent(testData.member.sheep.user, testData.member.sheep, testData.guild.general.id), { avatar_url: "mxc://cadence.moe/rfemHmAtcprjLEiPiEuzPhpl", - displayname: "The Expert's Submarine | aprilsong", + displayname: "The Expert's Submarine", membership: "join", "moe.cadence.ooye.member": { avatar: "/guilds/112760669178241024/users/134826546694193153/avatars/38dd359aa12bcd52dd3164126c587f8c.png?size=1024" diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index 2132905..a5c8dac 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -22,8 +22,11 @@ async function sendMessage(message, guild) { let senderMxid = null if (!message.webhook_id) { - assert(message.member) - senderMxid = await registerUser.syncUser(message.author, message.member, message.guild_id, roomID) + if (message.member) { // available on a gateway message create event + senderMxid = await registerUser.syncUser(message.author, message.member, message.guild_id, roomID) + } else { // well, good enough... + senderMxid = await registerUser.ensureSimJoined(message.author, roomID) + } } const events = await messageToEvent.messageToEvent(message, guild, {}, {api}) @@ -35,7 +38,7 @@ async function sendMessage(message, guild) { const eventWithoutType = {...event} delete eventWithoutType.$type - const eventID = await api.sendEvent(roomID, eventType, event, senderMxid) + const eventID = await api.sendEvent(roomID, eventType, event, senderMxid, new Date(message.timestamp).getTime()) 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 diff --git a/d2m/converters/message-to-event.embeds.test.js b/d2m/converters/message-to-event.embeds.test.js new file mode 100644 index 0000000..7972f13 --- /dev/null +++ b/d2m/converters/message-to-event.embeds.test.js @@ -0,0 +1,40 @@ +const {test} = require("supertape") +const {messageToEvent} = require("./message-to-event") +const data = require("../../test/data") +const Ty = require("../../types") + +/** + * @param {string} roomID + * @param {string} eventID + * @returns {(roomID: string, eventID: string) => Promise>} + */ +function mockGetEvent(t, roomID_in, eventID_in, outer) { + return async function(roomID, eventID) { + t.equal(roomID, roomID_in) + t.equal(eventID, eventID_in) + return new Promise(resolve => { + setTimeout(() => { + resolve({ + event_id: eventID_in, + room_id: roomID_in, + origin_server_ts: 1680000000000, + unsigned: { + age: 2245, + transaction_id: "$local.whatever" + }, + ...outer + }) + }) + }) + } +} + +test("message2event embeds: nothing but a field", async t => { + const events = await messageToEvent(data.message_with_embeds.nothing_but_a_field, data.guild.general, {}) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.mentions": {}, + msgtype: "m.text", + body: "Amanda" + }]) +}) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 29730d4..3808beb 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -108,6 +108,13 @@ async function messageToEvent(message, guild, options = {}, di) { addMention(repliedToEventSenderMxid) } + let msgtype = "m.text" + // Handle message type 4, channel name changed + if (message.type === DiscordTypes.MessageType.ChannelNameChange) { + msgtype = "m.emote" + message.content = "changed the channel name to **" + message.content + "**" + } + // Text content appears first if (message.content) { let content = message.content @@ -188,7 +195,7 @@ async function messageToEvent(message, guild, options = {}, di) { const newTextMessageEvent = { $type: "m.room.message", "m.mentions": mentions, - msgtype: "m.text", + msgtype, body: body } @@ -239,6 +246,22 @@ async function messageToEvent(message, guild, options = {}, di) { size: attachment.size } } + } else if (attachment.content_type?.startsWith("video/") && attachment.width && attachment.height) { + return { + $type: "m.room.message", + "m.mentions": mentions, + msgtype: "m.video", + url: await file.uploadDiscordFileToMxc(attachment.url), + external_url: attachment.url, + body: attachment.description || attachment.filename, + filename: attachment.filename, + info: { + mimetype: attachment.content_type, + w: attachment.width, + h: attachment.height, + size: attachment.size + } + } } else { return { $type: "m.room.message", diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index 86942a7..260ecda 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -330,4 +330,14 @@ test("message2event: very large attachment is linked instead of being uploaded", }]) }) -// TODO: read "edits of replies" in the spec +test("message2event: type 4 channel name change", async t => { + const events = await messageToEvent(data.special_message.thread_name_change, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.mentions": {}, + msgtype: "m.emote", + body: "changed the channel name to **worming**", + format: "org.matrix.custom.html", + formatted_body: "changed the channel name to worming" + }]) +}) diff --git a/d2m/converters/user-to-mxid.test.js b/d2m/converters/user-to-mxid.test.js index 8c4c430..1b31260 100644 --- a/d2m/converters/user-to-mxid.test.js +++ b/d2m/converters/user-to-mxid.test.js @@ -16,6 +16,10 @@ test("user2name: works on emojis", t => { t.equal(userToSimName({username: "🍪 Cookie Monster 🍪", discriminator: "0001"}), "cookie_monster") }) +test("user2name: works on single emoji at the end", t => { + t.equal(userToSimName({username: "Amanda 🎵", discriminator: "2192"}), "amanda") +}) + test("user2name: works on crazy name", t => { t.equal(userToSimName({username: "*** D3 &W (89) _7//-", discriminator: "0001"}), "d3_w_89__7//") }) @@ -34,4 +38,4 @@ test("user2name: uses ID if name becomes too short", t => { test("user2name: uses ID when name has only disallowed characters", t => { t.equal(userToSimName({username: "!@#$%^&*", discriminator: "0001", id: "9"}), "9") -}) \ No newline at end of file +}) diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index 79138b2..970bab4 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -41,6 +41,7 @@ const utils = { arr.push(thread.id) client.channels.set(thread.id, thread) } + eventDispatcher.checkMissedMessages(client, message.d) } else if (message.t === "GUILD_DELETE") { diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 9387199..273e89b 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -63,6 +63,42 @@ module.exports = { }) }, + /** + * When logging back in, check if we missed any conversations in any channels. Bridge up to 49 missed messages per channel. + * If more messages were missed, only the latest missed message will be posted. TODO: Consider bridging more, or post a warning when skipping history? + * This can ONLY detect new messages, not any other kind of event. Any missed edits, deletes, reactions, etc will not be bridged. + * @param {import("./discord-client")} client + * @param {import("discord-api-types/v10").GatewayGuildCreateDispatchData} guild + */ + async checkMissedMessages(client, guild) { + if (guild.unavailable) return + const bridgedChannels = db.prepare("SELECT channel_id FROM channel_room").pluck().all() + const prepared = db.prepare("SELECT message_id FROM event_message WHERE channel_id = ? AND message_id = ?").pluck() + for (const channel of guild.channels.concat(guild.threads)) { + if (!bridgedChannels.includes(channel.id)) continue + if (!channel.last_message_id) continue + const latestWasBridged = prepared.get(channel.id, channel.last_message_id) + if (latestWasBridged) continue + + /** More recent messages come first. */ + console.log(`[check missed messages] in ${channel.id} (${guild.name} / ${channel.name}) because its last message ${channel.last_message_id} is not in the database`) + const messages = await client.snow.channel.getChannelMessages(channel.id, {limit: 50}) + let latestBridgedMessageIndex = messages.findIndex(m => { + return prepared.get(channel.id, m.id) + }) + console.log(`[check missed messages] got ${messages.length} messages; last message that IS bridged is at position ${latestBridgedMessageIndex} in the channel`) + if (latestBridgedMessageIndex === -1) latestBridgedMessageIndex = 1 // rather than crawling the ENTIRE channel history, let's just bridge the most recent 1 message to make it up to date. + for (let i = Math.min(messages.length, latestBridgedMessageIndex)-1; i >= 0; i--) { + const simulatedGatewayDispatchData = { + guild_id: guild.id, + mentions: [], + ...messages[i] + } + await module.exports.onMessageCreate(client, simulatedGatewayDispatchData) + } + } + }, + /** * @param {import("./discord-client")} client * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message @@ -85,7 +121,7 @@ module.exports = { /** * @param {import("./discord-client")} client - * @param {import("discord-api-types/v10").GatewayMessageUpdateDispatchData} message + * @param {import("discord-api-types/v10").GatewayMessageUpdateDispatchData} data */ async onMessageUpdate(client, data) { if (data.webhook_id) { diff --git a/db/data-for-test.sql b/db/data-for-test.sql index aa82c91..ee31fe3 100644 --- a/db/data-for-test.sql +++ b/db/data-for-test.sql @@ -33,6 +33,7 @@ CREATE TABLE IF NOT EXISTS "channel_room" ( "room_id" TEXT NOT NULL UNIQUE, "name" TEXT, "nick" TEXT, + "thread_parent" TEXT, PRIMARY KEY("channel_id") ); CREATE TABLE IF NOT EXISTS "event_message" ( @@ -54,10 +55,10 @@ BEGIN TRANSACTION; INSERT INTO guild_space (guild_id, space_id) VALUES ('112760669178241024', '!jjWAGMeQdNrVZSSfvz:cadence.moe'); -INSERT INTO channel_room (channel_id, room_id, name, nick, is_thread) VALUES -('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main', NULL, 0), -('497161350934560778', '!edUxjVdzgUvXDUIQCK:cadence.moe', 'amanda-spam', NULL, 0), -('160197704226439168', '!uCtjHhfGlYbVnPVlkG:cadence.moe', 'the-stanley-parable-channel', 'bots', 0); +INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent) VALUES +('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main', NULL), +('497161350934560778', '!edUxjVdzgUvXDUIQCK:cadence.moe', 'amanda-spam', NULL, NULL), +('160197704226439168', '!uCtjHhfGlYbVnPVlkG:cadence.moe', 'the-stanley-parable-channel', 'bots', NULL); INSERT INTO sim (discord_id, sim_name, localpart, mxid) VALUES ('0', 'bot', '_ooye_bot', '@_ooye_bot:cadence.moe'), diff --git a/m2d/actions/channel-webhook.js b/m2d/actions/channel-webhook.js index f5fd9a9..6d39da7 100644 --- a/m2d/actions/channel-webhook.js +++ b/m2d/actions/channel-webhook.js @@ -41,9 +41,8 @@ async function ensureWebhook(channelID, forceCreate = false) { async function withWebhook(channelID, callback) { const webhook = await ensureWebhook(channelID, false) return callback(webhook).catch(e => { - console.error(e) // TODO: check if the error was webhook-related and if webhook.created === false, then: const webhook = ensureWebhook(channelID, true); return callback(webhook) - throw new Error(e) + throw e }) } diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index 74a45ce..b2c56a9 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -30,6 +30,12 @@ function eventToMessage(event) { username: displayName, avatar_url: avatarURL }) + } else if (event.content.msgtype === "m.emote") { + messages.push({ + content: `*${displayName} ${event.content.body}*`, + username: displayName, + avatar_url: avatarURL + }) } return messages diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index e687059..f0c4664 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -21,7 +21,7 @@ test("event2message: janky test", t => { } }), [{ - username: "cadence:cadence.moe", + username: "cadence", content: "test", avatar_url: undefined }] diff --git a/stdin.js b/stdin.js index 7e0db89..61a2a08 100644 --- a/stdin.js +++ b/stdin.js @@ -13,6 +13,7 @@ const registerUser = sync.require("./d2m/actions/register-user") const mreq = sync.require("./matrix/mreq") const api = sync.require("./matrix/api") const sendEvent = sync.require("./m2d/actions/send-event") +const eventDispatcher = sync.require("./d2m/event-dispatcher") const guildID = "112760669178241024" const extraContext = {} diff --git a/test/data.js b/test/data.js index b579a24..fc8cbbd 100644 --- a/test/data.js +++ b/test/data.js @@ -864,7 +864,7 @@ module.exports = { components: [], channel_id: "910283343378120754", author: { - username: "kumaccino", + username: "kumaccino", public_flags: 128, id: "113340068197859328", global_name: "kumaccino", @@ -876,6 +876,72 @@ module.exports = { guild_id: "112760669178241024" } }, + message_with_embeds: { + nothing_but_a_field: { + guild_id: "497159726455455754", + mentions: [], + id: "1141934888862351440", + type: 20, + content: "", + channel_id: "497161350934560778", + author: { + id: "1109360903096369153", + username: "Amanda 🎵", + avatar: "d56cd1b26e043ae512edae2214962faa", + discriminator: "2192", + public_flags: 524288, + flags: 524288, + bot: true, + banner: null, + accent_color: null, + global_name: null, + avatar_decoration_data: null, + banner_color: null + }, + attachments: [], + embeds: [ + { + type: "rich", + color: 3092790, + fields: [ + { + name: "Amanda 🎵#2192 <:online:606664341298872324>\nwillow tree, branch 0", + value: "**❯ Uptime:**\n3m 55s\n**❯ Memory:**\n64.45MB", + inline: false + } + ] + } + ], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2023-08-18T03:21:33.629000+00:00", + edited_timestamp: null, + flags: 0, + components: [], + application_id: "1109360903096369153", + interaction: { + id: "1141934887608254475", + type: 2, + name: "stats", + user: { + id: "320067006521147393", + username: "papiophidian", + avatar: "47a19b0445069b826e136da4df4259bb", + discriminator: "0", + public_flags: 4194880, + flags: 4194880, + banner: null, + accent_color: null, + global_name: "PapiOphidian", + avatar_decoration_data: null, + banner_color: null + } + }, + webhook_id: "1109360903096369153" + } + }, message_update: { edit_by_webhook: { application_id: "684280192553844747", @@ -1277,5 +1343,38 @@ module.exports = { ], guild_id: "112760669178241024" } + }, + special_message: { + thread_name_change: { + id: "1142391602799710298", + type: 4, + content: "worming", + channel_id: "1142271000067706880", + author: { + id: "772659086046658620", + username: "cadence.worm", + avatar: "4b5c4b28051144e4c111f0113a0f1cf1", + discriminator: "0", + public_flags: 0, + flags: 0, + banner: null, + accent_color: null, + global_name: "cadence", + avatar_decoration_data: null, + banner_color: null + }, + attachments: [], + embeds: [], + mentions: [], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2023-08-19T09:36:22.717000+00:00", + edited_timestamp: null, + flags: 0, + components: [], + position: 12 + } } } diff --git a/test/test.js b/test/test.js index 31ab099..03394f0 100644 --- a/test/test.js +++ b/test/test.js @@ -21,6 +21,7 @@ require("../matrix/kstate.test") require("../matrix/api.test") require("../matrix/read-registration.test") require("../d2m/converters/message-to-event.test") +require("../d2m/converters/message-to-event.embeds.test") require("../d2m/converters/edit-to-changes.test") require("../d2m/actions/create-room.test") require("../d2m/converters/user-to-mxid.test") From 08d3f3d804b54e6bc545f34f3658eb03c5dc785f Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 19 Aug 2023 23:12:36 +1200 Subject: [PATCH 63/99] synchronise channel updates --- d2m/actions/create-room.js | 7 +++++-- d2m/discord-packets.js | 12 +++++++++++- d2m/event-dispatcher.js | 13 +++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 0178347..484470e 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -252,8 +252,11 @@ async function createAllForGuild(guildID) { const channelIDs = discord.guildChannelMap.get(guildID) assert.ok(channelIDs) for (const channelID of channelIDs) { - if (discord.channels.get(channelID)?.type === DiscordTypes.ChannelType.GuildText) { // TODO: guild sync thread channels and such. maybe make a helper function to check if a given channel is syncable? - await syncRoom(channelID).then(r => console.log(`synced ${channelID}:`, r)) + const allowedTypes = [DiscordTypes.ChannelType.GuildText, DiscordTypes.ChannelType.PublicThread] + // @ts-ignore + if (allowedTypes.includes(discord.channels.get(channelID)?.type)) { + const roomID = await syncRoom(channelID) + console.log(`synced ${channelID} <-> ${roomID}`) } } } diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index 970bab4..f172321 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -44,6 +44,10 @@ const utils = { eventDispatcher.checkMissedMessages(client, message.d) + } else if (message.t === "CHANNEL_UPDATE" || message.t === "THREAD_UPDATE") { + client.channels.set(message.d.id, message.d) + + } else if (message.t === "GUILD_DELETE") { client.guilds.delete(message.d.id) const channels = client.guildChannelMap.get(message.d.id) @@ -74,7 +78,13 @@ const utils = { // Event dispatcher for OOYE bridge operations try { - if (message.t === "MESSAGE_CREATE") { + if (message.t === "CHANNEL_UPDATE") { + await eventDispatcher.onChannelOrThreadUpdate(client, message.d, false) + + } else if (message.t === "THREAD_UPDATE") { + await eventDispatcher.onChannelOrThreadUpdate(client, message.d, true) + + } else if (message.t === "MESSAGE_CREATE") { await eventDispatcher.onMessageCreate(client, message.d) } else if (message.t === "MESSAGE_UPDATE") { diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 273e89b..ad2a2d2 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/create-room")}) */ +const createRoom = sync.require("./actions/create-room") /** @type {import("../matrix/api")}) */ const api = sync.require("../matrix/api") @@ -99,6 +101,17 @@ module.exports = { } }, + /** + * @param {import("./discord-client")} client + * @param {import("discord-api-types/v10").GatewayChannelUpdateDispatchData} channelOrThread + * @param {boolean} isThread + */ + async onChannelOrThreadUpdate(client, channelOrThread, isThread) { + const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").get(channelOrThread.id) + if (!roomID) return // No target room to update the data on + await createRoom.syncRoom(channelOrThread.id) + }, + /** * @param {import("./discord-client")} client * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message From d666c0aedb3ad97b78ad85ef4a915ce6317548fe Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 21 Aug 2023 08:07:05 +1200 Subject: [PATCH 64/99] attempt to support THREAD_CREATE but it explodes --- d2m/converters/message-to-event.js | 10 ++++++---- d2m/discord-packets.js | 10 ++++++++++ d2m/event-dispatcher.js | 13 ++++++++++++- notes.md | 10 ++++++++++ 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 3808beb..c34b389 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -27,11 +27,13 @@ function getDiscordParseCallbacks(message, useHTML) { }, /** @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) { - return `#${nick || name}` + const row = db.prepare("SELECT room_id, name, nick FROM channel_room WHERE channel_id = ?").get(node.id) + if (!row) { + return `<#${node.id}>` // fallback for when this channel is not bridged + } else if (useHTML) { + return `#${row.nick || row.name}` } else { - return `#${nick || name}` + return `#${row.nick || row.name}` } }, /** @param {{animated: boolean, name: string, id: string, type: "discordEmoji"}} node */ diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index f172321..776d4b1 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -44,6 +44,10 @@ const utils = { eventDispatcher.checkMissedMessages(client, message.d) + } else if (message.t === "THREAD_CREATE") { + client.channels.set(message.d.id, message.d) + + } else if (message.t === "CHANNEL_UPDATE" || message.t === "THREAD_UPDATE") { client.channels.set(message.d.id, message.d) @@ -81,13 +85,19 @@ const utils = { if (message.t === "CHANNEL_UPDATE") { await eventDispatcher.onChannelOrThreadUpdate(client, message.d, false) + } else if (message.t === "THREAD_CREATE") { + console.log(message) + // await eventDispatcher.onThreadCreate(client, message.d) + } else if (message.t === "THREAD_UPDATE") { await eventDispatcher.onChannelOrThreadUpdate(client, message.d, true) } else if (message.t === "MESSAGE_CREATE") { + console.log(message) await eventDispatcher.onMessageCreate(client, message.d) } else if (message.t === "MESSAGE_UPDATE") { + console.log(message) await eventDispatcher.onMessageUpdate(client, message.d) } else if (message.t === "MESSAGE_DELETE") { diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index ad2a2d2..154cb34 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -101,7 +101,18 @@ module.exports = { } }, - /** + /** + * @param {import("./discord-client")} client + * @param {import("discord-api-types/v10").APIChannel} thread + */ + async onThreadCreate(client, thread) { + console.log(thread) + const parentRoomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").get(thread.parent_id) + if (!parentRoomID) return // Not interested in a thread if we aren't interested in its wider channel + await createRoom.syncRoom(thread.id) + }, + + /** * @param {import("./discord-client")} client * @param {import("discord-api-types/v10").GatewayChannelUpdateDispatchData} channelOrThread * @param {boolean} isThread diff --git a/notes.md b/notes.md index 1dcbcd7..dbc5ecd 100644 --- a/notes.md +++ b/notes.md @@ -9,6 +9,16 @@ A database will be used to store the discord id to matrix event id mapping. Tabl There needs to be a way to easily manually trigger something later. For example, it should be easy to manually retry sending a message, or check all members for changes, etc. +## Discord's gateway when a new thread is created from an existing message: + +1. Regular MESSAGE_CREATE of the message that it's going to branch off in the future. Example ID -6423 +2. It MESSAGE_UPDATEd the ID -6423 with this whole data: {id:-6423,flags: 32,channel_id:-2084,guild_id:-1727} (ID is the message ID it's branching off, channel ID is the parent channel containing the message ID it's branching off) +3. It THREAD_CREATEd and gave us a channel object with type 11 (public thread) and parent ID -2084 and ID -6423. +4. It MESSAGE_CREATEd type 21 with blank content and a message reference pointing towards channel -2084 message -6423. (That's the message it branched from in the parent channel.) This MESSAGE_CREATE got ID -4631 (a new ID). Apart from that it's a regular message object. +5. Finally, as the first "real" message in that thread (which a user must send to create that thread!) it sent a regular message object with a new message ID and a channel ID of -6423. + +When viewing this thread, it shows the message branched from at the top, and then the first "real" message right underneath, as separate groups. + ## Current manual process for setting up a server 1. Call createSpace.createSpace(discord.guilds.get(GUILD_ID)) From c22f434c1f64b5ae684dabb9ccecf2fa37f61fdd Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 21 Aug 2023 17:25:51 +1200 Subject: [PATCH 65/99] explain inflight thread room creation --- d2m/actions/announce-thread.js | 44 ++++++++++++++++++ d2m/actions/create-room.js | 68 ++++++++++++++++------------ d2m/event-dispatcher.js | 7 ++- notes.md | 82 ++++++++++++++++++++++++++++++++-- 4 files changed, 166 insertions(+), 35 deletions(-) create mode 100644 d2m/actions/announce-thread.js diff --git a/d2m/actions/announce-thread.js b/d2m/actions/announce-thread.js new file mode 100644 index 0000000..546d307 --- /dev/null +++ b/d2m/actions/announce-thread.js @@ -0,0 +1,44 @@ +// @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 {string} parentRoomID + * @param {string} threadRoomID + * @param {import("discord-api-types/v10").APIThreadChannel} thread + */ +async function announceThread(parentRoomID, threadRoomID, thread) { + /** @type {string?} */ + const creatorMxid = db.prepare("SELECT mxid FROM sim WHERE discord_id = ?").pluck().get(thread.owner_id) + /** @type {string?} */ + const branchedFromEventID = db.prepare("SELECT event_id FROM event_message WHERE message_id = ?").get(thread.id) + + const msgtype = creatorMxid ? "m.emote" : "m.text" + const template = creatorMxid ? "started a thread:" : "Thread started:" + let body = `${template} ${thread.name} https://matrix.to/#/${threadRoomID}` + let html = `${template} ${thread.name}` + + const mentions = {} + + await api.sendEvent(parentRoomID, "m.room.message", { + msgtype, + body: `${template} , + format: "org.matrix.custom.html", + formatted_body: "", + "m.mentions": mentions + + }, creatorMxid) +} + +module.exports.announceThread = announceThread diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 484470e..2ee0913 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -12,6 +12,9 @@ const api = sync.require("../../matrix/api") /** @type {import("../../matrix/kstate")} */ const ks = sync.require("../../matrix/kstate") +/** @type {Map>} channel ID -> Promise */ +const inflightRoomCreate = new Map() + /** * @param {string} roomID */ @@ -98,23 +101,20 @@ async function channelToKState(channel, guild) { * @returns {Promise} room ID */ async function createRoom(channel, guild, spaceID, kstate) { + let threadParent = null + if (channel.type === DiscordTypes.ChannelType.PublicThread) threadParent = channel.parent_id + const invite = threadParent ? [] : ["@cadence:cadence.moe"] // TODO + const [convertedName, convertedTopic] = convertNameAndTopic(channel, guild, null) const roomID = await api.createRoom({ name: convertedName, topic: convertedTopic, preset: "private_chat", visibility: "private", - invite: ["@cadence:cadence.moe"], // TODO + invite, initial_state: ks.kstateToState(kstate) }) - let threadParent = null - if (channel.type === DiscordTypes.ChannelType.PublicThread) { - /** @type {DiscordTypes.APIThreadChannel} */ // @ts-ignore - const thread = channel - threadParent = thread.parent_id - } - db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent) VALUES (?, ?, ?, NULL, ?)").run(channel.id, roomID, channel.name, threadParent) // Put the newly created child into the space @@ -162,32 +162,42 @@ async function _syncRoom(channelID, shouldActuallySync) { assert.ok(channel) const guild = channelToGuild(channel) + if (inflightRoomCreate.has(channelID)) { + await inflightRoomCreate.get(channelID) // just waiting, and then doing a new db query afterwards, is the simplest way of doing it + } + /** @type {{room_id: string, thread_parent: string?}} */ const existing = db.prepare("SELECT room_id, thread_parent from channel_room WHERE channel_id = ?").get(channelID) if (!existing) { - const {spaceID, channelKState} = await channelToKState(channel, guild) - return createRoom(channel, guild, spaceID, channelKState) - } else { - if (!shouldActuallySync) { - return existing.room_id // only need to ensure room exists, and it does. return the room ID - } - - console.log(`[room sync] to matrix: ${channel.name}`) - - const {spaceID, channelKState} = await channelToKState(channel, guild) - - // sync channel state to room - const roomKState = await roomToKState(existing.room_id) - const roomDiff = ks.diffKState(roomKState, channelKState) - const roomApply = applyKStateDiffToRoom(existing.room_id, roomDiff) - - // sync room as space member - const spaceApply = _syncSpaceMember(channel, spaceID, existing.room_id) - await Promise.all([roomApply, spaceApply]) - - return existing.room_id + const creation = (async () => { + const {spaceID, channelKState} = await channelToKState(channel, guild) + const roomID = await createRoom(channel, guild, spaceID, channelKState) + inflightRoomCreate.delete(channelID) // OK to release inflight waiters now. they will read the correct `existing` row + return roomID + })() + inflightRoomCreate.set(channelID, creation) + return creation // Naturally, the newly created room is already up to date, so we can always skip syncing here. } + + if (!shouldActuallySync) { + return existing.room_id // only need to ensure room exists, and it does. return the room ID + } + + console.log(`[room sync] to matrix: ${channel.name}`) + + const {spaceID, channelKState} = await channelToKState(channel, guild) + + // sync channel state to room + const roomKState = await roomToKState(existing.room_id) + const roomDiff = ks.diffKState(roomKState, channelKState) + const roomApply = applyKStateDiffToRoom(existing.room_id, roomDiff) + + // sync room as space member + const spaceApply = _syncSpaceMember(channel, spaceID, existing.room_id) + await Promise.all([roomApply, spaceApply]) + + return existing.room_id } async function _unbridgeRoom(channelID) { diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 154cb34..6e4ffc4 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -102,14 +102,17 @@ module.exports = { }, /** + * Announces to the parent room that the thread room has been created. + * See notes.md, "Ignore MESSAGE_UPDATE and bridge THREAD_CREATE as the announcement" * @param {import("./discord-client")} client - * @param {import("discord-api-types/v10").APIChannel} thread + * @param {import("discord-api-types/v10").APIThreadChannel} thread */ async onThreadCreate(client, thread) { console.log(thread) const parentRoomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").get(thread.parent_id) if (!parentRoomID) return // Not interested in a thread if we aren't interested in its wider channel - await createRoom.syncRoom(thread.id) + const threadRoomID = await createRoom.syncRoom(thread.id) // Create room (will share the same inflight as the initial message to the thread) + await announceThread.announceThread(parentRoomID, threadRoomID, thread) }, /** diff --git a/notes.md b/notes.md index dbc5ecd..7383d3e 100644 --- a/notes.md +++ b/notes.md @@ -19,10 +19,48 @@ There needs to be a way to easily manually trigger something later. For example, When viewing this thread, it shows the message branched from at the top, and then the first "real" message right underneath, as separate groups. +### Problem 1 + +If THREAD_CREATE creates the matrix room, this will still be in-flight when MESSAGE_CREATE ensures the room exists and creates a room too. There will be two rooms created and the bridge falls over. + +#### Possible solution: Ignore THREAD_CREATE + +Then the room will be implicitly created by the two MESSAGE_CREATEs, which are in series. + +#### Possible solution: Store in-flight room creations ✔️ + +Then the room will definitely only be created once, and we can still handle both events if we want to do special things for THREAD_CREATE. + +#### Possible solution: Don't implicitly create rooms + +But then old and current threads would never have their messages bridged unless I manually intervene. Don't like that. + +### Problem 2 + +MESSAGE_UPDATE with flags=32 is telling that message to become an announcement of the new thread's creation, but this happens before THREAD_CREATE. The matrix room won't actually exist when we see MESSAGE_UPDATE, therefore we cannot make the MESSAGE_UPDATE link to the new thread. + +#### Possible solution: Ignore MESSAGE_UPDATE and bridge THREAD_CREATE as the announcement ✔️ + +When seeing THREAD_CREATE (if we use solution B above) we could react to it by creating the thread announcement message in the parent channel. This is possible because THREAD_CREATE gives a thread object and that includes the parent channel ID to send the announcement message to. + +While the thread announcement message could look more like Discord-side by being an edit of the message it branched off: + +> look at my cat +> +> Thread started: [#cat thread] + +if the thread branched off a matrix user's message then the bridge wouldn't be able to edit it, so this wouldn't work. + +Regardless, it would make the most sense to post a new message like this to the parent room: + +> > Reply to: look at my cat +> +> [me] started a new thread: [#cat thread] + ## Current manual process for setting up a server 1. Call createSpace.createSpace(discord.guilds.get(GUILD_ID)) -2. Call createRoom.createAllForGuild(GUILD_ID) +2. Call createRoom.createAllForGuild(GUILD_ID) // TODO: Only create rooms that the bridge bot has read permissions in! 3. Edit source code of event-dispatcher.js isGuildAllowed() and add the guild ID to the list 4. If developing, make sure SSH port forward is activated, then wait for events to sync over! @@ -101,9 +139,9 @@ Can use custom transaction ID (?) to send the original timestamps to Matrix. See ## Webhook message sent - Consider using the _ooye_bot account to send all webhook messages to prevent extraneous joins? - - Downside: the profile information from the most recently sent message would stick around in the member list. This is toleable. + - Downside: the profile information from the most recently sent message would stick around in the member list. This is tolerable. - Otherwise, could use an account per webhook ID, but if webhook IDs are often deleted and re-created, this could still end up leaving too many accounts in the room. -- The original bridge uses an account per webhook display name, which does the most sense in terms of canonical accounts, but leaves too many accounts in the room. +- The original bridge uses an account per webhook display name, which makes the most sense in terms of canonical accounts, but leaves too many accounts in the room. ## Message deleted @@ -113,7 +151,9 @@ Can use custom transaction ID (?) to send the original timestamps to Matrix. See ## Message edited / embeds added 1. Look up equivalents on matrix. -2. Replace content on matrix. +2. Transform content. +3. Build replacement event with fallbacks. +4. Send to matrix. ## Reaction added @@ -148,3 +188,37 @@ Can use custom transaction ID (?) to send the original timestamps to Matrix. See 3. The emojis may now be sent by Matrix users! TOSPEC: m2d emoji uploads?? + +## Issues if the bridge database is rolled back + +### channel_room table + +- Duplicate rooms will be created on matrix. + +### sim table + +- Sims will already be registered, registration will fail, all events from those sims will fail. + +### sim_member table + +- Sims won't be invited because they are already joined, all events from those sims will fail. + +### guild_space table + +- channelToKState will fail, so channel data differences won't be calculated, so channel/thread creation and sync will fail. + +### event_message table + +- Events referenced by other events will be dropped, for example + - edits will be ignored + - deletes will be ignored + - reactions will be ignored + - replies won't generate a reply + +### file + +- Some files like avatars may be re-uploaded to the matrix content repository, secretly taking more storage space on the server. + +### webhook + +- Some duplicate webhooks may be created. From c8021cadecb5c84b69a206413b4eb100ad9f76f2 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 21 Aug 2023 21:04:41 +1200 Subject: [PATCH 66/99] changing spaces to tabs --- .vscode/settings.json | 3 + d2m/actions/add-reaction.js | 22 ++--- m2d/actions/channel-webhook.js | 8 +- m2d/actions/send-event.js | 10 +-- matrix/api.js | 129 +++++++++++++++++----------- matrix/kstate.test.js | 26 +++--- matrix/read-registration.test.js | 10 +-- scripts/save-channel-names-to-db.js | 48 +++++------ scripts/save-event-types-to-db.js | 18 ++-- 9 files changed, 154 insertions(+), 120 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 2c63c08..9f1e183 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,2 +1,5 @@ { + "editor.insertSpaces": false, + "editor.detectIndentation": false, + "editor.tabSize": 3 } diff --git a/d2m/actions/add-reaction.js b/d2m/actions/add-reaction.js index b46af59..2eab8f0 100644 --- a/d2m/actions/add-reaction.js +++ b/d2m/actions/add-reaction.js @@ -15,20 +15,20 @@ const createRoom = sync.require("../actions/create-room") * @param {import("discord-api-types/v10").GatewayMessageReactionAddDispatchData} data */ async function addReaction(data) { - const user = data.member?.user - assert.ok(user && user.username) - const parentID = db.prepare("SELECT event_id FROM event_message WHERE message_id = ? AND part = 0").pluck().get(data.message_id) // 0 = primary - if (!parentID) return // Nothing can be done if the parent message was never bridged. - assert.equal(typeof parentID, "string") + const user = data.member?.user + assert.ok(user && user.username) + const parentID = db.prepare("SELECT event_id FROM event_message WHERE message_id = ? AND part = 0").pluck().get(data.message_id) // 0 = primary + if (!parentID) return // Nothing can be done if the parent message was never bridged. + assert.equal(typeof parentID, "string") const roomID = await createRoom.ensureRoom(data.channel_id) const senderMxid = await registerUser.ensureSimJoined(user, roomID) const eventID = await api.sendEvent(roomID, "m.reaction", { - "m.relates_to": { - rel_type: "m.annotation", - event_id: parentID, - key: data.emoji.name - } - }, senderMxid) + "m.relates_to": { + rel_type: "m.annotation", + event_id: parentID, + key: data.emoji.name + } + }, senderMxid) return eventID } diff --git a/m2d/actions/channel-webhook.js b/m2d/actions/channel-webhook.js index 6d39da7..b0bc072 100644 --- a/m2d/actions/channel-webhook.js +++ b/m2d/actions/channel-webhook.js @@ -52,10 +52,10 @@ async function withWebhook(channelID, callback) { * @param {string} [threadID] */ async function sendMessageWithWebhook(channelID, data, threadID) { - const result = await withWebhook(channelID, async webhook => { - return discord.snow.webhook.executeWebhook(webhook.id, webhook.token, data, {wait: true, thread_id: threadID, disableEveryone: true}) - }) - return result + const result = await withWebhook(channelID, async webhook => { + return discord.snow.webhook.executeWebhook(webhook.id, webhook.token, data, {wait: true, thread_id: threadID, disableEveryone: true}) + }) + return result } module.exports.ensureWebhook = ensureWebhook diff --git a/m2d/actions/send-event.js b/m2d/actions/send-event.js index 88ba0fd..39eed22 100644 --- a/m2d/actions/send-event.js +++ b/m2d/actions/send-event.js @@ -12,7 +12,7 @@ const eventToMessage = sync.require("../converters/event-to-message") /** @param {import("../../types").Event.Outer} event */ async function sendEvent(event) { - // TODO: we just assume the bridge has already been created + // TODO: we just assume the bridge has already been created const row = db.prepare("SELECT channel_id, thread_parent FROM channel_room WHERE room_id = ?").get(event.room_id) let channelID = row.channel_id let threadID = undefined @@ -21,16 +21,16 @@ async function sendEvent(event) { channelID = row.thread_parent // it's the thread's parent... get with the times... } - // no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it + // no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it const messages = eventToMessage.eventToMessage(event) - assert(Array.isArray(messages)) // sanity + assert(Array.isArray(messages)) // sanity - /** @type {DiscordTypes.APIMessage[]} */ + /** @type {DiscordTypes.APIMessage[]} */ const messageResponses = [] let eventPart = 0 // 0 is primary, 1 is supporting for (const message of messages) { - const messageResponse = await channelWebhook.sendMessageWithWebhook(channelID, message, threadID) + const messageResponse = await channelWebhook.sendMessageWithWebhook(channelID, message, threadID) 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? diff --git a/matrix/api.js b/matrix/api.js index 81d8a16..b382631 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -19,15 +19,15 @@ const makeTxnId = sync.require("./txnid") * @returns {string} the new endpoint */ 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() + 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() } /** @@ -35,40 +35,40 @@ function path(p, mxid, otherParams = {}) { * @returns {Promise} */ function register(username) { - console.log(`[api] register: ${username}`) - return mreq.mreq("POST", "/client/v3/register", { - type: "m.login.application_service", - username - }) + console.log(`[api] register: ${username}`) + return mreq.mreq("POST", "/client/v3/register", { + type: "m.login.application_service", + username + }) } /** * @returns {Promise} room ID */ async function createRoom(content) { - console.log(`[api] create room:`, content) - /** @type {Ty.R.RoomCreated} */ - const root = await mreq.mreq("POST", "/client/v3/createRoom", content) - return root.room_id + console.log(`[api] create room:`, content) + /** @type {Ty.R.RoomCreated} */ + const root = await mreq.mreq("POST", "/client/v3/createRoom", content) + return root.room_id } /** * @returns {Promise} room ID */ async function joinRoom(roomIDOrAlias, mxid) { - /** @type {Ty.R.RoomJoined} */ - const root = await mreq.mreq("POST", path(`/client/v3/join/${roomIDOrAlias}`, mxid)) - return root.room_id + /** @type {Ty.R.RoomJoined} */ + const root = await mreq.mreq("POST", path(`/client/v3/join/${roomIDOrAlias}`, mxid)) + return root.room_id } async function inviteToRoom(roomID, mxidToInvite, mxid) { - await mreq.mreq("POST", path(`/client/v3/rooms/${roomID}/invite`, mxid), { - user_id: mxidToInvite - }) + await mreq.mreq("POST", path(`/client/v3/rooms/${roomID}/invite`, mxid), { + user_id: mxidToInvite + }) } async function leaveRoom(roomID, mxid) { - await mreq.mreq("POST", path(`/client/v3/rooms/${roomID}/leave`, mxid), {}) + await mreq.mreq("POST", path(`/client/v3/rooms/${roomID}/leave`, mxid), {}) } /** @@ -77,9 +77,9 @@ async function leaveRoom(roomID, mxid) { * @template T */ async function getEvent(roomID, eventID) { - /** @type {Ty.Event.Outer} */ - const root = await mreq.mreq("GET", `/client/v3/rooms/${roomID}/event/${eventID}`) - return root + /** @type {Ty.Event.Outer} */ + const root = await mreq.mreq("GET", `/client/v3/rooms/${roomID}/event/${eventID}`) + return root } /** @@ -87,7 +87,17 @@ async function getEvent(roomID, eventID) { * @returns {Promise} */ function getAllState(roomID) { - return mreq.mreq("GET", `/client/v3/rooms/${roomID}/state`) + return mreq.mreq("GET", `/client/v3/rooms/${roomID}/state`) +} + +/** + * @param {string} roomID + * @param {string} type + * @param {string} key + * @returns the *content* of the state event + */ +function getStateEvent(roomID, type, key) { + return mreq.mreq("GET", `/client/v3/rooms/${roomID}/state/${type}/${key}`) } /** @@ -96,7 +106,7 @@ function getAllState(roomID) { * @returns {Promise<{joined: {[mxid: string]: Ty.R.RoomMember}}>} */ function getJoinedMembers(roomID) { - return mreq.mreq("GET", `/client/v3/rooms/${roomID}/joined_members`) + return mreq.mreq("GET", `/client/v3/rooms/${roomID}/joined_members`) } /** @@ -107,12 +117,12 @@ function getJoinedMembers(roomID) { * @returns {Promise} event ID */ async function sendState(roomID, type, stateKey, content, mxid) { - console.log(`[api] state: ${roomID}: ${type}/${stateKey}`) - assert.ok(type) - assert.ok(typeof stateKey === "string") - /** @type {Ty.R.EventSent} */ - const root = await mreq.mreq("PUT", path(`/client/v3/rooms/${roomID}/state/${type}/${stateKey}`, mxid), content) - return root.event_id + console.log(`[api] state: ${roomID}: ${type}/${stateKey}`) + assert.ok(type) + assert.ok(typeof stateKey === "string") + /** @type {Ty.R.EventSent} */ + const root = await mreq.mreq("PUT", path(`/client/v3/rooms/${roomID}/state/${type}/${stateKey}`, mxid), content) + return root.event_id } /** @@ -123,31 +133,51 @@ async function sendState(roomID, type, stateKey, content, 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, {ts: timestamp}), content) - return root.event_id + 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, {ts: timestamp}), content) + return root.event_id } /** * @returns {Promise} room ID */ async function redactEvent(roomID, eventID, mxid) { - /** @type {Ty.R.EventRedacted} */ - const root = await mreq.mreq("PUT", path(`/client/v3/rooms/${roomID}/redact/${eventID}/${makeTxnId.makeTxnId()}`, mxid), {}) - return root.event_id + /** @type {Ty.R.EventRedacted} */ + const root = await mreq.mreq("PUT", path(`/client/v3/rooms/${roomID}/redact/${eventID}/${makeTxnId.makeTxnId()}`, mxid), {}) + return root.event_id } async function profileSetDisplayname(mxid, displayname) { - await mreq.mreq("PUT", path(`/client/v3/profile/${mxid}/displayname`, mxid), { - displayname - }) + await mreq.mreq("PUT", path(`/client/v3/profile/${mxid}/displayname`, mxid), { + displayname + }) } async function profileSetAvatarUrl(mxid, avatar_url) { - await mreq.mreq("PUT", path(`/client/v3/profile/${mxid}/avatar_url`, mxid), { - avatar_url - }) + await mreq.mreq("PUT", path(`/client/v3/profile/${mxid}/avatar_url`, mxid), { + avatar_url + }) +} + +/** + * Set a user's power level within a room. + * @param {string} roomID + * @param {string} mxid + * @param {number} power + */ +async function setUserPower(roomID, mxid, power) { + // Yes it's this hard https://github.com/matrix-org/matrix-appservice-bridge/blob/2334b0bae28a285a767fe7244dad59f5a5963037/src/components/intent.ts#L352 + const powerLevels = await getStateEvent(roomID, "m.room.power_levels", "") + const users = powerLevels.users || {} + if (power != null) { + users[mxid] = power + } else { + delete users[mxid] + } + powerLevels.users = users + await sendState(roomID, "m.room.power_levels", "", powerLevels) + return powerLevels } module.exports.path = path @@ -164,3 +194,4 @@ module.exports.sendEvent = sendEvent module.exports.redactEvent = redactEvent module.exports.profileSetDisplayname = profileSetDisplayname module.exports.profileSetAvatarUrl = profileSetAvatarUrl +module.exports.setUserPower = setUserPower diff --git a/matrix/kstate.test.js b/matrix/kstate.test.js index ed59e9d..1541898 100644 --- a/matrix/kstate.test.js +++ b/matrix/kstate.test.js @@ -2,22 +2,22 @@ const {kstateToState, stateToKState, diffKState, kstateStripConditionals} = requ const {test} = require("supertape") test("kstate strip: strips false conditions", t => { - t.deepEqual(kstateStripConditionals({ - a: {$if: false, value: 2}, - b: {value: 4} - }), { - b: {value: 4} - }) + t.deepEqual(kstateStripConditionals({ + a: {$if: false, value: 2}, + b: {value: 4} + }), { + b: {value: 4} + }) }) test("kstate strip: keeps true conditions while removing $if", t => { - t.deepEqual(kstateStripConditionals({ - a: {$if: true, value: 2}, - b: {value: 4} - }), { - a: {value: 2}, - b: {value: 4} - }) + t.deepEqual(kstateStripConditionals({ + a: {$if: true, value: 2}, + b: {value: 4} + }), { + a: {value: 2}, + b: {value: 4} + }) }) test("kstate2state: general", t => { diff --git a/matrix/read-registration.test.js b/matrix/read-registration.test.js index d402cfb..e5123b9 100644 --- a/matrix/read-registration.test.js +++ b/matrix/read-registration.test.js @@ -2,9 +2,9 @@ const {test} = require("supertape") const reg = require("./read-registration") test("reg: has necessary parameters", t => { - const propertiesToCheck = ["sender_localpart", "id", "as_token", "ooye"] - t.deepEqual( - propertiesToCheck.filter(p => p in reg), - propertiesToCheck - ) + const propertiesToCheck = ["sender_localpart", "id", "as_token", "ooye"] + t.deepEqual( + propertiesToCheck.filter(p => p in reg), + propertiesToCheck + ) }) diff --git a/scripts/save-channel-names-to-db.js b/scripts/save-channel-names-to-db.js index a70b1bb..6f5867a 100644 --- a/scripts/save-channel-names-to-db.js +++ b/scripts/save-channel-names-to-db.js @@ -18,10 +18,10 @@ passthrough.discord = discord ;(async () => { await discord.cloud.connect() - console.log("Discord gateway started") + console.log("Discord gateway started") - const f = event => onPacket(discord, event, () => discord.cloud.off("event", f)) - discord.cloud.on("event", f) + const f = event => onPacket(discord, event, () => discord.cloud.off("event", f)) + discord.cloud.on("event", f) })() const expectedGuilds = new Set() @@ -30,29 +30,29 @@ const prepared = db.prepare("UPDATE channel_room SET name = ? WHERE channel_id = /** @param {DiscordClient} discord */ function onPacket(discord, event, unsubscribe) { - if (event.t === "READY") { - for (const obj of event.d.guilds) { - expectedGuilds.add(obj.id) - } + if (event.t === "READY") { + for (const obj of event.d.guilds) { + expectedGuilds.add(obj.id) + } - } else if (event.t === "GUILD_CREATE") { - expectedGuilds.delete(event.d.id) + } else if (event.t === "GUILD_CREATE") { + expectedGuilds.delete(event.d.id) - // Store the channel. - for (const channel of event.d.channels || []) { - prepared.run(channel.name, channel.id) - } + // Store the channel. + for (const channel of event.d.channels || []) { + prepared.run(channel.name, channel.id) + } - // Checked them all? - if (expectedGuilds.size === 0) { - discord.cloud.disconnect() - unsubscribe() + // Checked them all? + if (expectedGuilds.size === 0) { + discord.cloud.disconnect() + unsubscribe() - // I don't know why node keeps running. - setTimeout(() => { - console.log("Stopping now.") - process.exit() - }, 1500).unref() - } - } + // I don't know why node keeps running. + setTimeout(() => { + console.log("Stopping now.") + process.exit() + }, 1500).unref() + } + } } diff --git a/scripts/save-event-types-to-db.js b/scripts/save-event-types-to-db.js index 83f5d2b..547e85c 100644 --- a/scripts/save-event-types-to-db.js +++ b/scripts/save-event-types-to-db.js @@ -18,13 +18,13 @@ const rows = db.prepare("SELECT event_id, room_id, event_type FROM event_message 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}`) - } - } + 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}`) + } + } })() From 180708b60e78a07ff366aa25d69ba5345ed33401 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 21 Aug 2023 21:16:10 +1200 Subject: [PATCH 67/99] I WAS USING THE WRONG VARIABLE :GRIEF: --- d2m/actions/send-message.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index a5c8dac..2dcc324 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -38,7 +38,7 @@ async function sendMessage(message, guild) { const eventWithoutType = {...event} delete eventWithoutType.$type - const eventID = await api.sendEvent(roomID, eventType, event, senderMxid, new Date(message.timestamp).getTime()) + const eventID = await api.sendEvent(roomID, eventType, eventWithoutType, senderMxid, new Date(message.timestamp).getTime()) 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 From 6d1635539b4edfd31bf2384c944b123d9e9f5031 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 21 Aug 2023 23:31:40 +1200 Subject: [PATCH 68/99] finally got the thread's early messages working --- d2m/actions/announce-thread.js | 26 +-- d2m/actions/create-room.js | 11 +- d2m/converters/message-to-event.js | 23 +++ d2m/converters/message-to-event.test.js | 22 +++ d2m/converters/thread-to-announcement.js | 46 ++++++ d2m/converters/thread-to-announcement.test.js | 150 ++++++++++++++++++ d2m/discord-packets.js | 6 +- d2m/event-dispatcher.js | 5 +- db/data-for-test.sql | 6 +- test/data.js | 57 +++++++ test/test.js | 1 + 11 files changed, 321 insertions(+), 32 deletions(-) create mode 100644 d2m/converters/thread-to-announcement.js create mode 100644 d2m/converters/thread-to-announcement.test.js diff --git a/d2m/actions/announce-thread.js b/d2m/actions/announce-thread.js index 546d307..aa6def2 100644 --- a/d2m/actions/announce-thread.js +++ b/d2m/actions/announce-thread.js @@ -4,14 +4,10 @@ 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("../converters/thread-to-announcement")} */ +const threadToAnnouncement = sync.require("../converters/thread-to-announcement") /** @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 {string} parentRoomID @@ -21,24 +17,10 @@ const createRoom = sync.require("../actions/create-room") async function announceThread(parentRoomID, threadRoomID, thread) { /** @type {string?} */ const creatorMxid = db.prepare("SELECT mxid FROM sim WHERE discord_id = ?").pluck().get(thread.owner_id) - /** @type {string?} */ - const branchedFromEventID = db.prepare("SELECT event_id FROM event_message WHERE message_id = ?").get(thread.id) - const msgtype = creatorMxid ? "m.emote" : "m.text" - const template = creatorMxid ? "started a thread:" : "Thread started:" - let body = `${template} ${thread.name} https://matrix.to/#/${threadRoomID}` - let html = `${template} ${thread.name}` + const content = await threadToAnnouncement.threadToAnnouncement(parentRoomID, threadRoomID, creatorMxid, thread, {api}) - const mentions = {} - - await api.sendEvent(parentRoomID, "m.room.message", { - msgtype, - body: `${template} , - format: "org.matrix.custom.html", - formatted_body: "", - "m.mentions": mentions - - }, creatorMxid) + await api.sendEvent(parentRoomID, "m.room.message", content, creatorMxid) } module.exports.announceThread = announceThread diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 2ee0913..4576320 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -70,12 +70,15 @@ async function channelToKState(channel, guild) { avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) // TODO: somehow represent future values in kstate (callbacks?), while still allowing for diffing, so test cases don't need to touch the media API } + let history_visibility = "invited" + if (channel["thread_metadata"]) history_visibility = "world_readable" + const channelKState = { "m.room.name/": {name: convertedName}, "m.room.topic/": {topic: convertedTopic}, "m.room.avatar/": avatarEventContent, "m.room.guest_access/": {guest_access: "can_join"}, - "m.room.history_visibility/": {history_visibility: "invited"}, + "m.room.history_visibility/": {history_visibility}, [`m.space.parent/${spaceID}`]: { via: ["cadence.moe"], // TODO: put the proper server here canonical: true @@ -234,19 +237,23 @@ async function _unbridgeRoom(channelID) { * @returns {Promise} */ async function _syncSpaceMember(channel, spaceID, roomID) { + console.error(channel) + console.error("syncing space for", roomID) const spaceKState = await roomToKState(spaceID) let spaceEventContent = {} if ( channel.type !== DiscordTypes.ChannelType.PrivateThread // private threads do not belong in the space (don't offer people something they can't join) - || channel["thread_metadata"]?.archived // archived threads do not belong in the space (don't offer people conversations that are no longer relevant) + && !channel["thread_metadata"]?.archived // archived threads do not belong in the space (don't offer people conversations that are no longer relevant) ) { spaceEventContent = { via: ["cadence.moe"] // TODO: use the proper server } } + console.error(spaceEventContent) const spaceDiff = ks.diffKState(spaceKState, { [`m.space.child/${roomID}`]: spaceEventContent }) + console.error(spaceDiff) return applyKStateDiffToRoom(spaceID, spaceDiff) } diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index c34b389..c5da78d 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -65,6 +65,29 @@ function getDiscordParseCallbacks(message, useHTML) { async function messageToEvent(message, guild, options = {}, di) { const events = [] + if (message.type === DiscordTypes.MessageType.ThreadCreated) { + // This is the kind of message that appears when somebody makes a thread which isn't close enough to the message it's based off. + // It lacks the lines and the pill, so it looks kind of like a member join message, and it says: + // [#] NICKNAME started a thread: __THREAD NAME__. __See all threads__ + // We're already bridging the THREAD_CREATED gateway event to make a comparable message, so drop this one. + return [] + } + + if (message.type === DiscordTypes.MessageType.ThreadStarterMessage) { + // This is the message that appears at the top of a thread when the thread was based off an existing message. + // It's just a message reference, no content. + const ref = message.message_reference + assert(ref) + assert(ref.message_id) + const row = db.prepare("SELECT room_id, event_id FROM event_message INNER JOIN channel_room USING (channel_id) WHERE channel_id = ? AND message_id = ?").get(ref.channel_id, ref.message_id) + if (!row) return [] + const event = await di.api.getEvent(row.room_id, row.event_id) + return [{ + ...event.content, + $type: event.type + }] + } + /** @type {{room?: boolean, user_ids?: string[]}} We should consider the following scenarios for mentions: diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index 260ecda..062ee0b 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -341,3 +341,25 @@ test("message2event: type 4 channel name change", async t => { formatted_body: "changed the channel name to worming" }]) }) + +test("message2event: thread start message reference", async t => { + const events = await messageToEvent(data.special_message.thread_start_context, data.guild.general, {}, { + api: { + getEvent: mockGetEvent(t, "!PnyBKvUBOhjuCucEfk:cadence.moe", "$FchUVylsOfmmbj-VwEs5Z9kY49_dt2zd0vWfylzy5Yo", { + "type": "m.room.message", + "sender": "@_ooye_cadence:cadence.moe", + "content": { + "m.mentions": {}, + "msgtype": "m.text", + "body": "layer 4" + } + }) + } + }) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "layer 4", + "m.mentions": {} + }]) +}) diff --git a/d2m/converters/thread-to-announcement.js b/d2m/converters/thread-to-announcement.js new file mode 100644 index 0000000..405f7e9 --- /dev/null +++ b/d2m/converters/thread-to-announcement.js @@ -0,0 +1,46 @@ +// @ts-check + +const assert = require("assert") + +const passthrough = require("../../passthrough") +const { discord, sync, db } = passthrough +/** @type {import("../../matrix/read-registration")} */ +const reg = sync.require("../../matrix/read-registration.js") + +const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex)) + +/** + * @param {string} parentRoomID + * @param {string} threadRoomID + * @param {string?} creatorMxid + * @param {import("discord-api-types/v10").APIThreadChannel} thread + * @param {{api: import("../../matrix/api")}} di simple-as-nails dependency injection for the matrix API + */ +async function threadToAnnouncement(parentRoomID, threadRoomID, creatorMxid, thread, di) { + /** @type {string?} */ + const branchedFromEventID = db.prepare("SELECT event_id FROM event_message WHERE message_id = ?").pluck().get(thread.id) + /** @type {{"m.mentions"?: any, "m.in_reply_to"?: any}} */ + const context = {} + if (branchedFromEventID) { + // Need to figure out who sent that event... + const event = await di.api.getEvent(parentRoomID, branchedFromEventID) + context["m.relates_to"] = {"m.in_reply_to": {event_id: event.event_id}} + if (event.sender && !userRegex.some(rx => event.sender.match(rx))) context["m.mentions"] = {user_ids: [event.sender]} + } + + const msgtype = creatorMxid ? "m.emote" : "m.text" + const template = creatorMxid ? "started a thread:" : "Thread started:" + let body = `${template} ${thread.name} https://matrix.to/#/${threadRoomID}` + let html = `${template} ${thread.name}` + + return { + msgtype, + body, + format: "org.matrix.custom.html", + formatted_body: html, + "m.mentions": {}, + ...context + } +} + +module.exports.threadToAnnouncement = threadToAnnouncement diff --git a/d2m/converters/thread-to-announcement.test.js b/d2m/converters/thread-to-announcement.test.js new file mode 100644 index 0000000..06d937f --- /dev/null +++ b/d2m/converters/thread-to-announcement.test.js @@ -0,0 +1,150 @@ +const {test} = require("supertape") +const {threadToAnnouncement} = require("./thread-to-announcement") +const data = require("../../test/data") +const Ty = require("../../types") + +/** + * @param {string} roomID + * @param {string} eventID + * @returns {(roomID: string, eventID: string) => Promise>} + */ +function mockGetEvent(t, roomID_in, eventID_in, outer) { + return async function(roomID, eventID) { + t.equal(roomID, roomID_in) + t.equal(eventID, eventID_in) + return new Promise(resolve => { + setTimeout(() => { + resolve({ + event_id: eventID_in, + room_id: roomID_in, + origin_server_ts: 1680000000000, + unsigned: { + age: 2245, + transaction_id: "$local.whatever" + }, + ...outer + }) + }) + }) + } +} + +test("thread2announcement: no known creator, no branched from event", async t => { + const content = await threadToAnnouncement("!parent", "!thread", null, { + name: "test thread", + id: "-1" + }) + t.deepEqual(content, { + msgtype: "m.text", + body: "Thread started: test thread https://matrix.to/#/!thread", + format: "org.matrix.custom.html", + formatted_body: `Thread started: test thread`, + "m.mentions": {} + }) +}) + +test("thread2announcement: known creator, no branched from event", async t => { + const content = await threadToAnnouncement("!parent", "!thread", "@_ooye_crunch_god:cadence.moe", { + name: "test thread", + id: "-1" + }) + t.deepEqual(content, { + msgtype: "m.emote", + body: "started a thread: test thread https://matrix.to/#/!thread", + format: "org.matrix.custom.html", + formatted_body: `started a thread: test thread`, + "m.mentions": {} + }) +}) + +test("thread2announcement: no known creator, branched from discord event", async t => { + const content = await threadToAnnouncement("!kLRqKKUQXcibIMtOpl:cadence.moe", "!thread", null, { + name: "test thread", + id: "1126786462646550579" + }, { + api: { + getEvent: mockGetEvent(t, "!kLRqKKUQXcibIMtOpl:cadence.moe", "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg", { + type: 'm.room.message', + sender: '@_ooye_bot:cadence.moe', + content: { + msgtype: 'm.text', + body: 'testing testing testing' + } + }) + } + }) + t.deepEqual(content, { + msgtype: "m.text", + body: "Thread started: test thread https://matrix.to/#/!thread", + format: "org.matrix.custom.html", + formatted_body: `Thread started: test thread`, + "m.mentions": {}, + "m.relates_to": { + "m.in_reply_to": { + event_id: "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg" + } + } + }) +}) + +test("thread2announcement: known creator, branched from discord event", async t => { + const content = await threadToAnnouncement("!kLRqKKUQXcibIMtOpl:cadence.moe", "!thread", "@_ooye_crunch_god:cadence.moe", { + name: "test thread", + id: "1126786462646550579" + }, { + api: { + getEvent: mockGetEvent(t, "!kLRqKKUQXcibIMtOpl:cadence.moe", "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg", { + type: 'm.room.message', + sender: '@_ooye_bot:cadence.moe', + content: { + msgtype: 'm.text', + body: 'testing testing testing' + } + }) + } + }) + t.deepEqual(content, { + msgtype: "m.emote", + body: "started a thread: test thread https://matrix.to/#/!thread", + format: "org.matrix.custom.html", + formatted_body: `started a thread: test thread`, + "m.mentions": {}, + "m.relates_to": { + "m.in_reply_to": { + event_id: "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg" + } + } + }) +}) + +test("thread2announcement: no known creator, branched from matrix event", async t => { + const content = await threadToAnnouncement("!kLRqKKUQXcibIMtOpl:cadence.moe", "!thread", null, { + name: "test thread", + id: "1128118177155526666" + }, { + api: { + getEvent: mockGetEvent(t, "!kLRqKKUQXcibIMtOpl:cadence.moe", "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4", { + type: "m.room.message", + content: { + msgtype: "m.text", + body: "so can you reply to my webhook uwu" + }, + sender: "@cadence:cadence.moe" + }) + } + }) + t.deepEqual(content, { + msgtype: "m.text", + body: "Thread started: test thread https://matrix.to/#/!thread", + format: "org.matrix.custom.html", + formatted_body: `Thread started: test thread`, + "m.mentions": { + user_ids: ["@cadence:cadence.moe"] + }, + "m.relates_to": { + "m.in_reply_to": { + event_id: "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4" + } + } + }) +}) diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index 776d4b1..a1a4505 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -86,18 +86,16 @@ const utils = { await eventDispatcher.onChannelOrThreadUpdate(client, message.d, false) } else if (message.t === "THREAD_CREATE") { - console.log(message) - // await eventDispatcher.onThreadCreate(client, message.d) + // @ts-ignore + await eventDispatcher.onThreadCreate(client, message.d) } else if (message.t === "THREAD_UPDATE") { await eventDispatcher.onChannelOrThreadUpdate(client, message.d, true) } else if (message.t === "MESSAGE_CREATE") { - console.log(message) await eventDispatcher.onMessageCreate(client, message.d) } else if (message.t === "MESSAGE_UPDATE") { - console.log(message) await eventDispatcher.onMessageUpdate(client, message.d) } else if (message.t === "MESSAGE_DELETE") { diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 6e4ffc4..c871ff1 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/announce-thread")}) */ +const announceThread = sync.require("./actions/announce-thread") /** @type {import("./actions/create-room")}) */ const createRoom = sync.require("./actions/create-room") /** @type {import("../matrix/api")}) */ @@ -108,8 +110,7 @@ module.exports = { * @param {import("discord-api-types/v10").APIThreadChannel} thread */ async onThreadCreate(client, thread) { - console.log(thread) - const parentRoomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").get(thread.parent_id) + const parentRoomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(thread.parent_id) if (!parentRoomID) return // Not interested in a thread if we aren't interested in its wider channel const threadRoomID = await createRoom.syncRoom(thread.id) // Create room (will share the same inflight as the initial message to the thread) await announceThread.announceThread(parentRoomID, threadRoomID, thread) diff --git a/db/data-for-test.sql b/db/data-for-test.sql index ee31fe3..4a406c9 100644 --- a/db/data-for-test.sql +++ b/db/data-for-test.sql @@ -58,7 +58,8 @@ INSERT INTO guild_space (guild_id, space_id) VALUES INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent) VALUES ('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main', NULL), ('497161350934560778', '!edUxjVdzgUvXDUIQCK:cadence.moe', 'amanda-spam', NULL, NULL), -('160197704226439168', '!uCtjHhfGlYbVnPVlkG:cadence.moe', 'the-stanley-parable-channel', 'bots', NULL); +('160197704226439168', '!uCtjHhfGlYbVnPVlkG:cadence.moe', 'the-stanley-parable-channel', 'bots', NULL), +('1100319550446252084', '!PnyBKvUBOhjuCucEfk:cadence.moe', 'worm-farm', NULL, NULL); INSERT INTO sim (discord_id, sim_name, localpart, mxid) VALUES ('0', 'bot', '_ooye_bot', '@_ooye_bot:cadence.moe'), @@ -80,7 +81,8 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, chan ('$51f4yqHinwnSbPEQ9dCgoyy4qiIJSX0QYYVUnvwyTCJ', 'm.room.message', 'm.image', '1141501302736695317', '112760669178241024', 0, 1), ('$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M', 'm.room.message', 'm.text', '1128084851279536279', '112760669178241024', 0, 1), ('$YUJFa5j0ZJe7PUvD2DykRt9g51RoadUEYmuJLdSEbJ0', 'm.room.message', 'm.image', '1128084851279536279', '112760669178241024', 1, 1), -('$oLyUTyZ_7e_SUzGNWZKz880ll9amLZvXGbArJCKai2Q', 'm.room.message', 'm.text', '1128084748338741392', '112760669178241024', 0, 1); +('$oLyUTyZ_7e_SUzGNWZKz880ll9amLZvXGbArJCKai2Q', 'm.room.message', 'm.text', '1128084748338741392', '112760669178241024', 0, 1), +('$FchUVylsOfmmbj-VwEs5Z9kY49_dt2zd0vWfylzy5Yo', 'm.room.message', 'm.text', '1143121514925928541', '1100319550446252084', 0, 1); INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'), diff --git a/test/data.js b/test/data.js index fc8cbbd..30d108a 100644 --- a/test/data.js +++ b/test/data.js @@ -1375,6 +1375,63 @@ module.exports = { flags: 0, components: [], position: 12 + }, + updated_to_start_thread_from_here: { + t: "MESSAGE_UPDATE", + s: 19, + op: 0, + d: { + id: "1143121514925928541", + flags: 32, + channel_id: "1100319550446252084", + guild_id: "1100319549670301727" + }, + shard_id: 0 + }, + thread_start_context: { + type: 21, + tts: false, + timestamp: "2023-08-21T09:57:12.558000+00:00", + position: 0, + pinned: false, + message_reference: { + message_id: "1143121514925928541", + guild_id: "1100319549670301727", + channel_id: "1100319550446252084" + }, + mentions: [], + mention_roles: [], + mention_everyone: false, + member: { + roles: [], + premium_since: null, + pending: false, + nick: "worm", + mute: false, + joined_at: "2023-04-25T07:17:03.696000+00:00", + flags: 0, + deaf: false, + communication_disabled_until: null, + avatar: null + }, + id: "1143121620744032327", + flags: 0, + embeds: [], + edited_timestamp: null, + content: "", + components: [], + channel_id: "1143121514925928541", + author: { + username: "cadence.worm", + public_flags: 0, + id: "772659086046658620", + global_name: "cadence", + discriminator: "0", + avatar_decoration_data: null, + avatar: "4b5c4b28051144e4c111f0113a0f1cf1" + }, + attachments: [], + guild_id: "1100319549670301727" } } } diff --git a/test/test.js b/test/test.js index 03394f0..606bd4b 100644 --- a/test/test.js +++ b/test/test.js @@ -23,6 +23,7 @@ require("../matrix/read-registration.test") require("../d2m/converters/message-to-event.test") require("../d2m/converters/message-to-event.embeds.test") require("../d2m/converters/edit-to-changes.test") +require("../d2m/converters/thread-to-announcement.test") require("../d2m/actions/create-room.test") require("../d2m/converters/user-to-mxid.test") require("../d2m/actions/register-user.test") From e04617eb04cdfce76d1bd544c0414b29c271d7d1 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 21 Aug 2023 23:34:49 +1200 Subject: [PATCH 69/99] remove console.log --- d2m/actions/create-room.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 4576320..3be2429 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -237,8 +237,6 @@ async function _unbridgeRoom(channelID) { * @returns {Promise} */ async function _syncSpaceMember(channel, spaceID, roomID) { - console.error(channel) - console.error("syncing space for", roomID) const spaceKState = await roomToKState(spaceID) let spaceEventContent = {} if ( @@ -249,11 +247,9 @@ async function _syncSpaceMember(channel, spaceID, roomID) { via: ["cadence.moe"] // TODO: use the proper server } } - console.error(spaceEventContent) const spaceDiff = ks.diffKState(spaceKState, { [`m.space.child/${roomID}`]: spaceEventContent }) - console.error(spaceDiff) return applyKStateDiffToRoom(spaceID, spaceDiff) } From 6cd509d27420f9188bf02c9609b5a4118298ba70 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Tue, 22 Aug 2023 17:11:07 +1200 Subject: [PATCH 70/99] start using kstate for guild/spaces too --- d2m/actions/create-space.js | 76 +++++++++++++++++++++++++++---------- 1 file changed, 57 insertions(+), 19 deletions(-) diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index 02c2dcf..1f00312 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -1,18 +1,29 @@ // @ts-check const assert = require("assert") +const DiscordTypes = require("discord-api-types/v10") + const passthrough = require("../../passthrough") const { sync, db } = passthrough /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") +/** @type {import("../../matrix/file")} */ +const file = sync.require("../../matrix/file") +/** @type {import("./create-room")} */ +const createRoom = sync.require("./create-room") /** * @param {import("discord-api-types/v10").RESTGetAPIGuildResult} guild + * @param {any} kstate */ -async function createSpace(guild) { - assert(guild.name) +async function createSpace(guild, kstate) { + const name = kstate["m.room.name/"].name + const topic = kstate["m.room.topic/"]?.topic || undefined + + assert(name) + const roomID = await api.createRoom({ - name: guild.name, + name, preset: "private_chat", // cannot join space unless invited visibility: "private", power_level_content_override: { @@ -20,28 +31,55 @@ async function createSpace(guild) { invite: 0 // any existing member can invite others }, invite: ["@cadence:cadence.moe"], // TODO - topic: guild.description || undefined, + topic, creation_content: { type: "m.space" }, - initial_state: [ - { - type: "m.room.guest_access", - state_key: "", - content: { - guest_access: "can_join" // guests can join space if other conditions are met - } - }, - { - type: "m.room.history_visibility", - content: { - history_visibility: "invited" // any events sent after user was invited are visible - } - } - ] + initial_state: ks.kstateToState(kstate) }) db.prepare("INSERT INTO guild_space (guild_id, space_id) VALUES (?, ?)").run(guild.id, roomID) return roomID } +/** + * @param {DiscordTypes.APIGuild} guild] + */ +async function guildToKState(guild) { + const avatarEventContent = {} + if (guild.icon) { + avatarEventContent.discord_path = file.guildIcon(guild) + avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) // TODO: somehow represent future values in kstate (callbacks?), while still allowing for diffing, so test cases don't need to touch the media API + } + + let history_visibility = "invited" + if (guild["thread_metadata"]) history_visibility = "world_readable" + + const guildKState = { + "m.room.name/": {name: guild.name}, + "m.room.avatar/": avatarEventContent, + "m.room.guest_access/": {guest_access: "can_join"}, // guests can join space if other conditions are met + "m.room.history_visibility": {history_visibility: "invited"} // any events sent after user was invited are visible + } + + return guildKState +} + +async function syncSpace(guildID) { + /** @ts-ignore @type {DiscordTypes.APIGuild} */ + const guild = discord.guilds.get(guildID) + assert.ok(guild) + + /** @type {{room_id: string, thread_parent: string?}} */ + const existing = db.prepare("SELECT space_id from guild_space WHERE guild_id = ?").get(guildID) + + const guildKState = await guildToKState(guild) + + if (!existing) { + const spaceID = await createSpace(guild, guildKState) + return spaceID + } + + + + module.exports.createSpace = createSpace From 892bf4496d93f85e26c08732479a8fdae99d08ce Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 23 Aug 2023 12:31:31 +1200 Subject: [PATCH 71/99] be able to sync space properties --- d2m/actions/create-space.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index 1f00312..34aa25f 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -4,13 +4,15 @@ const assert = require("assert") const DiscordTypes = require("discord-api-types/v10") const passthrough = require("../../passthrough") -const { sync, db } = passthrough +const { discord, sync, db } = passthrough /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") /** @type {import("../../matrix/file")} */ const file = sync.require("../../matrix/file") /** @type {import("./create-room")} */ const createRoom = sync.require("./create-room") +/** @type {import("../../matrix/kstate")} */ +const ks = sync.require("../../matrix/kstate") /** * @param {import("discord-api-types/v10").RESTGetAPIGuildResult} guild @@ -58,7 +60,7 @@ async function guildToKState(guild) { "m.room.name/": {name: guild.name}, "m.room.avatar/": avatarEventContent, "m.room.guest_access/": {guest_access: "can_join"}, // guests can join space if other conditions are met - "m.room.history_visibility": {history_visibility: "invited"} // any events sent after user was invited are visible + "m.room.history_visibility/": {history_visibility: "invited"} // any events sent after user was invited are visible } return guildKState @@ -69,17 +71,26 @@ async function syncSpace(guildID) { const guild = discord.guilds.get(guildID) assert.ok(guild) - /** @type {{room_id: string, thread_parent: string?}} */ - const existing = db.prepare("SELECT space_id from guild_space WHERE guild_id = ?").get(guildID) + /** @type {string?} */ + const spaceID = db.prepare("SELECT space_id from guild_space WHERE guild_id = ?").pluck().get(guildID) const guildKState = await guildToKState(guild) - if (!existing) { + if (!spaceID) { const spaceID = await createSpace(guild, guildKState) - return spaceID + return spaceID // Naturally, the newly created space is already up to date, so we can always skip syncing here. } + console.log(`[space sync] to matrix: ${guild.name}`) + // sync channel state to room + const spaceKState = await createRoom.roomToKState(spaceID) + const spaceDiff = ks.diffKState(spaceKState, guildKState) + await createRoom.applyKStateDiffToRoom(spaceID, spaceDiff) + return spaceID +} module.exports.createSpace = createSpace +module.exports.syncSpace = syncSpace +module.exports.guildToKState = guildToKState From bfe9efe62eaccf43cb238b0e5fd79b9fe1674f59 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 23 Aug 2023 12:37:25 +1200 Subject: [PATCH 72/99] listen for guild updates and connect them --- d2m/discord-packets.js | 5 ++++- d2m/event-dispatcher.js | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index a1a4505..6ece8ca 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -82,7 +82,10 @@ const utils = { // Event dispatcher for OOYE bridge operations try { - if (message.t === "CHANNEL_UPDATE") { + if (message.t === "GUILD_UPDATE") { + await eventDispatcher.onGuildUpdate(client, message.d) + + } else if (message.t === "CHANNEL_UPDATE") { await eventDispatcher.onChannelOrThreadUpdate(client, message.d, false) } else if (message.t === "THREAD_CREATE") { diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index c871ff1..9bb07c0 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -14,6 +14,8 @@ const addReaction = sync.require("./actions/add-reaction") const announceThread = sync.require("./actions/announce-thread") /** @type {import("./actions/create-room")}) */ const createRoom = sync.require("./actions/create-room") +/** @type {import("./actions/create-space")}) */ +const createSpace = sync.require("./actions/create-space") /** @type {import("../matrix/api")}) */ const api = sync.require("../matrix/api") @@ -116,6 +118,16 @@ module.exports = { await announceThread.announceThread(parentRoomID, threadRoomID, thread) }, + /** + * @param {import("./discord-client")} client + * @param {import("discord-api-types/v10").GatewayGuildUpdateDispatchData} guild + */ + async onGuildUpdate(client, guild) { + const spaceID = db.prepare("SELECT space_id FROM guild_space WHERE guild_id = ?").pluck().get(guild.id) + if (!spaceID) return + await createSpace.syncSpace(guild.id) + }, + /** * @param {import("./discord-client")} client * @param {import("discord-api-types/v10").GatewayChannelUpdateDispatchData} channelOrThread From 92cd628a6c845dfd16d0eb4e0e8611020759b49a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 23 Aug 2023 12:39:37 +1200 Subject: [PATCH 73/99] remember any room avatars set on matrix-side --- d2m/actions/create-room.js | 22 ++++++++++++++++------ m2d/event-dispatcher.js | 13 ++++++++++++- matrix/api.js | 2 ++ matrix/kstate.js | 1 + types.d.ts | 9 +++++++++ 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 3be2429..166d52f 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -61,11 +61,16 @@ async function channelToKState(channel, guild) { const spaceID = db.prepare("SELECT space_id FROM guild_space WHERE guild_id = ?").pluck().get(guild.id) assert.ok(typeof spaceID === "string") - const customName = db.prepare("SELECT nick FROM channel_room WHERE channel_id = ?").pluck().get(channel.id) + const row = db.prepare("SELECT nick, custom_avatar FROM channel_room WHERE channel_id = ?").get(channel.id) + assert(row) + const customName = row.nick + const customAvatar = row.custom_avatar const [convertedName, convertedTopic] = convertNameAndTopic(channel, guild, customName) const avatarEventContent = {} - if (guild.icon) { + if (customAvatar) { + avatarEventContent.url = customAvatar + } else if (guild.icon) { avatarEventContent.discord_path = file.guildIcon(guild) avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) // TODO: somehow represent future values in kstate (callbacks?), while still allowing for diffing, so test cases don't need to touch the media API } @@ -183,6 +188,8 @@ async function _syncRoom(channelID, shouldActuallySync) { return creation // Naturally, the newly created room is already up to date, so we can always skip syncing here. } + const roomID = existing.room_id + if (!shouldActuallySync) { return existing.room_id // only need to ensure room exists, and it does. return the room ID } @@ -192,15 +199,16 @@ async function _syncRoom(channelID, shouldActuallySync) { const {spaceID, channelKState} = await channelToKState(channel, guild) // sync channel state to room - const roomKState = await roomToKState(existing.room_id) + const roomKState = await roomToKState(roomID) const roomDiff = ks.diffKState(roomKState, channelKState) - const roomApply = applyKStateDiffToRoom(existing.room_id, roomDiff) + const roomApply = applyKStateDiffToRoom(roomID, roomDiff) + db.prepare("UPDATE channel_room SET name = ? WHERE room_id = ?").run(channel.name, roomID) // sync room as space member - const spaceApply = _syncSpaceMember(channel, spaceID, existing.room_id) + const spaceApply = _syncSpaceMember(channel, spaceID, roomID) await Promise.all([roomApply, spaceApply]) - return existing.room_id + return roomID } async function _unbridgeRoom(channelID) { @@ -279,5 +287,7 @@ module.exports.ensureRoom = ensureRoom module.exports.syncRoom = syncRoom module.exports.createAllForGuild = createAllForGuild module.exports.channelToKState = channelToKState +module.exports.roomToKState = roomToKState +module.exports.applyKStateDiffToRoom = applyKStateDiffToRoom module.exports._convertNameAndTopic = convertNameAndTopic module.exports._unbridgeRoom = _unbridgeRoom diff --git a/m2d/event-dispatcher.js b/m2d/event-dispatcher.js index 44eba85..c62d805 100644 --- a/m2d/event-dispatcher.js +++ b/m2d/event-dispatcher.js @@ -6,7 +6,7 @@ const util = require("util") const Ty = require("../types") -const {sync, as} = require("../passthrough") +const {db, sync, as} = require("../passthrough") /** @type {import("./actions/send-event")} */ const sendEvent = sync.require("./actions/send-event") @@ -69,3 +69,14 @@ async event => { if (utils.eventSenderIsFromDiscord(event.sender)) return await addReaction.addReaction(event) })) + +sync.addTemporaryListener(as, "type:m.room.avatar", guard("m.room.avatar", +/** + * @param {Ty.Event.StateOuter} event + */ +async event => { + if (event.state_key !== "") return + if (utils.eventSenderIsFromDiscord(event.sender)) return + const url = event.content.url || null + db.prepare("UPDATE channel_room SET custom_avatar = ? WHERE room_id = ?").run(url, event.room_id) +})) diff --git a/matrix/api.js b/matrix/api.js index b382631..9eff6c7 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -167,6 +167,8 @@ async function profileSetAvatarUrl(mxid, avatar_url) { * @param {number} power */ async function setUserPower(roomID, mxid, power) { + assert(roomID[0] === "!") + assert(mxid[0] === "@") // Yes it's this hard https://github.com/matrix-org/matrix-appservice-bridge/blob/2334b0bae28a285a767fe7244dad59f5a5963037/src/components/intent.ts#L352 const powerLevels = await getStateEvent(roomID, "m.room.power_levels", "") const users = powerLevels.users || {} diff --git a/matrix/kstate.js b/matrix/kstate.js index 398b1b6..1b2ca14 100644 --- a/matrix/kstate.js +++ b/matrix/kstate.js @@ -42,6 +42,7 @@ function diffKState(actual, target) { const diff = {} // go through each key that it should have for (const key of Object.keys(target)) { + if (!key.includes("/")) throw new Error(`target kstate's key "${key}" does not contain a slash separator; if a blank state_key was intended, add a trailing slash to the kstate key.`) if (key in actual) { // diff try { diff --git a/types.d.ts b/types.d.ts index b9f7ed6..faf4c70 100644 --- a/types.d.ts +++ b/types.d.ts @@ -38,6 +38,10 @@ namespace Event { event_id: string } + export type StateOuter = Outer & { + state_key: string + } + export type ReplacementContent = T & { "m.new_content": T "m.relates_to": { @@ -74,6 +78,11 @@ namespace Event { avatar_url?: string } + export type M_Room_Avatar = { + discord_path?: string + url?: string + } + export type M_Reaction = { "m.relates_to": { rel_type: "m.annotation" From 458a620f4a160f1ca9dfccf220097239d8084963 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 23 Aug 2023 12:45:19 +1200 Subject: [PATCH 74/99] make hardcoded "cadence.moe" configurable --- d2m/actions/create-room.js | 5 +++-- d2m/actions/register-user.js | 2 +- m2d/converters/utils.js | 1 + seed.js | 4 ++-- types.d.ts | 5 +++-- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 166d52f..c8f0661 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -2,6 +2,7 @@ const assert = require("assert").strict const DiscordTypes = require("discord-api-types/v10") +const reg = require("../../matrix/read-registration") const passthrough = require("../../passthrough") const { discord, sync, db } = passthrough @@ -85,7 +86,7 @@ async function channelToKState(channel, guild) { "m.room.guest_access/": {guest_access: "can_join"}, "m.room.history_visibility/": {history_visibility}, [`m.space.parent/${spaceID}`]: { - via: ["cadence.moe"], // TODO: put the proper server here + via: [reg.ooye.server_name], canonical: true }, "m.room.join_rules/": { @@ -252,7 +253,7 @@ async function _syncSpaceMember(channel, spaceID, roomID) { && !channel["thread_metadata"]?.archived // archived threads do not belong in the space (don't offer people conversations that are no longer relevant) ) { spaceEventContent = { - via: ["cadence.moe"] // TODO: use the proper server + via: [reg.ooye.server_name] } } const spaceDiff = ks.diffKState(spaceKState, { diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index a33cecc..00a985c 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -21,7 +21,7 @@ async function createSim(user) { // Choose sim name const simName = userToMxid.userToSimName(user) const localpart = reg.ooye.namespace_prefix + simName - const mxid = "@" + localpart + ":cadence.moe" + const mxid = `@${localpart}:${reg.ooye.server_name}` // Save chosen name in the database forever // Making this database change right away so that in a concurrent registration, the 2nd registration will already have generated a different localpart because it can see this row when it generates diff --git a/m2d/converters/utils.js b/m2d/converters/utils.js index 108da1f..7b9c504 100644 --- a/m2d/converters/utils.js +++ b/m2d/converters/utils.js @@ -11,6 +11,7 @@ function eventSenderIsFromDiscord(sender) { // If it's from a user in the bridge's namespace, then it originated from discord // This includes messages sent by the appservice's bot user, because that is what's used for webhooks // TODO: It would be nice if bridge system messages wouldn't trigger this check and could be bridged from matrix to discord, while webhook reflections would remain ignored... + // TODO that only applies to the above todo: But you'd have to watch out for the /icon command, where the bridge bot would set the room avatar, and that shouldn't be reflected into the room a second time. if (userRegex.some(x => sender.match(x))) { return true } diff --git a/seed.js b/seed.js index d84ca8d..e513f63 100644 --- a/seed.js +++ b/seed.js @@ -25,8 +25,8 @@ const reg = require("./matrix/read-registration") const avatarUrl = await file.uploadDiscordFileToMxc("https://cadence.moe/friends/out_of_your_element_rev_2.jpg") // set profile data on homeserver... - await api.profileSetDisplayname(`@${reg.sender_localpart}:cadence.moe`, "Out Of Your Element") - await api.profileSetAvatarUrl(`@${reg.sender_localpart}:cadence.moe`, avatarUrl) + await api.profileSetDisplayname(`@${reg.sender_localpart}:${reg.ooye.server_name}`, "Out Of Your Element") + await api.profileSetAvatarUrl(`@${reg.sender_localpart}:${reg.ooye.server_name}`, avatarUrl) // database ddl... diff --git a/types.d.ts b/types.d.ts index faf4c70..2ba8d1d 100644 --- a/types.d.ts +++ b/types.d.ts @@ -19,6 +19,7 @@ export type AppServiceRegistrationConfig = { ooye: { namespace_prefix: string max_file_size: number + server_name: string } } @@ -27,7 +28,7 @@ export type WebhookCreds = { token: string } -namespace Event { +export namespace Event { export type Outer = { type: string room_id: string @@ -92,7 +93,7 @@ namespace Event { } } -namespace R { +export namespace R { export type RoomCreated = { room_id: string } From 8d536d5ef266a23253a55db2827503b9e42b3bab Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 23 Aug 2023 16:16:36 +1200 Subject: [PATCH 75/99] using kstate with power levels should mix them --- db/data-for-test.sql | 11 +++++---- matrix/api.js | 2 +- matrix/kstate.js | 23 +++++++++++++++--- matrix/kstate.test.js | 54 +++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 8 +++---- package.json | 2 +- stdin.js | 1 + 7 files changed, 87 insertions(+), 14 deletions(-) diff --git a/db/data-for-test.sql b/db/data-for-test.sql index 4a406c9..ec9f9ec 100644 --- a/db/data-for-test.sql +++ b/db/data-for-test.sql @@ -34,6 +34,7 @@ CREATE TABLE IF NOT EXISTS "channel_room" ( "name" TEXT, "nick" TEXT, "thread_parent" TEXT, + "custom_avatar" TEXT, PRIMARY KEY("channel_id") ); CREATE TABLE IF NOT EXISTS "event_message" ( @@ -55,11 +56,11 @@ BEGIN TRANSACTION; INSERT INTO guild_space (guild_id, space_id) VALUES ('112760669178241024', '!jjWAGMeQdNrVZSSfvz:cadence.moe'); -INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent) VALUES -('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main', NULL), -('497161350934560778', '!edUxjVdzgUvXDUIQCK:cadence.moe', 'amanda-spam', NULL, NULL), -('160197704226439168', '!uCtjHhfGlYbVnPVlkG:cadence.moe', 'the-stanley-parable-channel', 'bots', NULL), -('1100319550446252084', '!PnyBKvUBOhjuCucEfk:cadence.moe', 'worm-farm', NULL, NULL); +INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar) VALUES +('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main', NULL, NULL), +('497161350934560778', '!edUxjVdzgUvXDUIQCK:cadence.moe', 'amanda-spam', NULL, NULL, NULL), +('160197704226439168', '!uCtjHhfGlYbVnPVlkG:cadence.moe', 'the-stanley-parable-channel', 'bots', NULL, NULL), +('1100319550446252084', '!PnyBKvUBOhjuCucEfk:cadence.moe', 'worm-farm', NULL, NULL, NULL); INSERT INTO sim (discord_id, sim_name, localpart, mxid) VALUES ('0', 'bot', '_ooye_bot', '@_ooye_bot:cadence.moe'), diff --git a/matrix/api.js b/matrix/api.js index 9eff6c7..2e0763e 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -169,7 +169,7 @@ async function profileSetAvatarUrl(mxid, avatar_url) { async function setUserPower(roomID, mxid, power) { assert(roomID[0] === "!") assert(mxid[0] === "@") - // Yes it's this hard https://github.com/matrix-org/matrix-appservice-bridge/blob/2334b0bae28a285a767fe7244dad59f5a5963037/src/components/intent.ts#L352 + // Yes there's no shortcut https://github.com/matrix-org/matrix-appservice-bridge/blob/2334b0bae28a285a767fe7244dad59f5a5963037/src/components/intent.ts#L352 const powerLevels = await getStateEvent(roomID, "m.room.power_levels", "") const users = powerLevels.users || {} if (power != null) { diff --git a/matrix/kstate.js b/matrix/kstate.js index 1b2ca14..469ec91 100644 --- a/matrix/kstate.js +++ b/matrix/kstate.js @@ -1,6 +1,7 @@ // @ts-check -const assert = require("assert") +const assert = require("assert").strict +const mixin = require("mixin-deep") /** Mutates the input. */ function kstateStripConditionals(kstate) { @@ -43,18 +44,34 @@ function diffKState(actual, target) { // go through each key that it should have for (const key of Object.keys(target)) { if (!key.includes("/")) throw new Error(`target kstate's key "${key}" does not contain a slash separator; if a blank state_key was intended, add a trailing slash to the kstate key.`) - if (key in actual) { + + if (key === "m.room.power_levels/") { + // Special handling for power levels, we want to deep merge the actual and target into the final state. + console.log(actual[key]) + const temp = mixin({}, actual[key], target[key]) + console.log(actual[key]) + console.log(temp) + try { + assert.deepEqual(actual[key], temp) + } catch (e) { + // they differ. use the newly prepared object as the diff. + diff[key] = temp + } + + } else if (key in actual) { // diff try { assert.deepEqual(actual[key], target[key]) } catch (e) { - // they differ. reassign the target + // they differ. use the target as the diff. diff[key] = target[key] } + } else { // not present, needs to be added diff[key] = target[key] } + // keys that are missing in "actual" will not be deleted on "target" (no action) } return diff diff --git a/matrix/kstate.test.js b/matrix/kstate.test.js index 1541898..11d5131 100644 --- a/matrix/kstate.test.js +++ b/matrix/kstate.test.js @@ -92,3 +92,57 @@ test("diffKState: detects new properties", t => { } ) }) + +test("diffKState: power levels are mixed together", t => { + const original = { + "m.room.power_levels/": { + "ban": 50, + "events": { + "m.room.name": 100, + "m.room.power_levels": 100 + }, + "events_default": 0, + "invite": 50, + "kick": 50, + "notifications": { + "room": 20 + }, + "redact": 50, + "state_default": 50, + "users": { + "@example:localhost": 100 + }, + "users_default": 0 + } + } + const result = diffKState(original, { + "m.room.power_levels/": { + "events": { + "m.room.avatar": 0 + } + } + }) + t.deepEqual(result, { + "m.room.power_levels/": { + "ban": 50, + "events": { + "m.room.name": 100, + "m.room.power_levels": 100, + "m.room.avatar": 0 + }, + "events_default": 0, + "invite": 50, + "kick": 50, + "notifications": { + "room": 20 + }, + "redact": 50, + "state_default": 50, + "users": { + "@example:localhost": 100 + }, + "users_default": 0 + } + }) + t.notDeepEqual(original, result) +}) diff --git a/package-lock.json b/package-lock.json index 875e329..e808e1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "js-yaml": "^4.1.0", "matrix-appservice": "^2.0.0", "matrix-js-sdk": "^24.1.0", - "mixin-deep": "^2.0.1", + "mixin-deep": "github:cloudrac3r/mixin-deep#v3.0.0", "node-fetch": "^2.6.7", "prettier-bytes": "^1.0.4", "snowtransfer": "^0.8.0", @@ -2111,9 +2111,9 @@ } }, "node_modules/mixin-deep": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-2.0.1.tgz", - "integrity": "sha512-imbHQNRglyaplMmjBLL3V5R6Bfq5oM+ivds3SKgc6oRtzErEnBUUc5No11Z2pilkUvl42gJvi285xTNswcKCMA==", + "version": "3.0.0", + "resolved": "git+ssh://git@github.com/cloudrac3r/mixin-deep.git#2dd70d6b8644263f7ed2c1620506c9eb3f11d32a", + "license": "MIT", "engines": { "node": ">=6" } diff --git a/package.json b/package.json index bc0a0db..67aeade 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "js-yaml": "^4.1.0", "matrix-appservice": "^2.0.0", "matrix-js-sdk": "^24.1.0", - "mixin-deep": "^2.0.1", + "mixin-deep": "github:cloudrac3r/mixin-deep#v3.0.0", "node-fetch": "^2.6.7", "prettier-bytes": "^1.0.4", "snowtransfer": "^0.8.0", diff --git a/stdin.js b/stdin.js index 61a2a08..ce612f5 100644 --- a/stdin.js +++ b/stdin.js @@ -14,6 +14,7 @@ const mreq = sync.require("./matrix/mreq") const api = sync.require("./matrix/api") const sendEvent = sync.require("./m2d/actions/send-event") const eventDispatcher = sync.require("./d2m/event-dispatcher") +const ks = sync.require("./matrix/kstate") const guildID = "112760669178241024" const extraContext = {} From a8fab062a4527d52d45a7be54da11f9c176bd6db Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 23 Aug 2023 17:08:20 +1200 Subject: [PATCH 76/99] workaround synapse bug that would make broken PLs --- d2m/actions/create-room.js | 60 ++++++++++++++++++++++++++++++------- d2m/actions/create-space.js | 31 +++++++++---------- matrix/kstate.js | 4 +-- test/data.js | 7 ++++- 4 files changed, 72 insertions(+), 30 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index c8f0661..29bcdc0 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -95,6 +95,11 @@ async function channelToKState(channel, guild) { type: "m.room_membership", room_id: spaceID }] + }, + "m.room.power_levels/": { + events: { + "m.room.avatar": 0 + } } } @@ -114,24 +119,56 @@ async function createRoom(channel, guild, spaceID, kstate) { if (channel.type === DiscordTypes.ChannelType.PublicThread) threadParent = channel.parent_id const invite = threadParent ? [] : ["@cadence:cadence.moe"] // TODO - const [convertedName, convertedTopic] = convertNameAndTopic(channel, guild, null) - const roomID = await api.createRoom({ - name: convertedName, - topic: convertedTopic, - preset: "private_chat", - visibility: "private", - invite, - initial_state: ks.kstateToState(kstate) + const roomID = await postApplyPowerLevels(kstate, async kstate => { + const [convertedName, convertedTopic] = convertNameAndTopic(channel, guild, null) + const roomID = await api.createRoom({ + name: convertedName, + topic: convertedTopic, + preset: "private_chat", + visibility: "private", + invite, + initial_state: ks.kstateToState(kstate) + }) + + db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent) VALUES (?, ?, ?, NULL, ?)").run(channel.id, roomID, channel.name, threadParent) + + return roomID }) - db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent) VALUES (?, ?, ?, NULL, ?)").run(channel.id, roomID, channel.name, threadParent) - - // Put the newly created child into the space + // Put the newly created child into the space, no need to await this _syncSpaceMember(channel, spaceID, roomID) return roomID } +/** + * Handling power levels separately. The spec doesn't specify what happens, Dendrite differs, + * and Synapse does an absolutely insane *shallow merge* of what I provide on top of what it creates. + * We don't want the `events` key to be overridden completely. + * https://github.com/matrix-org/synapse/blob/develop/synapse/handlers/room.py#L1170-L1210 + * https://github.com/matrix-org/matrix-spec/issues/492 + * @param {any} kstate + * @param {(_: any) => Promise} callback must return room ID + * @returns {Promise} room ID + */ +async function postApplyPowerLevels(kstate, callback) { + const powerLevelContent = kstate["m.room.power_levels/"] + const kstateWithoutPowerLevels = {...kstate} + delete kstateWithoutPowerLevels["m.room.power_levels/"] + + /** @type {string} */ + const roomID = await callback(kstateWithoutPowerLevels) + + // Now *really* apply the power level overrides on top of what Synapse *really* set + if (powerLevelContent) { + const newRoomKState = await roomToKState(roomID) + const newRoomPowerLevelsDiff = ks.diffKState(newRoomKState, {"m.room.power_levels/": powerLevelContent}) + await applyKStateDiffToRoom(roomID, newRoomPowerLevelsDiff) + } + + return roomID +} + /** * @param {DiscordTypes.APIGuildChannel} channel */ @@ -290,5 +327,6 @@ module.exports.createAllForGuild = createAllForGuild module.exports.channelToKState = channelToKState module.exports.roomToKState = roomToKState module.exports.applyKStateDiffToRoom = applyKStateDiffToRoom +module.exports.postApplyPowerLevels = postApplyPowerLevels module.exports._convertNameAndTopic = convertNameAndTopic module.exports._unbridgeRoom = _unbridgeRoom diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index 34aa25f..46fa71f 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -21,23 +21,24 @@ const ks = sync.require("../../matrix/kstate") async function createSpace(guild, kstate) { const name = kstate["m.room.name/"].name const topic = kstate["m.room.topic/"]?.topic || undefined - assert(name) - const roomID = await api.createRoom({ - name, - preset: "private_chat", // cannot join space unless invited - visibility: "private", - power_level_content_override: { - events_default: 100, // space can only be managed by bridge - invite: 0 // any existing member can invite others - }, - invite: ["@cadence:cadence.moe"], // TODO - topic, - creation_content: { - type: "m.space" - }, - initial_state: ks.kstateToState(kstate) + const roomID = await createRoom.postApplyPowerLevels(kstate, async kstate => { + return api.createRoom({ + name, + preset: "private_chat", // cannot join space unless invited + visibility: "private", + power_level_content_override: { + events_default: 100, // space can only be managed by bridge + invite: 0 // any existing member can invite others + }, + invite: ["@cadence:cadence.moe"], // TODO + topic, + creation_content: { + type: "m.space" + }, + initial_state: ks.kstateToState(kstate) + }) }) db.prepare("INSERT INTO guild_space (guild_id, space_id) VALUES (?, ?)").run(guild.id, roomID) return roomID diff --git a/matrix/kstate.js b/matrix/kstate.js index 469ec91..e840254 100644 --- a/matrix/kstate.js +++ b/matrix/kstate.js @@ -47,10 +47,8 @@ function diffKState(actual, target) { if (key === "m.room.power_levels/") { // Special handling for power levels, we want to deep merge the actual and target into the final state. - console.log(actual[key]) + if (!(key in actual)) throw new Error(`want to apply a power levels diff, but original power level data is missing\nstarted with: ${JSON.stringify(actual)}\nwant to apply: ${JSON.stringify(target)}`) const temp = mixin({}, actual[key], target[key]) - console.log(actual[key]) - console.log(temp) try { assert.deepEqual(actual[key], temp) } catch (e) { diff --git a/test/data.js b/test/data.js index 30d108a..6ed2f42 100644 --- a/test/data.js +++ b/test/data.js @@ -27,7 +27,7 @@ module.exports = { "m.room.guest_access/": {guest_access: "can_join"}, "m.room.history_visibility/": {history_visibility: "invited"}, "m.space.parent/!jjWAGMeQdNrVZSSfvz:cadence.moe": { - via: ["cadence.moe"], // TODO: put the proper server here + via: ["cadence.moe"], canonical: true }, "m.room.join_rules/": { @@ -40,6 +40,11 @@ module.exports = { "m.room.avatar/": { discord_path: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024", url: "mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF" + }, + "m.room.power_levels/": { + events: { + "m.room.avatar": 0 + } } } }, From f0ff89161a7fc4da2e4a31b63ac3ceae8acb5008 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 24 Aug 2023 00:27:51 +1200 Subject: [PATCH 77/99] syncing all room power levels --- d2m/actions/create-room.js | 5 ++--- matrix/read-registration.js | 6 +++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 29bcdc0..b641d37 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -63,9 +63,8 @@ async function channelToKState(channel, guild) { assert.ok(typeof spaceID === "string") const row = db.prepare("SELECT nick, custom_avatar FROM channel_room WHERE channel_id = ?").get(channel.id) - assert(row) - const customName = row.nick - const customAvatar = row.custom_avatar + const customName = row?.nick + const customAvatar = row?.custom_avatar const [convertedName, convertedTopic] = convertNameAndTopic(channel, guild, customName) const avatarEventContent = {} diff --git a/matrix/read-registration.js b/matrix/read-registration.js index a1d920d..54d77ae 100644 --- a/matrix/read-registration.js +++ b/matrix/read-registration.js @@ -1,8 +1,12 @@ // @ts-check const fs = require("fs") +const assert = require("assert").strict const yaml = require("js-yaml") /** @ts-ignore @type {import("../types").AppServiceRegistrationConfig} */ const reg = yaml.load(fs.readFileSync("registration.yaml", "utf8")) -module.exports = reg \ No newline at end of file +assert(reg.ooye.max_file_size) +assert(reg.ooye.namespace_prefix) +assert(reg.ooye.server_name) +module.exports = reg From 4c0aa57ba77da5a5de33b00a0669031a2b850980 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 24 Aug 2023 00:35:35 +1200 Subject: [PATCH 78/99] add test case for m->d too-long message --- m2d/converters/event-to-message.test.js | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index f0c4664..a45c23b 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -27,3 +27,31 @@ test("event2message: janky test", t => { }] ) }) + +test("event2message: long messages are split", t => { + t.deepEqual( + eventToMessage({ + content: { + body: ("a".repeat(130) + " ").repeat(19), + msgtype: "m.text" + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }), + [{ + username: "cadence", + content: (("a".repeat(130) + " ").repeat(15)).slice(0, -1), + avatar_url: undefined + }, { + username: "cadence", + content: (("a".repeat(130) + " ").repeat(4)).slice(0, -1), + avatar_url: undefined + }] + ) +}) From 40c3ef8e83ff27d2c5446266d8347bbe7e254eb8 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 24 Aug 2023 12:42:12 +1200 Subject: [PATCH 79/99] don't set the name and topic twice --- d2m/actions/create-room.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index b641d37..cb1bc85 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -118,13 +118,21 @@ async function createRoom(channel, guild, spaceID, kstate) { if (channel.type === DiscordTypes.ChannelType.PublicThread) threadParent = channel.parent_id const invite = threadParent ? [] : ["@cadence:cadence.moe"] // TODO + // Name and topic can be done earlier in room creation rather than in initial_state + // https://spec.matrix.org/latest/client-server-api/#creation + const name = kstate["m.room.name/"].name + delete kstate["m.room.name/"] + assert(name) + const topic = kstate["m.room.topic/"].topic + delete kstate["m.room.topic/"] + assert(topic) + const roomID = await postApplyPowerLevels(kstate, async kstate => { - const [convertedName, convertedTopic] = convertNameAndTopic(channel, guild, null) const roomID = await api.createRoom({ - name: convertedName, - topic: convertedTopic, - preset: "private_chat", - visibility: "private", + name, + topic, + preset: "private_chat", // This is closest to what we want, but properties from kstate override it anyway + visibility: "private", // Not shown in the room directory invite, initial_state: ks.kstateToState(kstate) }) From c56e92ccfbbdb3c0948895cb056e4984a4c6f443 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 24 Aug 2023 17:09:25 +1200 Subject: [PATCH 80/99] start adding command handlers --- d2m/discord-command-handler.js | 81 ++++++++++++++++++++++++++++++++++ d2m/event-dispatcher.js | 8 +++- matrix/api.js | 1 + 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 d2m/discord-command-handler.js diff --git a/d2m/discord-command-handler.js b/d2m/discord-command-handler.js new file mode 100644 index 0000000..9f96c18 --- /dev/null +++ b/d2m/discord-command-handler.js @@ -0,0 +1,81 @@ +// @ts-check + +const assert = require("assert").strict +const util = require("util") +const DiscordTypes = require("discord-api-types/v10") +const {discord, sync, db} = require("../passthrough") +/** @type {import("../matrix/api")}) */ +const api = sync.require("../matrix/api") + +const prefix = "/" + +/** + * @callback CommandExecute + * @param {DiscordTypes.GatewayMessageCreateDispatchData} message + * @param {DiscordTypes.APIGuildTextChannel} channel + * @param {DiscordTypes.APIGuild} guild + * @param {any} [ctx] + */ + +/** + * @typedef Command + * @property {string[]} aliases + * @property {(message: DiscordTypes.GatewayMessageCreateDispatchData, channel: DiscordTypes.APIGuildTextChannel, guild: DiscordTypes.APIGuild) => Promise} execute + */ + +/** @param {CommandExecute} execute */ +function replyctx(execute) { + /** @type {CommandExecute} */ + return function(message, channel, guild, ctx = {}) { + ctx.message_reference = { + message_id: message.id, + channel_id: channel.id, + guild_id: guild.id, + fail_if_not_exists: false + } + return execute(message, channel, guild, ctx) + } +} + +/** @type {Command[]} */ +const commands = [{ + aliases: ["icon", "avatar", "roomicon", "roomavatar", "channelicon", "channelavatar"], + execute: replyctx( + async (message, channel, guild, ctx) => { + const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(channel.id) + if (!roomID) return discord.snow.channel.createMessage(channel.id, { + ...ctx, + content: "This channel isn't bridged to the other side." + }) + const avatarEvent = await api.getStateEvent(roomID, "m.room.avatar", "") + const avatarURL = avatarEvent?.url + return discord.snow.channel.createMessage(channel.id, { + ...ctx, + content: `Current room avatar: ${avatarURL}` + }) + } + ) +}, { + aliases: ["invite"], + execute: replyctx( + async (message, channel, guild, ctx) => { + discord.snow.channel.createMessage(channel.id, { + ...ctx, + content: "This command isn't implemented yet." + }) + } + ) +}] + +/** @type {CommandExecute} */ +async function execute(message, channel, guild) { + if (!message.content.startsWith(prefix)) return + const words = message.content.split(" ") + const commandName = words[0] + const command = commands.find(c => c.aliases.includes(commandName)) + if (!command) return + + await command.execute(message, channel, guild) +} + +module.exports.execute = execute diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 9bb07c0..91a7cba 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -18,6 +18,8 @@ const createRoom = sync.require("./actions/create-room") const createSpace = sync.require("./actions/create-space") /** @type {import("../matrix/api")}) */ const api = sync.require("../matrix/api") +/** @type {import("./discord-command-handler")}) */ +const discordCommandHandler = sync.require("./discord-command-handler") let lastReportedEvent = 0 @@ -156,7 +158,11 @@ module.exports = { if (!channel.guild_id) return // Nothing we can do in direct messages. const guild = client.guilds.get(channel.guild_id) if (!isGuildAllowed(guild.id)) return - await sendMessage.sendMessage(message, guild) + + await Promise.all([ + sendMessage.sendMessage(message, guild), + discordCommandHandler.execute(message, channel, guild) + ]) }, /** diff --git a/matrix/api.js b/matrix/api.js index 2e0763e..7253a1a 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -190,6 +190,7 @@ module.exports.inviteToRoom = inviteToRoom module.exports.leaveRoom = leaveRoom module.exports.getEvent = getEvent module.exports.getAllState = getAllState +module.exports.getStateEvent = getStateEvent module.exports.getJoinedMembers = getJoinedMembers module.exports.sendState = sendState module.exports.sendEvent = sendEvent From 1e9e9685c539565cca760fcc32e732b4d80dbab6 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 24 Aug 2023 17:23:32 +1200 Subject: [PATCH 81/99] minor code coverage --- d2m/converters/user-to-mxid.js | 3 ++- m2d/converters/event-to-message.js | 2 +- matrix/txnid.test.js | 12 ++++++++++++ test/test.js | 1 + 4 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 matrix/txnid.test.js diff --git a/d2m/converters/user-to-mxid.js b/d2m/converters/user-to-mxid.js index 89e47a4..1fe8ffc 100644 --- a/d2m/converters/user-to-mxid.js +++ b/d2m/converters/user-to-mxid.js @@ -39,6 +39,7 @@ function* generateLocalpartAlternatives(preferences) { let i = 2 while (true) { yield best + (i++) + /* c8 ignore next */ } } @@ -69,7 +70,7 @@ function userToSimName(user) { for (const suggestion of generateLocalpartAlternatives(preferences)) { if (!matches.includes(suggestion)) return suggestion } - + /* c8 ignore next */ throw new Error(`Ran out of suggestions when generating sim name. downcased: "${downcased}"`) } diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index b2c56a9..dde77b7 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -32,7 +32,7 @@ function eventToMessage(event) { }) } else if (event.content.msgtype === "m.emote") { messages.push({ - content: `*${displayName} ${event.content.body}*`, + content: `\* _${displayName} ${event.content.body}_`, username: displayName, avatar_url: avatarURL }) diff --git a/matrix/txnid.test.js b/matrix/txnid.test.js new file mode 100644 index 0000000..4db873c --- /dev/null +++ b/matrix/txnid.test.js @@ -0,0 +1,12 @@ +// @ts-check + +const {test} = require("supertape") +const txnid = require("./txnid") + +test("txnid: generates different values each run", t => { + const one = txnid.makeTxnId() + t.ok(one) + const two = txnid.makeTxnId() + t.ok(two) + t.notEqual(two, one) +}) diff --git a/test/test.js b/test/test.js index 606bd4b..e19f8ff 100644 --- a/test/test.js +++ b/test/test.js @@ -20,6 +20,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../matrix/kstate.test") require("../matrix/api.test") require("../matrix/read-registration.test") +require("../matrix/txnid.test") require("../d2m/converters/message-to-event.test") require("../d2m/converters/message-to-event.embeds.test") require("../d2m/converters/edit-to-changes.test") From beeb27bbd87498a18abfc06bcfca962531118433 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 25 Aug 2023 11:44:58 +1200 Subject: [PATCH 82/99] test on a member that has no member props --- d2m/actions/register-user.test.js | 22 ++++++++++++++++- db/data-for-test.sql | 3 ++- matrix/file.js | 12 +++++----- stdin.js | 1 + test/data.js | 40 +++++++++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 8 deletions(-) diff --git a/d2m/actions/register-user.test.js b/d2m/actions/register-user.test.js index 34470ba..74818ea 100644 --- a/d2m/actions/register-user.test.js +++ b/d2m/actions/register-user.test.js @@ -3,7 +3,27 @@ const {_memberToStateContent} = require("./register-user") const {test} = require("supertape") const testData = require("../../test/data") -test("member2state: general", async t => { +test("member2state: without member nick or avatar", async t => { + t.deepEqual( + await _memberToStateContent(testData.member.kumaccino.user, testData.member.kumaccino, testData.guild.general.id), + { + avatar_url: "mxc://cadence.moe/UpAeIqeclhKfeiZNdIWNcXXL", + displayname: "kumaccino", + membership: "join", + "moe.cadence.ooye.member": { + avatar: "/avatars/113340068197859328/b48302623a12bc7c59a71328f72ccb39.png?size=1024" + }, + "uk.half-shot.discord.member": { + bot: false, + displayColor: 10206929, + id: "113340068197859328", + username: "@kumaccino" + } + } + ) +}) + +test("member2state: with member nick and avatar", async t => { t.deepEqual( await _memberToStateContent(testData.member.sheep.user, testData.member.sheep, testData.guild.general.id), { diff --git a/db/data-for-test.sql b/db/data-for-test.sql index ec9f9ec..e88b967 100644 --- a/db/data-for-test.sql +++ b/db/data-for-test.sql @@ -93,6 +93,7 @@ INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/attachments/112760669178241024/1141501302497615912/piper_2.png', 'mxc://cadence.moe/KQYdXKRcHWjDYDLPkTOOWOjA'), ('https://cdn.discordapp.com/attachments/112760669178241024/1128084851023675515/RDT_20230704_0936184915846675925224905.jpg', 'mxc://cadence.moe/WlAbFSiNRIHPDEwKdyPeGywa'), ('https://cdn.discordapp.com/guilds/112760669178241024/users/134826546694193153/avatars/38dd359aa12bcd52dd3164126c587f8c.png?size=1024', 'mxc://cadence.moe/rfemHmAtcprjLEiPiEuzPhpl'), -('https://cdn.discordapp.com/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024', 'mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF'); +('https://cdn.discordapp.com/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024', 'mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF'), +('https://cdn.discordapp.com/avatars/113340068197859328/b48302623a12bc7c59a71328f72ccb39.png?size=1024', 'mxc://cadence.moe/UpAeIqeclhKfeiZNdIWNcXXL'); COMMIT; diff --git a/matrix/file.js b/matrix/file.js index 965ec1c..7d74d5d 100644 --- a/matrix/file.js +++ b/matrix/file.js @@ -27,15 +27,15 @@ async function uploadDiscordFileToMxc(path) { } // Are we uploading this file RIGHT NOW? Return the same inflight promise with the same resolution - let existing = inflight.get(url) - if (typeof existing === "string") { - return existing + const existingInflight = inflight.get(url) + if (existingInflight) { + return existingInflight } // Has this file already been uploaded in the past? Grab the existing copy from the database. - existing = db.prepare("SELECT mxc_url FROM file WHERE discord_url = ?").pluck().get(url) - if (typeof existing === "string") { - return existing + const existingFromDb = db.prepare("SELECT mxc_url FROM file WHERE discord_url = ?").pluck().get(url) + if (typeof existingFromDb === "string") { + return existingFromDb } // Download from Discord diff --git a/stdin.js b/stdin.js index ce612f5..a687c6c 100644 --- a/stdin.js +++ b/stdin.js @@ -12,6 +12,7 @@ const createRoom = sync.require("./d2m/actions/create-room") const registerUser = sync.require("./d2m/actions/register-user") const mreq = sync.require("./matrix/mreq") const api = sync.require("./matrix/api") +const file = sync.require("./matrix/file") const sendEvent = sync.require("./m2d/actions/send-event") const eventDispatcher = sync.require("./d2m/event-dispatcher") const ks = sync.require("./matrix/kstate") diff --git a/test/data.js b/test/data.js index 6ed2f42..a4d836d 100644 --- a/test/data.js +++ b/test/data.js @@ -98,6 +98,46 @@ module.exports = { } }, member: { + kumaccino: { + avatar: null, + communication_disabled_until: null, + flags: 0, + joined_at: "2015-11-11T09:55:40.321000+00:00", + nick: null, + pending: false, + premium_since: null, + roles: [ + "112767366235959296", "118924814567211009", + "199995902742626304", "204427286542417920", + "222168467627835392", "238028326281805825", + "259806643414499328", "265239342648131584", + "271173313575780353", "287733611912757249", + "225744901915148298", "305775031223320577", + "318243902521868288", "348651574924541953", + "349185088157777920", "378402925128712193", + "392141548932038658", "393912152173576203", + "482860581670486028", "495384759074160642", + "638988388740890635", "373336013109461013", + "530220455085473813", "454567553738473472", + "790724320824655873", "1040735082610167858", + "695946570482450442", "849737964090556488" + ], + user: { + id: "113340068197859328", + username: "kumaccino", + avatar: "b48302623a12bc7c59a71328f72ccb39", + discriminator: "0", + public_flags: 128, + flags: 128, + banner: null, + accent_color: 10206929, + global_name: "kumaccino", + avatar_decoration_data: null, + banner_color: "#9bbed1" + }, + mute: false, + deaf: false + }, sheep: { avatar: "38dd359aa12bcd52dd3164126c587f8c", communication_disabled_until: null, From 30bf87b106a94a0dc64ad87b851baf21111e3414 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 25 Aug 2023 12:05:16 +1200 Subject: [PATCH 83/99] channel name decoration for threads and voice-text --- d2m/actions/create-room.js | 11 ++++++++--- d2m/actions/create-room.test.js | 29 +++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index cb1bc85..6fbc42e 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -36,12 +36,17 @@ function applyKStateDiffToRoom(roomID, kstate) { } /** - * @param {{id: string, name: string, topic?: string?}} channel + * @param {{id: string, name: string, topic?: string?, type: number}} channel * @param {{id: string}} guild * @param {string?} customName */ function convertNameAndTopic(channel, guild, customName) { - const convertedName = customName || channel.name; + let channelPrefix = + ( channel.type === DiscordTypes.ChannelType.PublicThread ? "[⛓️] " + : channel.type === DiscordTypes.ChannelType.PrivateThread ? "[🔒⛓️] " + : channel.type === DiscordTypes.ChannelType.GuildVoice ? "[🔊] " + : "") + const chosenName = customName || (channelPrefix + channel.name); const maybeTopicWithPipe = channel.topic ? ` | ${channel.topic}` : ''; const maybeTopicWithNewlines = channel.topic ? `${channel.topic}\n\n` : ''; const channelIDPart = `Channel ID: ${channel.id}`; @@ -51,7 +56,7 @@ function convertNameAndTopic(channel, guild, customName) { ? `#${channel.name}${maybeTopicWithPipe}\n\n${channelIDPart}\n${guildIDPart}` : `${maybeTopicWithNewlines}${channelIDPart}\n${guildIDPart}`; - return [convertedName, convertedTopic]; + return [chosenName, convertedTopic]; } /** diff --git a/d2m/actions/create-room.test.js b/d2m/actions/create-room.test.js index ec5c3d3..e40bf6f 100644 --- a/d2m/actions/create-room.test.js +++ b/d2m/actions/create-room.test.js @@ -14,28 +14,49 @@ test("channel2room: general", async t => { test("convertNameAndTopic: custom name and topic", t => { t.deepEqual( - _convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:"}, {id: "456"}, "hauntings"), + _convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:", type: 0}, {id: "456"}, "hauntings"), ["hauntings", "#the-twilight-zone | Spooky stuff here. :ghost:\n\nChannel ID: 123\nGuild ID: 456"] ) }) test("convertNameAndTopic: custom name, no topic", t => { t.deepEqual( - _convertNameAndTopic({id: "123", name: "the-twilight-zone"}, {id: "456"}, "hauntings"), + _convertNameAndTopic({id: "123", name: "the-twilight-zone", type: 0}, {id: "456"}, "hauntings"), ["hauntings", "#the-twilight-zone\n\nChannel ID: 123\nGuild ID: 456"] ) }) test("convertNameAndTopic: original name and topic", t => { t.deepEqual( - _convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:"}, {id: "456"}, null), + _convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:", type: 0}, {id: "456"}, null), ["the-twilight-zone", "Spooky stuff here. :ghost:\n\nChannel ID: 123\nGuild ID: 456"] ) }) test("convertNameAndTopic: original name, no topic", t => { t.deepEqual( - _convertNameAndTopic({id: "123", name: "the-twilight-zone"}, {id: "456"}, null), + _convertNameAndTopic({id: "123", name: "the-twilight-zone", type: 0}, {id: "456"}, null), ["the-twilight-zone", "Channel ID: 123\nGuild ID: 456"] ) }) + +test("convertNameAndTopic: public thread icon", t => { + t.deepEqual( + _convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:", type: 11}, {id: "456"}, null), + ["[⛓️] the-twilight-zone", "Spooky stuff here. :ghost:\n\nChannel ID: 123\nGuild ID: 456"] + ) +}) + +test("convertNameAndTopic: private thread icon", t => { + t.deepEqual( + _convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:", type: 12}, {id: "456"}, null), + ["[🔒⛓️] the-twilight-zone", "Spooky stuff here. :ghost:\n\nChannel ID: 123\nGuild ID: 456"] + ) +}) + +test("convertNameAndTopic: voice channel icon", t => { + t.deepEqual( + _convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:", type: 2}, {id: "456"}, null), + ["[🔊] the-twilight-zone", "Spooky stuff here. :ghost:\n\nChannel ID: 123\nGuild ID: 456"] + ) +}) From 69a01a0608966492639bbc7da8656ea3d4a39e02 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 25 Aug 2023 16:01:19 +1200 Subject: [PATCH 84/99] implemented //icon with button confirmation system --- d2m/discord-command-handler.js | 77 ++++++++++++++++++++++++++++++---- d2m/event-dispatcher.js | 1 + 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/d2m/discord-command-handler.js b/d2m/discord-command-handler.js index 9f96c18..1bd52c8 100644 --- a/d2m/discord-command-handler.js +++ b/d2m/discord-command-handler.js @@ -6,15 +6,48 @@ const DiscordTypes = require("discord-api-types/v10") const {discord, sync, db} = require("../passthrough") /** @type {import("../matrix/api")}) */ const api = sync.require("../matrix/api") +/** @type {import("../matrix/file")} */ +const file = sync.require("../matrix/file") -const prefix = "/" +const PREFIX = "//" + +let buttons = [] + +/** + * @param {string} channelID where to add the button + * @param {string} messageID where to add the button + * @param {string} emoji emoji to add as a button + * @param {string} userID only listen for responses from this user + * @returns {Promise} + */ +async function addButton(channelID, messageID, emoji, userID) { + await discord.snow.channel.createReaction(channelID, messageID, emoji) + return new Promise(resolve => { + buttons.push({channelID, messageID, userID, resolve, created: Date.now()}) + }) +} + +// Clear out old buttons every so often to free memory +setInterval(() => { + const now = Date.now() + buttons = buttons.filter(b => now - b.created < 2*60*60*1000) +}, 10*60*1000) + +/** @param {import("discord-api-types/v10").GatewayMessageReactionAddDispatchData} data */ +function onReactionAdd(data) { + const button = buttons.find(b => b.channelID === data.channel_id && b.messageID === data.message_id && b.userID === data.user_id) + if (button) { + buttons = buttons.filter(b => b !== button) // remove button data so it can't be clicked again + button.resolve(data) + } +} /** * @callback CommandExecute * @param {DiscordTypes.GatewayMessageCreateDispatchData} message * @param {DiscordTypes.APIGuildTextChannel} channel * @param {DiscordTypes.APIGuild} guild - * @param {any} [ctx] + * @param {Partial} [ctx] */ /** @@ -42,24 +75,51 @@ const commands = [{ aliases: ["icon", "avatar", "roomicon", "roomavatar", "channelicon", "channelavatar"], execute: replyctx( async (message, channel, guild, ctx) => { + // Guard const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(channel.id) if (!roomID) return discord.snow.channel.createMessage(channel.id, { ...ctx, content: "This channel isn't bridged to the other side." }) + + // Current avatar const avatarEvent = await api.getStateEvent(roomID, "m.room.avatar", "") - const avatarURL = avatarEvent?.url - return discord.snow.channel.createMessage(channel.id, { + const avatarURLParts = avatarEvent?.url.match(/^mxc:\/\/([^/]+)\/(\w+)$/) + let currentAvatarMessage = + ( avatarURLParts ? `Current room-specific avatar: https://matrix.cadence.moe/_matrix/media/r0/download/${avatarURLParts[1]}/${avatarURLParts[2]}` + : "No avatar. Now's your time to strike. Use `//icon` again with a link or upload to set the room-specific avatar.") + + // Next potential avatar + const nextAvatarURL = message.attachments.find(a => a.content_type?.startsWith("image/"))?.url || message.content.match(/https?:\/\/[^ ]+\.[^ ]+\.(?:png|jpg|jpeg|webp)\b/)?.[0] + let nextAvatarMessage = + ( nextAvatarURL ? `\nYou want to set it to: ${nextAvatarURL}\nHit ✅ to make it happen.` + : "") + + const sent = await discord.snow.channel.createMessage(channel.id, { ...ctx, - content: `Current room avatar: ${avatarURL}` + content: currentAvatarMessage + nextAvatarMessage }) + + if (nextAvatarURL) { + addButton(channel.id, sent.id, "✅", message.author.id).then(async data => { + const mxcUrl = await file.uploadDiscordFileToMxc(nextAvatarURL) + await api.sendState(roomID, "m.room.avatar", "", { + url: mxcUrl + }) + db.prepare("UPDATE channel_room SET custom_avatar = ? WHERE channel_id = ?").run(mxcUrl, channel.id) + await discord.snow.channel.createMessage(channel.id, { + ...ctx, + content: "Your creation is unleashed. Any complaints will be redirected to Grelbo." + }) + }) + } } ) }, { aliases: ["invite"], execute: replyctx( async (message, channel, guild, ctx) => { - discord.snow.channel.createMessage(channel.id, { + return discord.snow.channel.createMessage(channel.id, { ...ctx, content: "This command isn't implemented yet." }) @@ -69,8 +129,8 @@ const commands = [{ /** @type {CommandExecute} */ async function execute(message, channel, guild) { - if (!message.content.startsWith(prefix)) return - const words = message.content.split(" ") + if (!message.content.startsWith(PREFIX)) return + const words = message.content.slice(PREFIX.length).split(" ") const commandName = words[0] const command = commands.find(c => c.aliases.includes(commandName)) if (!command) return @@ -79,3 +139,4 @@ async function execute(message, channel, guild) { } module.exports.execute = execute +module.exports.onReactionAdd = onReactionAdd diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 91a7cba..bf9bbd2 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -197,6 +197,7 @@ module.exports = { */ async onReactionAdd(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. + discordCommandHandler.onReactionAdd(data) if (data.emoji.id !== null) return // TODO: image emoji reactions await addReaction.addReaction(data) }, From 9f717dc24fb701cbccf7e525785c1514c85366b9 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 25 Aug 2023 16:03:43 +1200 Subject: [PATCH 85/99] preserve order: finish bridge before command reply --- d2m/event-dispatcher.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index bf9bbd2..6939c59 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -159,10 +159,8 @@ module.exports = { const guild = client.guilds.get(channel.guild_id) if (!isGuildAllowed(guild.id)) return - await Promise.all([ - sendMessage.sendMessage(message, guild), - discordCommandHandler.execute(message, channel, guild) - ]) + await sendMessage.sendMessage(message, guild), + await discordCommandHandler.execute(message, channel, guild) }, /** From 20cd7ab38e8177edbf742484f1e8db0d6ddd268a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 25 Aug 2023 17:23:51 +1200 Subject: [PATCH 86/99] sync child room avatars when guild is updated --- d2m/actions/create-room.js | 27 ++++++++++++++++----------- d2m/actions/create-space.js | 24 ++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 6fbc42e..2095130 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -17,6 +17,7 @@ const ks = sync.require("../../matrix/kstate") const inflightRoomCreate = new Map() /** + * Async because it gets all room state from the homeserver. * @param {string} roomID */ async function roomToKState(roomID) { @@ -60,6 +61,7 @@ function convertNameAndTopic(channel, guild, customName) { } /** + * Async because it may upload the guild icon to mxc. * @param {DiscordTypes.APIGuildTextChannel | DiscordTypes.APIThreadChannel} channel * @param {DiscordTypes.APIGuild} guild */ @@ -200,10 +202,10 @@ function channelToGuild(channel) { 3. Get kstate for channel 4. Create room, return new ID - New combined flow with ensure / sync: + Ensure + sync flow: 1. Get IDs 2. Does room exist? - 2.5: If room does exist AND don't need to sync: return here + 2.5: If room does exist AND wasn't asked to sync: return here 3. Get kstate for channel 4. Create room with kstate if room doesn't exist 5. Get and update room state with kstate if room does exist @@ -246,7 +248,7 @@ async function _syncRoom(channelID, shouldActuallySync) { console.log(`[room sync] to matrix: ${channel.name}`) - const {spaceID, channelKState} = await channelToKState(channel, guild) + const {spaceID, channelKState} = await channelToKState(channel, guild) // calling this in both branches because we don't want to calculate this if not syncing // sync channel state to room const roomKState = await roomToKState(roomID) @@ -261,6 +263,16 @@ async function _syncRoom(channelID, shouldActuallySync) { return roomID } +/** Ensures the room exists. If it doesn't, creates the room with an accurate initial state. */ +function ensureRoom(channelID) { + return _syncRoom(channelID, false) +} + +/** Actually syncs. Gets all room state from the homeserver in order to diff, and uploads the icon to mxc if it has changed. */ +function syncRoom(channelID) { + return _syncRoom(channelID, true) +} + async function _unbridgeRoom(channelID) { /** @ts-ignore @type {DiscordTypes.APIGuildChannel} */ const channel = discord.channels.get(channelID) @@ -289,6 +301,7 @@ async function _unbridgeRoom(channelID) { /** + * Async because it gets all space state from the homeserver, then if necessary sends one state event back. * @param {DiscordTypes.APIGuildTextChannel} channel * @param {string} spaceID * @param {string} roomID @@ -311,14 +324,6 @@ async function _syncSpaceMember(channel, spaceID, roomID) { return applyKStateDiffToRoom(spaceID, spaceDiff) } -function ensureRoom(channelID) { - return _syncRoom(channelID, false) -} - -function syncRoom(channelID) { - return _syncRoom(channelID, true) -} - async function createAllForGuild(guildID) { const channelIDs = discord.guildChannelMap.get(guildID) assert.ok(channelIDs) diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index 46fa71f..838bef9 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -1,6 +1,6 @@ // @ts-check -const assert = require("assert") +const assert = require("assert").strict const DiscordTypes = require("discord-api-types/v10") const passthrough = require("../../passthrough") @@ -84,11 +84,31 @@ async function syncSpace(guildID) { console.log(`[space sync] to matrix: ${guild.name}`) - // sync channel state to room + // sync guild state to space const spaceKState = await createRoom.roomToKState(spaceID) const spaceDiff = ks.diffKState(spaceKState, guildKState) await createRoom.applyKStateDiffToRoom(spaceID, spaceDiff) + // guild icon was changed, so room avatars need to be updated as well as the space ones + // doing it this way rather than calling syncRoom for great efficiency gains + const newAvatarState = spaceDiff["m.room.avatar/"] + if (guild.icon && newAvatarState?.url) { + // don't try to update rooms with custom avatars though + const roomsWithCustomAvatars = db.prepare("SELECT room_id FROM channel_room WHERE custom_avatar IS NOT NULL").pluck().all() + + const childRooms = ks.kstateToState(spaceKState).filter(({type, state_key, content}) => { + return type === "m.space.child" && "via" in content && roomsWithCustomAvatars.includes(state_key) + }).map(({state_key}) => state_key) + + for (const roomID of childRooms) { + const avatarEventContent = await api.getStateEvent(roomID, "m.room.avatar", "") + if (avatarEventContent.url !== newAvatarState.url) { + await api.sendState(roomID, "m.room.avatar", "", newAvatarState) + } + } + } + + return spaceID } From c4bc07986510af811bfb6f40f0fedd39017f82db Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 25 Aug 2023 17:35:34 +1200 Subject: [PATCH 87/99] bug fix :( --- d2m/actions/create-space.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index 838bef9..87e25eb 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -97,7 +97,7 @@ async function syncSpace(guildID) { const roomsWithCustomAvatars = db.prepare("SELECT room_id FROM channel_room WHERE custom_avatar IS NOT NULL").pluck().all() const childRooms = ks.kstateToState(spaceKState).filter(({type, state_key, content}) => { - return type === "m.space.child" && "via" in content && roomsWithCustomAvatars.includes(state_key) + return type === "m.space.child" && "via" in content && !roomsWithCustomAvatars.includes(state_key) }).map(({state_key}) => state_key) for (const roomID of childRooms) { @@ -108,7 +108,6 @@ async function syncSpace(guildID) { } } - return spaceID } From 27b8c547e3a71b30fb7f7d781635931d2c9b2098 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 25 Aug 2023 23:27:44 +1200 Subject: [PATCH 88/99] updating tap-dot for prettier output --- d2m/actions/create-room.js | 1 + package-lock.json | 17 +++++++++-------- package.json | 2 +- types.d.ts | 4 ++-- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index 2095130..e778d22 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -283,6 +283,7 @@ async function _unbridgeRoom(channelID) { assert.ok(spaceID) // remove room from being a space member + await api.sendState(roomID, "m.space.parent", spaceID, {}) await api.sendState(spaceID, "m.space.child", roomID, {}) // send a notification in the room diff --git a/package-lock.json b/package-lock.json index e808e1f..4d32bf5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "cross-env": "^7.0.3", "discord-api-types": "^0.37.53", "supertape": "^8.3.0", - "tap-dot": "github:cloudrac3r/tap-dot#223a4e67a6f7daf015506a12a7af74605f06c7f4" + "tap-dot": "github:cloudrac3r/tap-dot#9dd7750ececeae3a96afba91905be812b6b2cc2d" } }, "node_modules/@babel/runtime": { @@ -2518,15 +2518,16 @@ "dev": true }, "node_modules/readable-stream": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.3.0.tgz", - "integrity": "sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", "dev": true, "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", - "process": "^0.11.10" + "process": "^0.11.10", + "string_decoder": "^1.3.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -3043,8 +3044,8 @@ }, "node_modules/tap-dot": { "version": "2.0.0", - "resolved": "git+ssh://git@github.com/cloudrac3r/tap-dot.git#223a4e67a6f7daf015506a12a7af74605f06c7f4", - "integrity": "sha512-nhpVoX/s4IJJdm7OymbZ1rdZNlqt3l/yQ9Z9if06jcgRNto6QAZOrLIvdCILYQ6GE0mu+cyVA8s24amdwbvHiQ==", + "resolved": "git+ssh://git@github.com/cloudrac3r/tap-dot.git#9dd7750ececeae3a96afba91905be812b6b2cc2d", + "integrity": "sha512-SLg6KF3cSkKII+5hA/we9FjnMCrL5uk0wYap7RXD9KJziy7xqZolvEOamt3CJlm5LSzRXIGblm3nmhY/EBE3AA==", "dev": true, "license": "MIT", "dependencies": { @@ -3058,7 +3059,7 @@ "node_modules/tap-out": { "version": "3.2.1", "resolved": "git+ssh://git@github.com/cloudrac3r/tap-out.git#1b4ec6084aedb9f44ccaa0c7185ff9bfd83da771", - "integrity": "sha512-hyMMeN6jagEyeEOq7Xyg3GNIAR3iUDDocaoK5QRPjnEGbFZOYJ39Dkn7BsFUXyGVl+s4b3zPkDcTS38+6KTXCQ==", + "integrity": "sha512-55eUSaX5AeEOqJMRlj9XSqUlLV/yYPOPeC3kOFqjmorq6/jlH5kIeqpgLNW5PlPEAuggzYREYYXqrN8E37ZPfQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 67aeade..155bf2e 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "cross-env": "^7.0.3", "discord-api-types": "^0.37.53", "supertape": "^8.3.0", - "tap-dot": "github:cloudrac3r/tap-dot#223a4e67a6f7daf015506a12a7af74605f06c7f4" + "tap-dot": "github:cloudrac3r/tap-dot#9dd7750ececeae3a96afba91905be812b6b2cc2d" }, "scripts": { "test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap test/test.js | tap-dot", diff --git a/types.d.ts b/types.d.ts index 2ba8d1d..e9b8a7a 100644 --- a/types.d.ts +++ b/types.d.ts @@ -69,8 +69,8 @@ export namespace Event { export type M_Room_Message = { msgtype: "m.text" body: string - formatted_body?: "org.matrix.custom.html" - format?: string + format?: "org.matrix.custom.html" + formatted_body?: string } export type M_Room_Member = { From 8c4e16e255fcb75da55696b9597fb20fccc64b89 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 26 Aug 2023 01:43:17 +1200 Subject: [PATCH 89/99] add turndown for m->d formatting --- m2d/converters/event-to-message.js | 79 ++++++++-- m2d/converters/event-to-message.test.js | 191 ++++++++++++++++++++++++ package-lock.json | 37 ++++- package.json | 4 +- types.d.ts | 2 +- 5 files changed, 297 insertions(+), 16 deletions(-) diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index dde77b7..f793f85 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -2,19 +2,41 @@ const Ty = require("../../types") const DiscordTypes = require("discord-api-types/v10") -const markdown = require("discord-markdown") +const chunk = require("chunk-text") +const TurndownService = require("turndown") const passthrough = require("../../passthrough") const { sync, db, discord } = passthrough /** @type {import("../../matrix/file")} */ const file = sync.require("../../matrix/file") +// https://github.com/mixmark-io/turndown/blob/97e4535ca76bb2e70d9caa2aa4d4686956b06d44/src/utilities.js#L26C28-L33C2 +const BLOCK_ELEMENTS = [ + "ADDRESS", "ARTICLE", "ASIDE", "AUDIO", "BLOCKQUOTE", "BODY", "CANVAS", + "CENTER", "DD", "DETAILS", "DIR", "DIV", "DL", "DT", "FIELDSET", "FIGCAPTION", "FIGURE", + "FOOTER", "FORM", "FRAMESET", "H1", "H2", "H3", "H4", "H5", "H6", "HEADER", + "HGROUP", "HR", "HTML", "ISINDEX", "LI", "MAIN", "MENU", "NAV", "NOFRAMES", + "NOSCRIPT", "OL", "OUTPUT", "P", "PRE", "SECTION", "SUMMARY", "TABLE", "TBODY", "TD", + "TFOOT", "TH", "THEAD", "TR", "UL" +] + +const turndownService = new TurndownService({ + hr: "----" +}) + +turndownService.addRule("strikethrough", { + filter: ["del", "s", "strike"], + replacement: function (content) { + return "~~" + content + "~~" + } +}) + /** * @param {Ty.Event.Outer} event */ function eventToMessage(event) { /** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]})[]} */ - const messages = [] + let messages = [] let displayName = event.sender let avatarURL = undefined @@ -24,20 +46,51 @@ function eventToMessage(event) { // TODO: get the media repo domain and the avatar url from the matrix member event } - if (event.content.msgtype === "m.text") { - messages.push({ - content: event.content.body, - username: displayName, - avatar_url: avatarURL - }) - } else if (event.content.msgtype === "m.emote") { - messages.push({ - content: `\* _${displayName} ${event.content.body}_`, - username: displayName, - avatar_url: avatarURL + // Convert content depending on what the message is + let content = event.content.body // ultimate fallback + if (event.content.format === "org.matrix.custom.html" && event.content.formatted_body) { + let input = event.content.formatted_body + if (event.content.msgtype === "m.emote") { + input = `* ${displayName} ${input}` + } + + // Note: Element's renderers on Web and Android currently collapse whitespace, like the browser does. Turndown also collapses whitespace which is good for me. + // If later I'm using a client that doesn't collapse whitespace and I want turndown to follow suit, uncomment the following line of code, and it Just Works: + // input = input.replace(/ /g, " ") + // There is also a corresponding test to uncomment, named "event2message: whitespace is retained" + + // The matrix spec hasn't decided whether \n counts as a newline or not, but I'm going to count it, because if it's in the data it's there for a reason. + // But I should not count it if it's between block elements. + input = input.replace(/(<\/?([^ >]+)[^>]*>)?\n(<\/?([^ >]+)[^>]*>)?/g, (whole, beforeContext, beforeTag, afterContext, afterTag) => { + if (typeof beforeTag !== "string" && typeof afterTag !== "string") { + return "
" + } + beforeContext = beforeContext || "" + beforeTag = beforeTag || "" + afterContext = afterContext || "" + afterTag = afterTag || "" + if (!BLOCK_ELEMENTS.includes(beforeTag.toUpperCase()) && !BLOCK_ELEMENTS.includes(afterTag.toUpperCase())) { + return beforeContext + "
" + afterContext + } else { + return whole + } }) + + // @ts-ignore + content = turndownService.turndown(input) + + // It's optimised for commonmark, we need to replace the space-space-newline with just newline + content = content.replace(/ \n/g, "\n") } + // Split into 2000 character chunks + const chunks = chunk(content, 2000) + messages = messages.concat(chunks.map(content => ({ + content, + username: displayName, + avatar_url: avatarURL + }))) + return messages } diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index a45c23b..ac62bf3 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -4,6 +4,12 @@ const {test} = require("supertape") const {eventToMessage} = require("./event-to-message") const data = require("../../test/data") +function sameFirstContentAndWhitespace(t, a, b) { + const a2 = JSON.stringify(a[0].content) + const b2 = JSON.stringify(b[0].content) + t.equal(a2, b2) +} + test("event2message: janky test", t => { t.deepEqual( eventToMessage({ @@ -28,6 +34,165 @@ test("event2message: janky test", t => { ) }) +test("event2message: basic html is converted to markdown", t => { + t.deepEqual( + eventToMessage({ + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: "this is a test of formatting" + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }), + [{ + username: "cadence", + content: "this **is** a **_test_** of ~~formatting~~", + avatar_url: undefined + }] + ) +}) + +test("event2message: markdown syntax is escaped", t => { + t.deepEqual( + eventToMessage({ + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: "this **is** an extreme \\*test\\* of" + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }), + [{ + username: "cadence", + content: "this \\*\\*is\\*\\* an **_extreme_** \\\\\\*test\\\\\\* of", + avatar_url: undefined + }] + ) +}) + +test("event2message: html lines are bridged correctly", t => { + t.deepEqual( + eventToMessage({ + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: "

paragraph one
line two
line three

paragraph two\nline two\nline three\n\nparagraph three

paragraph four\nline two
line three\nline four

paragraph five" + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }), + [{ + username: "cadence", + content: "paragraph one\nline _two_\nline three\n\nparagraph two\nline _two_\nline three\n\nparagraph three\n\nparagraph four\nline two\nline three\nline four\n\nparagraph five", + avatar_url: undefined + }] + ) +}) + +/*test("event2message: whitespace is retained", t => { + t.deepEqual( + eventToMessage({ + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: "line one: test test
line two: test test
line three: test test
line four: test test
line five" + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }), + [{ + username: "cadence", + content: "line one: test test\nline two: **test** **test**\nline three: **test test**\nline four: test test\n line five", + avatar_url: undefined + }] + ) +})*/ + +test("event2message: whitespace is collapsed", t => { + sameFirstContentAndWhitespace( + t, + eventToMessage({ + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: "line one: test test
line two: test test
line three: test test
line four: test test
line five" + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }), + [{ + username: "cadence", + content: "line one: test test\nline two: **test** **test**\nline three: **test test**\nline four: test test\nline five", + avatar_url: undefined + }] + ) +}) + +test("event2message: lists are bridged correctly", t => { + sameFirstContentAndWhitespace( + t, + eventToMessage({ + "type": "m.room.message", + "sender": "@cadence:cadence.moe", + "content": { + "msgtype": "m.text", + "body": "* line one\n* line two\n* line three\n * nested one\n * nested two\n* line four", + "format": "org.matrix.custom.html", + "formatted_body": "
    \n
  • line one
  • \n
  • line two
  • \n
  • line three\n
      \n
    • nested one
    • \n
    • nested two
    • \n
    \n
  • \n
  • line four
  • \n
\n" + }, + "origin_server_ts": 1692967314062, + "unsigned": { + "age": 112, + "transaction_id": "m1692967313951.441" + }, + "event_id": "$l-xQPY5vNJo3SNxU9d8aOWNVD1glMslMyrp4M_JEF70", + "room_id": "!BpMdOUkWWhFxmTrENV:cadence.moe" + }), + [{ + username: "cadence", + content: "* line one\n* line two\n* line three\n * nested one\n * nested two\n* line four", + avatar_url: undefined + }] + ) +}) + test("event2message: long messages are split", t => { t.deepEqual( eventToMessage({ @@ -55,3 +220,29 @@ test("event2message: long messages are split", t => { }] ) }) + +test("event2message: m.emote markdown syntax is escaped", t => { + t.deepEqual( + eventToMessage({ + content: { + msgtype: "m.emote", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: "shows you **her** extreme \\*test\\* of" + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }), + [{ + username: "cadence", + content: "\\* cadence shows you \\*\\*her\\*\\* **_extreme_** \\\\\\*test\\\\\\* of", + avatar_url: undefined + }] + ) +}) diff --git a/package-lock.json b/package-lock.json index 4d32bf5..dfd21ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "better-sqlite3": "^8.3.0", + "chunk-text": "^2.0.1", "cloudstorm": "^0.8.0", "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#440130ef343c8183a81c7c09809731484aa3a182", "heatsync": "^2.4.1", @@ -20,7 +21,8 @@ "node-fetch": "^2.6.7", "prettier-bytes": "^1.0.4", "snowtransfer": "^0.8.0", - "try-to-catch": "^3.0.1" + "try-to-catch": "^3.0.1", + "turndown": "^7.1.2" }, "devDependencies": { "@types/node": "^18.16.0", @@ -732,6 +734,18 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, + "node_modules/chunk-text": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/chunk-text/-/chunk-text-2.0.1.tgz", + "integrity": "sha512-ER6TSpe2DT4wjOVOKJ3FFAYv7wE77HA/Ztz88Peiv3lq/2oVMsItYJJsVVI0xNZM8cdImOOTNqlw+LQz7gYdJg==", + "dependencies": { + "runes": "^0.4.3" + }, + "bin": { + "chunk": "bin/server.js", + "chunk-text": "bin/server.js" + } + }, "node_modules/ci-info": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", @@ -1057,6 +1071,11 @@ "simple-markdown": "^0.7.2" } }, + "node_modules/domino": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz", + "integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2646,6 +2665,14 @@ "node": "*" } }, + "node_modules/runes": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/runes/-/runes-0.4.3.tgz", + "integrity": "sha512-K6p9y4ZyL9wPzA+PMDloNQPfoDGTiFYDvdlXznyGKgD10BJpcAosvATKrExRKOrNLgD8E7Um7WGW0lxsnOuNLg==", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -3216,6 +3243,14 @@ "node": "*" } }, + "node_modules/turndown": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.1.2.tgz", + "integrity": "sha512-ntI9R7fcUKjqBP6QU8rBK2Ehyt8LAzt3UBT9JR9tgo6GtuKvyUzpayWmeMKJw1DPdXzktvtIT8m2mVXz+bL/Qg==", + "dependencies": { + "domino": "^2.1.6" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", diff --git a/package.json b/package.json index 155bf2e..238b9ae 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "license": "MIT", "dependencies": { "better-sqlite3": "^8.3.0", + "chunk-text": "^2.0.1", "cloudstorm": "^0.8.0", "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#440130ef343c8183a81c7c09809731484aa3a182", "heatsync": "^2.4.1", @@ -26,7 +27,8 @@ "node-fetch": "^2.6.7", "prettier-bytes": "^1.0.4", "snowtransfer": "^0.8.0", - "try-to-catch": "^3.0.1" + "try-to-catch": "^3.0.1", + "turndown": "^7.1.2" }, "devDependencies": { "@types/node": "^18.16.0", diff --git a/types.d.ts b/types.d.ts index e9b8a7a..badbbab 100644 --- a/types.d.ts +++ b/types.d.ts @@ -67,7 +67,7 @@ export namespace Event { } export type M_Room_Message = { - msgtype: "m.text" + msgtype: "m.text" | "m.emote" body: string format?: "org.matrix.custom.html" formatted_body?: string From 7780ee806aa1d0b291826cd204814aeb3ff2568e Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 26 Aug 2023 01:44:50 +1200 Subject: [PATCH 90/99] 100% edit-to-changes code coverage --- d2m/actions/edit-message.js | 2 +- d2m/converters/edit-to-changes.js | 4 +-- d2m/converters/edit-to-changes.test.js | 26 ++++++++++++++++ db/data-for-test.sql | 5 +++- test/data.js | 41 ++++++++++++++++++++++++++ 5 files changed, 74 insertions(+), 4 deletions(-) diff --git a/d2m/actions/edit-message.js b/d2m/actions/edit-message.js index fa152cf..d7cb81d 100644 --- a/d2m/actions/edit-message.js +++ b/d2m/actions/edit-message.js @@ -24,7 +24,7 @@ async function editMessage(message, guild) { await api.sendEvent(roomID, eventType, newContentWithoutType, senderMxid) // Ensure the database is up to date. // The columns are event_id, event_type, event_subtype, message_id, channel_id, part, source. Only event_subtype could potentially be changed by a replacement event. - const subtype = newContentWithoutType.msgtype ?? null + const subtype = newContentWithoutType.msgtype || null db.prepare("UPDATE event_message SET event_subtype = ? WHERE event_id = ?").run(subtype, oldID) } diff --git a/d2m/converters/edit-to-changes.js b/d2m/converters/edit-to-changes.js index 3f4b2d2..814bb97 100644 --- a/d2m/converters/edit-to-changes.js +++ b/d2m/converters/edit-to-changes.js @@ -23,7 +23,7 @@ async function editToChanges(message, guild, api) { const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(message.channel_id) /** @type {string?} */ - let senderMxid = db.prepare("SELECT mxid FROM sim WHERE discord_id = ?").pluck().get(message.author.id) ?? null + let senderMxid = db.prepare("SELECT mxid FROM sim WHERE discord_id = ?").pluck().get(message.author.id) || null if (senderMxid) { const senderIsInRoom = db.prepare("SELECT * FROM sim_member WHERE room_id = ? and mxid = ?").get(roomID, senderMxid) if (!senderIsInRoom) { @@ -66,7 +66,7 @@ async function editToChanges(message, guild, api) { // Find a new event to pair it with... for (let i = 0; i < oldEventRows.length; i++) { const olde = oldEventRows[i] - if (olde.event_type === newe.$type && olde.event_subtype === (newe.msgtype ?? null)) { // The spec does allow subtypes to change, so I can change this condition later if I want to + if (olde.event_type === newe.$type && olde.event_subtype === (newe.msgtype || null)) { // The spec does allow subtypes to change, so I can change this condition later if I want to // Found one! // Set up the pairing eventsToReplace.push({ diff --git a/d2m/converters/edit-to-changes.test.js b/d2m/converters/edit-to-changes.test.js index 674cb15..022b396 100644 --- a/d2m/converters/edit-to-changes.test.js +++ b/d2m/converters/edit-to-changes.test.js @@ -103,6 +103,32 @@ test("edit2changes: add caption back to that image", async t => { t.deepEqual(eventsToReplace, []) }) +test("edit2changes: stickers and attachments are not changed, only the content can be edited", async t => { + const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.edited_content_with_sticker_and_attachments, data.guild.general, {}) + t.deepEqual(eventsToRedact, []) + t.deepEqual(eventsToSend, []) + t.deepEqual(eventsToReplace, [{ + oldID: "$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qdFv4", + newContent: { + $type: "m.room.message", + msgtype: "m.text", + body: "* only the content can be edited", + "m.mentions": {}, + // *** Replaced With: *** + "m.new_content": { + msgtype: "m.text", + body: "only the content can be edited", + "m.mentions": {} + }, + "m.relates_to": { + rel_type: "m.replace", + event_id: "$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qdFv4" + } + } + }]) +}) + + 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, []) diff --git a/db/data-for-test.sql b/db/data-for-test.sql index e88b967..7148b19 100644 --- a/db/data-for-test.sql +++ b/db/data-for-test.sql @@ -83,7 +83,10 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, chan ('$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M', 'm.room.message', 'm.text', '1128084851279536279', '112760669178241024', 0, 1), ('$YUJFa5j0ZJe7PUvD2DykRt9g51RoadUEYmuJLdSEbJ0', 'm.room.message', 'm.image', '1128084851279536279', '112760669178241024', 1, 1), ('$oLyUTyZ_7e_SUzGNWZKz880ll9amLZvXGbArJCKai2Q', 'm.room.message', 'm.text', '1128084748338741392', '112760669178241024', 0, 1), -('$FchUVylsOfmmbj-VwEs5Z9kY49_dt2zd0vWfylzy5Yo', 'm.room.message', 'm.text', '1143121514925928541', '1100319550446252084', 0, 1); +('$FchUVylsOfmmbj-VwEs5Z9kY49_dt2zd0vWfylzy5Yo', 'm.room.message', 'm.text', '1143121514925928541', '1100319550446252084', 0, 1), +('$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qdFv4', 'm.room.message', 'm.text', '1106366167788044450', '122155380120748034', 0, 1), +('$Ijf1MFCD39ktrNHxrA-i2aKoRWNYdAV2ZXYQeiZIgEU', 'm.room.message', 'm.image', '1106366167788044450', '122155380120748034', 0, 0), +('$f9cjKiacXI9qPF_nUAckzbiKnJEi0LM399kOkhdd8f8', 'm.sticker', NULL, '1106366167788044450', '122155380120748034', 0, 0); INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'), diff --git a/test/data.js b/test/data.js index a4d836d..32ee3b0 100644 --- a/test/data.js +++ b/test/data.js @@ -1250,6 +1250,47 @@ module.exports = { tts: false, type: 0 }, + edited_content_with_sticker_and_attachments: { + id: "1106366167788044450", + type: 0, + content: "only the content can be edited", + 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: "2023-05-11T23:44:19.690000+00:00", + flags: 0, + components: [], + sticker_items: [{ + id: "1106323941183717586", + format_type: 1, + name: "pomu puff" + }] + }, edit_of_reply_to_skull_webp_attachment_with_content: { type: 19, tts: false, From 88eb8c22904e1cc4d2fae57f1e34d2f58a35f0cf Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 26 Aug 2023 02:04:49 +1200 Subject: [PATCH 91/99] configure turndown for code blocks --- m2d/converters/event-to-message.js | 10 ++++- m2d/converters/event-to-message.test.js | 57 +++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index f793f85..7893799 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -10,7 +10,6 @@ const { sync, db, discord } = passthrough /** @type {import("../../matrix/file")} */ const file = sync.require("../../matrix/file") -// https://github.com/mixmark-io/turndown/blob/97e4535ca76bb2e70d9caa2aa4d4686956b06d44/src/utilities.js#L26C28-L33C2 const BLOCK_ELEMENTS = [ "ADDRESS", "ARTICLE", "ASIDE", "AUDIO", "BLOCKQUOTE", "BODY", "CANVAS", "CENTER", "DD", "DETAILS", "DIR", "DIV", "DL", "DT", "FIELDSET", "FIGCAPTION", "FIGURE", @@ -21,7 +20,10 @@ const BLOCK_ELEMENTS = [ ] const turndownService = new TurndownService({ - hr: "----" + hr: "----", + headingStyle: "atx", + preformattedCode: true, + codeBlockStyle: "fenced" }) turndownService.addRule("strikethrough", { @@ -81,6 +83,10 @@ function eventToMessage(event) { // It's optimised for commonmark, we need to replace the space-space-newline with just newline content = content.replace(/ \n/g, "\n") + } else { + // Looks like we're using the plaintext body! + // Markdown needs to be escaped + content = content.replace(/([*_~`#])/g, `\\$1`) } // Split into 2000 character chunks diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index ac62bf3..7aefdb1 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -10,11 +10,11 @@ function sameFirstContentAndWhitespace(t, a, b) { t.equal(a2, b2) } -test("event2message: janky test", t => { +test("event2message: body is used when there is no formatted_body", t => { t.deepEqual( eventToMessage({ content: { - body: "test", + body: "testing plaintext", msgtype: "m.text" }, event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", @@ -28,7 +28,31 @@ test("event2message: janky test", t => { }), [{ username: "cadence", - content: "test", + content: "testing plaintext", + avatar_url: undefined + }] + ) +}) + +test("event2message: any markdown in body is escaped", t => { + t.deepEqual( + eventToMessage({ + content: { + body: "testing **special** ~~things~~ which _should_ *not* `trigger` @any ", + msgtype: "m.text" + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }), + [{ + username: "cadence", + content: "testing \\*\\*special\\*\\* \\~\\~things\\~\\~ which \\_should\\_ \\*not\\* \\`trigger\\` @any ", avatar_url: undefined }] ) @@ -221,6 +245,33 @@ test("event2message: long messages are split", t => { ) }) +test("event2message: code blocks work", t => { + t.deepEqual( + eventToMessage({ + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: "

preceding

\n
code block\n
\n

following code is inline

\n" + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }), + [{ + username: "cadence", + content: "preceding\n\n```\ncode block\n```\n\nfollowing `code` is inline", + avatar_url: undefined + }] + ) +}) + + test("event2message: m.emote markdown syntax is escaped", t => { t.deepEqual( eventToMessage({ From df651241a54b4429b51db88664e946901a89f986 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 26 Aug 2023 18:58:41 +1200 Subject: [PATCH 92/99] store room name changes to nick in db --- m2d/event-dispatcher.js | 11 +++++++++++ types.d.ts | 11 ++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/m2d/event-dispatcher.js b/m2d/event-dispatcher.js index c62d805..3425fdb 100644 --- a/m2d/event-dispatcher.js +++ b/m2d/event-dispatcher.js @@ -80,3 +80,14 @@ async event => { const url = event.content.url || null db.prepare("UPDATE channel_room SET custom_avatar = ? WHERE room_id = ?").run(url, event.room_id) })) + +sync.addTemporaryListener(as, "type:m.room.name", guard("m.room.name", +/** + * @param {Ty.Event.StateOuter} event + */ +async event => { + if (event.state_key !== "") return + if (utils.eventSenderIsFromDiscord(event.sender)) return + const name = event.content.name || null + db.prepare("UPDATE channel_room SET nick = ? WHERE room_id = ?").run(name, event.room_id) +})) diff --git a/types.d.ts b/types.d.ts index badbbab..2bf0af0 100644 --- a/types.d.ts +++ b/types.d.ts @@ -70,7 +70,12 @@ export namespace Event { msgtype: "m.text" | "m.emote" body: string format?: "org.matrix.custom.html" - formatted_body?: string + formatted_body?: string, + "m.relates_to"?: { + "m.in_reply_to": { + event_id: string + } + } } export type M_Room_Member = { @@ -84,6 +89,10 @@ export namespace Event { url?: string } + export type M_Room_Name = { + name?: string + } + export type M_Reaction = { "m.relates_to": { rel_type: "m.annotation" From 39fb4465f6861de3dc98d638c14c424616f5bf6a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 26 Aug 2023 19:07:19 +1200 Subject: [PATCH 93/99] fix m->d formatting of quotes and code --- m2d/converters/event-to-message.js | 38 ++++++++++++++++++ m2d/converters/event-to-message.test.js | 52 +++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index 7893799..f4382a3 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -33,6 +33,40 @@ turndownService.addRule("strikethrough", { } }) +turndownService.addRule("blockquote", { + filter: "blockquote", + replacement: function (content) { + content = content.replace(/^\n+|\n+$/g, "") + content = content.replace(/^/gm, "> ") + return content + } +}) + +turndownService.addRule("fencedCodeBlock", { + filter: function (node, options) { + return ( + options.codeBlockStyle === "fenced" && + node.nodeName === "PRE" && + node.firstChild && + node.firstChild.nodeName === "CODE" + ) + }, + replacement: function (content, node, options) { + const className = node.firstChild.getAttribute("class") || "" + const language = (className.match(/language-(\S+)/) || [null, ""])[1] + const code = node.firstChild + const visibleCode = code.childNodes.map(c => c.nodeName === "BR" ? "\n" : c.textContent).join("").replace(/\n*$/g, "") + + var fence = "```" + + return ( + fence + language + "\n" + + visibleCode + + "\n" + fence + ) + } +}) + /** * @param {Ty.Event.Outer} event */ @@ -61,9 +95,13 @@ function eventToMessage(event) { // input = input.replace(/ /g, " ") // There is also a corresponding test to uncomment, named "event2message: whitespace is retained" + // Element adds a bunch of
before but doesn't render them. I can't figure out how this works, so let's just delete those. + input = input.replace(/(?:\n|
\s*)*<\/blockquote>/g, "") + // The matrix spec hasn't decided whether \n counts as a newline or not, but I'm going to count it, because if it's in the data it's there for a reason. // But I should not count it if it's between block elements. input = input.replace(/(<\/?([^ >]+)[^>]*>)?\n(<\/?([^ >]+)[^>]*>)?/g, (whole, beforeContext, beforeTag, afterContext, afterTag) => { + // console.error(beforeContext, beforeTag, afterContext, afterTag) if (typeof beforeTag !== "string" && typeof afterTag !== "string") { return "
" } diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index 7aefdb1..afa40de 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -271,6 +271,58 @@ test("event2message: code blocks work", t => { ) }) +test("event2message: code block contents are formatted correctly and not escaped", t => { + t.deepEqual( + eventToMessage({ + "type": "m.room.message", + "sender": "@cadence:cadence.moe", + "content": { + "msgtype": "m.text", + "body": "wrong body", + "format": "org.matrix.custom.html", + "formatted_body": "
input = input.replace(/(<\\/?([^ >]+)[^>]*>)?\\n(<\\/?([^ >]+)[^>]*>)?/g,\n_input_ = input = input.replace(/(<\\/?([^ >]+)[^>]*>)?\\n(<\\/?([^ >]+)[^>]*>)?/g,\n
\n

input = input.replace(/(<\\/?([^ >]+)[^>]*>)?\\n(<\\/?([^ >]+)[^>]*>)?/g,

\n" + }, + "origin_server_ts": 1693031482275, + "unsigned": { + "age": 99, + "transaction_id": "m1693031482146.511" + }, + "event_id": "$pGkWQuGVmrPNByrFELxhzI6MCBgJecr5I2J3z88Gc2s", + "room_id": "!BpMdOUkWWhFxmTrENV:cadence.moe" + }), + [{ + username: "cadence", + content: "```\ninput = input.replace(/(<\\/?([^ >]+)[^>]*>)?\\n(<\\/?([^ >]+)[^>]*>)?/g,\n_input_ = input = input.replace(/(<\\/?([^ >]+)[^>]*>)?\\n(<\\/?([^ >]+)[^>]*>)?/g,\n```\n\n`input = input.replace(/(<\\/?([^ >]+)[^>]*>)?\\n(<\\/?([^ >]+)[^>]*>)?/g,`", + avatar_url: undefined + }] + ) +}) + +test("event2message: quotes have an appropriate amount of whitespace", t => { + t.deepEqual( + eventToMessage({ + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: "
Chancellor of Germany Angela Merkel, on March 17, 2017: they did not shake hands



🤨" + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }), + [{ + username: "cadence", + content: "> Chancellor of Germany Angela Merkel, on March 17, 2017: they did not shake hands\n🤨", + avatar_url: undefined + }] + ) +}) test("event2message: m.emote markdown syntax is escaped", t => { t.deepEqual( From 0ea2b4efc90b4f215a948654ae343a5f61d8bdf9 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 26 Aug 2023 20:30:22 +1200 Subject: [PATCH 94/99] m->d rich replies --- db/data-for-test.sql | 6 +- m2d/actions/send-event.js | 8 +- m2d/converters/event-to-message.js | 37 +++- m2d/converters/event-to-message.test.js | 213 ++++++++++++++++++++---- 4 files changed, 231 insertions(+), 33 deletions(-) diff --git a/db/data-for-test.sql b/db/data-for-test.sql index 7148b19..b8da73c 100644 --- a/db/data-for-test.sql +++ b/db/data-for-test.sql @@ -67,7 +67,8 @@ INSERT INTO sim (discord_id, sim_name, localpart, mxid) VALUES ('820865262526005258', 'crunch_god', '_ooye_crunch_god', '@_ooye_crunch_god:cadence.moe'), ('771520384671416320', 'bojack_horseman', '_ooye_bojack_horseman', '@_ooye_bojack_horseman:cadence.moe'), ('112890272819507200', '.wing.', '_ooye_.wing.', '@_ooye_.wing.:cadence.moe'), -('114147806469554185', 'extremity', '_ooye_extremity', '@_ooye_extremity:cadence.moe'); +('114147806469554185', 'extremity', '_ooye_extremity', '@_ooye_extremity:cadence.moe'), +('111604486476181504', 'kyuugryphon', '_ooye_kyuugryphon', '@_ooye_kyuugryphon:cadence.moe');; INSERT INTO sim_member (mxid, room_id, profile_event_content_hash) VALUES ('@_ooye_bojack_horseman:cadence.moe', '!uCtjHhfGlYbVnPVlkG:cadence.moe', NULL); @@ -86,7 +87,8 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, chan ('$FchUVylsOfmmbj-VwEs5Z9kY49_dt2zd0vWfylzy5Yo', 'm.room.message', 'm.text', '1143121514925928541', '1100319550446252084', 0, 1), ('$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qdFv4', 'm.room.message', 'm.text', '1106366167788044450', '122155380120748034', 0, 1), ('$Ijf1MFCD39ktrNHxrA-i2aKoRWNYdAV2ZXYQeiZIgEU', 'm.room.message', 'm.image', '1106366167788044450', '122155380120748034', 0, 0), -('$f9cjKiacXI9qPF_nUAckzbiKnJEi0LM399kOkhdd8f8', 'm.sticker', NULL, '1106366167788044450', '122155380120748034', 0, 0); +('$f9cjKiacXI9qPF_nUAckzbiKnJEi0LM399kOkhdd8f8', 'm.sticker', NULL, '1106366167788044450', '122155380120748034', 0, 0), +('$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04', 'm.room.message', 'm.text', '1144865310588014633', '687028734322147344', 0, 1); INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'), diff --git a/m2d/actions/send-event.js b/m2d/actions/send-event.js index 39eed22..016768e 100644 --- a/m2d/actions/send-event.js +++ b/m2d/actions/send-event.js @@ -9,6 +9,8 @@ const {sync, discord, db} = passthrough const channelWebhook = sync.require("./channel-webhook") /** @type {import("../converters/event-to-message")} */ const eventToMessage = sync.require("../converters/event-to-message") +/** @type {import("../../matrix/api")}) */ +const api = sync.require("../../matrix/api") /** @param {import("../../types").Event.Outer} event */ async function sendEvent(event) { @@ -20,10 +22,14 @@ async function sendEvent(event) { threadID = channelID channelID = row.thread_parent // it's the thread's parent... get with the times... } + // @ts-ignore + const guildID = discord.channels.get(channelID).guild_id + const guild = discord.guilds.get(guildID) + assert(guild) // no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it - const messages = eventToMessage.eventToMessage(event) + const messages = await eventToMessage.eventToMessage(event, guild, {api}) assert(Array.isArray(messages)) // sanity /** @type {DiscordTypes.APIMessage[]} */ diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index f4382a3..cf34705 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -26,6 +26,8 @@ const turndownService = new TurndownService({ codeBlockStyle: "fenced" }) +turndownService.remove("mx-reply") + turndownService.addRule("strikethrough", { filter: ["del", "s", "strike"], replacement: function (content) { @@ -69,13 +71,16 @@ turndownService.addRule("fencedCodeBlock", { /** * @param {Ty.Event.Outer} event + * @param {import("discord-api-types/v10").APIGuild} guild + * @param {{api: import("../../matrix/api")}} di simple-as-nails dependency injection for the matrix API */ -function eventToMessage(event) { +async function eventToMessage(event, guild, di) { /** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]})[]} */ let messages = [] let displayName = event.sender let avatarURL = undefined + let replyLine = "" const match = event.sender.match(/^@(.*?):/) if (match) { displayName = match[1] @@ -95,7 +100,33 @@ function eventToMessage(event) { // input = input.replace(/ /g, " ") // There is also a corresponding test to uncomment, named "event2message: whitespace is retained" - // Element adds a bunch of
before but doesn't render them. I can't figure out how this works, so let's just delete those. + // Handling replies. We'll look up the data of the replied-to event from the Matrix homeserver. + await (async () => { + const repliedToEventId = event.content["m.relates_to"]?.["m.in_reply_to"].event_id + if (!repliedToEventId) return + const repliedToEvent = await di.api.getEvent(event.room_id, repliedToEventId) + if (!repliedToEvent) return + const row = db.prepare("SELECT channel_id, message_id FROM event_message WHERE event_id = ? ORDER BY part").get(repliedToEventId) + if (row) { + replyLine = `<:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/${guild.id}/${row.channel_id}/${row.message_id} ` + } else { + replyLine = `<:L1:1144820033948762203><:L2:1144820084079087647>` + } + const sender = repliedToEvent.sender + const senderName = sender.match(/@([^:]*)/)?.[1] || sender + const authorID = db.prepare("SELECT discord_id FROM sim WHERE mxid = ?").pluck().get(repliedToEvent.sender) + if (authorID) { + replyLine += `<@${authorID}>: ` + } else { + replyLine += `Ⓜ️**${senderName}**: ` + } + const repliedToContent = repliedToEvent.content.formatted_body || repliedToEvent.content.body + const contentPreviewChunks = chunk(repliedToContent.replace(/.*<\/mx-reply>/, "").replace(/(?:\n|
)+/g, " ").replace(/<[^>]+>/g, ""), 24) + const contentPreview = contentPreviewChunks.length > 1 ? contentPreviewChunks[0] + "..." : contentPreviewChunks[0] + replyLine += contentPreview + "\n" + })() + + // Element adds a bunch of
before but doesn't render them. I can't figure out how this even works in the browser, so let's just delete those. input = input.replace(/(?:\n|
\s*)*<\/blockquote>/g, "") // The matrix spec hasn't decided whether \n counts as a newline or not, but I'm going to count it, because if it's in the data it's there for a reason. @@ -127,6 +158,8 @@ function eventToMessage(event) { content = content.replace(/([*_~`#])/g, `\\$1`) } + content = replyLine + content + // Split into 2000 character chunks const chunks = chunk(content, 2000) messages = messages.concat(chunks.map(content => ({ diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index afa40de..33c4d71 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -1,18 +1,42 @@ -// @ts-check - const {test} = require("supertape") const {eventToMessage} = require("./event-to-message") const data = require("../../test/data") +/** + * @param {string} roomID + * @param {string} eventID + * @returns {(roomID: string, eventID: string) => Promise>} + */ +function mockGetEvent(t, roomID_in, eventID_in, outer) { + return async function(roomID, eventID) { + t.equal(roomID, roomID_in) + t.equal(eventID, eventID_in) + return new Promise(resolve => { + setTimeout(() => { + resolve({ + event_id: eventID_in, + room_id: roomID_in, + origin_server_ts: 1680000000000, + unsigned: { + age: 2245, + transaction_id: "$local.whatever" + }, + ...outer + }) + }) + }) + } +} + function sameFirstContentAndWhitespace(t, a, b) { const a2 = JSON.stringify(a[0].content) const b2 = JSON.stringify(b[0].content) t.equal(a2, b2) } -test("event2message: body is used when there is no formatted_body", t => { +test("event2message: body is used when there is no formatted_body", async t => { t.deepEqual( - eventToMessage({ + await eventToMessage({ content: { body: "testing plaintext", msgtype: "m.text" @@ -34,9 +58,9 @@ test("event2message: body is used when there is no formatted_body", t => { ) }) -test("event2message: any markdown in body is escaped", t => { +test("event2message: any markdown in body is escaped", async t => { t.deepEqual( - eventToMessage({ + await eventToMessage({ content: { body: "testing **special** ~~things~~ which _should_ *not* `trigger` @any ", msgtype: "m.text" @@ -58,9 +82,9 @@ test("event2message: any markdown in body is escaped", t => { ) }) -test("event2message: basic html is converted to markdown", t => { +test("event2message: basic html is converted to markdown", async t => { t.deepEqual( - eventToMessage({ + await eventToMessage({ content: { msgtype: "m.text", body: "wrong body", @@ -84,9 +108,9 @@ test("event2message: basic html is converted to markdown", t => { ) }) -test("event2message: markdown syntax is escaped", t => { +test("event2message: markdown syntax is escaped", async t => { t.deepEqual( - eventToMessage({ + await eventToMessage({ content: { msgtype: "m.text", body: "wrong body", @@ -110,9 +134,9 @@ test("event2message: markdown syntax is escaped", t => { ) }) -test("event2message: html lines are bridged correctly", t => { +test("event2message: html lines are bridged correctly", async t => { t.deepEqual( - eventToMessage({ + await eventToMessage({ content: { msgtype: "m.text", body: "wrong body", @@ -136,9 +160,9 @@ test("event2message: html lines are bridged correctly", t => { ) }) -/*test("event2message: whitespace is retained", t => { +/*test("event2message: whitespace is retained", async t => { t.deepEqual( - eventToMessage({ + await eventToMessage({ content: { msgtype: "m.text", body: "wrong body", @@ -162,10 +186,10 @@ test("event2message: html lines are bridged correctly", t => { ) })*/ -test("event2message: whitespace is collapsed", t => { +test("event2message: whitespace is collapsed", async t => { sameFirstContentAndWhitespace( t, - eventToMessage({ + await eventToMessage({ content: { msgtype: "m.text", body: "wrong body", @@ -189,10 +213,10 @@ test("event2message: whitespace is collapsed", t => { ) }) -test("event2message: lists are bridged correctly", t => { +test("event2message: lists are bridged correctly", async t => { sameFirstContentAndWhitespace( t, - eventToMessage({ + await eventToMessage({ "type": "m.room.message", "sender": "@cadence:cadence.moe", "content": { @@ -217,9 +241,9 @@ test("event2message: lists are bridged correctly", t => { ) }) -test("event2message: long messages are split", t => { +test("event2message: long messages are split", async t => { t.deepEqual( - eventToMessage({ + await eventToMessage({ content: { body: ("a".repeat(130) + " ").repeat(19), msgtype: "m.text" @@ -245,9 +269,9 @@ test("event2message: long messages are split", t => { ) }) -test("event2message: code blocks work", t => { +test("event2message: code blocks work", async t => { t.deepEqual( - eventToMessage({ + await eventToMessage({ content: { msgtype: "m.text", body: "wrong body", @@ -271,9 +295,9 @@ test("event2message: code blocks work", t => { ) }) -test("event2message: code block contents are formatted correctly and not escaped", t => { +test("event2message: code block contents are formatted correctly and not escaped", async t => { t.deepEqual( - eventToMessage({ + await eventToMessage({ "type": "m.room.message", "sender": "@cadence:cadence.moe", "content": { @@ -298,9 +322,9 @@ test("event2message: code block contents are formatted correctly and not escaped ) }) -test("event2message: quotes have an appropriate amount of whitespace", t => { +test("event2message: quotes have an appropriate amount of whitespace", async t => { t.deepEqual( - eventToMessage({ + await eventToMessage({ content: { msgtype: "m.text", body: "wrong body", @@ -324,9 +348,9 @@ test("event2message: quotes have an appropriate amount of whitespace", t => { ) }) -test("event2message: m.emote markdown syntax is escaped", t => { +test("event2message: m.emote markdown syntax is escaped", async t => { t.deepEqual( - eventToMessage({ + await eventToMessage({ content: { msgtype: "m.emote", body: "wrong body", @@ -349,3 +373,136 @@ test("event2message: m.emote markdown syntax is escaped", t => { }] ) }) + +test("event2message: rich reply to a sim user", async t => { + t.deepEqual( + await eventToMessage({ + "type": "m.room.message", + "sender": "@cadence:cadence.moe", + "content": { + "msgtype": "m.text", + "body": "> <@_ooye_kyuugryphon:cadence.moe> Slow news day.\n\nTesting this reply, ignore", + "format": "org.matrix.custom.html", + "formatted_body": "
In reply to @_ooye_kyuugryphon:cadence.moe
Slow news day.
Testing this reply, ignore", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04" + } + } + }, + "origin_server_ts": 1693029683016, + "unsigned": { + "age": 91, + "transaction_id": "m1693029682894.510" + }, + "event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8", + "room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe" + }, data.guild.general, { + api: { + getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", { + type: "m.room.message", + content: { + msgtype: "m.text", + body: "Slow news day." + }, + sender: "@_ooye_kyuugryphon:cadence.moe" + }) + } + }), + [{ + username: "cadence", + content: "<:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>: Slow news day.\nTesting this reply, ignore", + avatar_url: undefined + }] + ) +}) + +test("event2message: rich reply to a matrix user's long message with formatting", async t => { + t.deepEqual( + await eventToMessage({ + "type": "m.room.message", + "sender": "@cadence:cadence.moe", + "content": { + "msgtype": "m.text", + "body": "> <@cadence:cadence.moe> ```\n> i should have a little happy test\n> ```\n> * list **bold** _em_ ~~strike~~\n> # heading 1\n> ## heading 2\n> ### heading 3\n> https://cadence.moe\n> [legit website](https://cadence.moe)\n\nno you can't!!!", + "format": "org.matrix.custom.html", + "formatted_body": "
In reply to @cadence:cadence.moe
i should have a little happy test\n
\n
    \n
  • list bold em ~~strike~~
  • \n
\n

heading 1

\n

heading 2

\n

heading 3

\n

https://cadence.moe
legit website

\n
no you can't!!!", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04" + } + } + }, + "origin_server_ts": 1693037401693, + "unsigned": { + "age": 381, + "transaction_id": "m1693037401592.521" + }, + "event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8", + "room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe" + }, data.guild.general, { + api: { + getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", { + "type": "m.room.message", + "sender": "@cadence:cadence.moe", + "content": { + "msgtype": "m.text", + "body": "```\ni should have a little happy test\n```\n* list **bold** _em_ ~~strike~~\n# heading 1\n## heading 2\n### heading 3\nhttps://cadence.moe\n[legit website](https://cadence.moe)", + "format": "org.matrix.custom.html", + "formatted_body": "
i should have a little happy test\n
\n
    \n
  • list bold em ~~strike~~
  • \n
\n

heading 1

\n

heading 2

\n

heading 3

\n

https://cadence.moe
legit website

\n" + } + }) + } + }), + [{ + username: "cadence", + content: "<:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 Ⓜ️**cadence**: i should have a little...\n**no you can't!!!**", + avatar_url: undefined + }] + ) +}) + +test("event2message: with layered rich replies, the preview should only be the real text", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "> <@cadence:cadence.moe> two\n\nthree", + format: "org.matrix.custom.html", + formatted_body: "
In reply to @cadence:cadence.moe
two
three", + "m.relates_to": { + "m.in_reply_to": { + event_id: "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04" + } + } + }, + event_id: "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8", + room_id: "!fGgIymcYWOqjbSRUdV:cadence.moe" + }, data.guild.general, { + api: { + getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", { + "type": "m.room.message", + "sender": "@cadence:cadence.moe", + "content": { + "msgtype": "m.text", + "body": "> <@cadence:cadence.moe> one\n\ntwo", + "format": "org.matrix.custom.html", + "formatted_body": "
In reply to @cadence:cadence.moe
one
two", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$5UtboIC30EFlAYD_Oh0pSYVW8JqOp6GsDIJZHtT0Wls" + } + } + } + }) + } + }), + [{ + username: "cadence", + content: "<:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 Ⓜ️**cadence**: two\nthree", + avatar_url: undefined + }] + ) +}) From 3ebfa8e3a7aa10fd26394b53b1f65cfc3b9149e3 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 26 Aug 2023 22:22:54 +1200 Subject: [PATCH 95/99] show member details on discord from cache --- db/data-for-test.sql | 12 +++++++ m2d/converters/event-to-message.js | 28 ++++++++++++++--- m2d/converters/event-to-message.test.js | 42 ++++++++++++------------- m2d/converters/utils.js | 11 +++++++ 4 files changed, 68 insertions(+), 25 deletions(-) diff --git a/db/data-for-test.sql b/db/data-for-test.sql index b8da73c..8c564df 100644 --- a/db/data-for-test.sql +++ b/db/data-for-test.sql @@ -47,6 +47,13 @@ CREATE TABLE IF NOT EXISTS "event_message" ( "source" INTEGER NOT NULL, PRIMARY KEY("event_id","message_id") ); +CREATE TABLE IF NOT EXISTS "member_cache" ( + "room_id" TEXT NOT NULL, + "mxid" TEXT NOT NULL, + "displayname" TEXT, + "avatar_url" TEXT, + PRIMARY KEY("room_id", "mxid") +); COMMIT; @@ -101,4 +108,9 @@ INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024', 'mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF'), ('https://cdn.discordapp.com/avatars/113340068197859328/b48302623a12bc7c59a71328f72ccb39.png?size=1024', 'mxc://cadence.moe/UpAeIqeclhKfeiZNdIWNcXXL'); +INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES +('!kLRqKKUQXcibIMtOpl:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL), +('!BpMdOUkWWhFxmTrENV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL), +('!fGgIymcYWOqjbSRUdV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'); + COMMIT; diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index cf34705..8363d8f 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -9,6 +9,8 @@ const passthrough = require("../../passthrough") const { sync, db, discord } = passthrough /** @type {import("../../matrix/file")} */ const file = sync.require("../../matrix/file") +/** @type {import("../converters/utils")} */ +const utils = sync.require("../converters/utils") const BLOCK_ELEMENTS = [ "ADDRESS", "ARTICLE", "ASIDE", "AUDIO", "BLOCKQUOTE", "BODY", "CANVAS", @@ -69,6 +71,22 @@ turndownService.addRule("fencedCodeBlock", { } }) +/** + * @param {string} roomID + * @param {string} mxid + * @returns {Promise<{displayname?: string?, avatar_url?: string?}>} + */ +async function getMemberFromCacheOrHomeserver(roomID, mxid, api) { + const row = db.prepare("SELECT displayname, avatar_url FROM member_cache WHERE room_id = ? AND mxid = ?").get(roomID, mxid) + if (row) return row + return api.getStateEvent(roomID, "m.room.member", mxid).then(event => { + db.prepare("INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES (?, ?, ?, ?)").run(roomID, mxid, event?.displayname || null, event?.avatar_url || null) + return event + }).catch(() => { + return {displayname: null, avatar_url: null} + }) +} + /** * @param {Ty.Event.Outer} event * @param {import("discord-api-types/v10").APIGuild} guild @@ -81,11 +99,13 @@ async function eventToMessage(event, guild, di) { let displayName = event.sender let avatarURL = undefined let replyLine = "" + // Extract a basic display name from the sender const match = event.sender.match(/^@(.*?):/) - if (match) { - displayName = match[1] - // TODO: get the media repo domain and the avatar url from the matrix member event - } + if (match) displayName = match[1] + // Try to extract an accurate display name and avatar URL from the member event + const member = await getMemberFromCacheOrHomeserver(event.room_id, event.sender, di?.api) + if (member.displayname) displayName = member.displayname + if (member.avatar_url) avatarURL = utils.getPublicUrlForMxc(member.avatar_url) // Convert content depending on what the message is let content = event.content.body // ultimate fallback diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index 33c4d71..b595be8 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -51,7 +51,7 @@ test("event2message: body is used when there is no formatted_body", async t => { } }), [{ - username: "cadence", + username: "cadence [they]", content: "testing plaintext", avatar_url: undefined }] @@ -75,7 +75,7 @@ test("event2message: any markdown in body is escaped", async t => { } }), [{ - username: "cadence", + username: "cadence [they]", content: "testing \\*\\*special\\*\\* \\~\\~things\\~\\~ which \\_should\\_ \\*not\\* \\`trigger\\` @any ", avatar_url: undefined }] @@ -101,7 +101,7 @@ test("event2message: basic html is converted to markdown", async t => { } }), [{ - username: "cadence", + username: "cadence [they]", content: "this **is** a **_test_** of ~~formatting~~", avatar_url: undefined }] @@ -127,7 +127,7 @@ test("event2message: markdown syntax is escaped", async t => { } }), [{ - username: "cadence", + username: "cadence [they]", content: "this \\*\\*is\\*\\* an **_extreme_** \\\\\\*test\\\\\\* of", avatar_url: undefined }] @@ -153,7 +153,7 @@ test("event2message: html lines are bridged correctly", async t => { } }), [{ - username: "cadence", + username: "cadence [they]", content: "paragraph one\nline _two_\nline three\n\nparagraph two\nline _two_\nline three\n\nparagraph three\n\nparagraph four\nline two\nline three\nline four\n\nparagraph five", avatar_url: undefined }] @@ -179,7 +179,7 @@ test("event2message: html lines are bridged correctly", async t => { } }), [{ - username: "cadence", + username: "cadence [they]", content: "line one: test test\nline two: **test** **test**\nline three: **test test**\nline four: test test\n line five", avatar_url: undefined }] @@ -206,7 +206,7 @@ test("event2message: whitespace is collapsed", async t => { } }), [{ - username: "cadence", + username: "cadence [they]", content: "line one: test test\nline two: **test** **test**\nline three: **test test**\nline four: test test\nline five", avatar_url: undefined }] @@ -234,7 +234,7 @@ test("event2message: lists are bridged correctly", async t => { "room_id": "!BpMdOUkWWhFxmTrENV:cadence.moe" }), [{ - username: "cadence", + username: "cadence [they]", content: "* line one\n* line two\n* line three\n * nested one\n * nested two\n* line four", avatar_url: undefined }] @@ -258,11 +258,11 @@ test("event2message: long messages are split", async t => { } }), [{ - username: "cadence", + username: "cadence [they]", content: (("a".repeat(130) + " ").repeat(15)).slice(0, -1), avatar_url: undefined }, { - username: "cadence", + username: "cadence [they]", content: (("a".repeat(130) + " ").repeat(4)).slice(0, -1), avatar_url: undefined }] @@ -288,7 +288,7 @@ test("event2message: code blocks work", async t => { } }), [{ - username: "cadence", + username: "cadence [they]", content: "preceding\n\n```\ncode block\n```\n\nfollowing `code` is inline", avatar_url: undefined }] @@ -315,7 +315,7 @@ test("event2message: code block contents are formatted correctly and not escaped "room_id": "!BpMdOUkWWhFxmTrENV:cadence.moe" }), [{ - username: "cadence", + username: "cadence [they]", content: "```\ninput = input.replace(/(<\\/?([^ >]+)[^>]*>)?\\n(<\\/?([^ >]+)[^>]*>)?/g,\n_input_ = input = input.replace(/(<\\/?([^ >]+)[^>]*>)?\\n(<\\/?([^ >]+)[^>]*>)?/g,\n```\n\n`input = input.replace(/(<\\/?([^ >]+)[^>]*>)?\\n(<\\/?([^ >]+)[^>]*>)?/g,`", avatar_url: undefined }] @@ -341,7 +341,7 @@ test("event2message: quotes have an appropriate amount of whitespace", async t = } }), [{ - username: "cadence", + username: "cadence [they]", content: "> Chancellor of Germany Angela Merkel, on March 17, 2017: they did not shake hands\n🤨", avatar_url: undefined }] @@ -367,8 +367,8 @@ test("event2message: m.emote markdown syntax is escaped", async t => { } }), [{ - username: "cadence", - content: "\\* cadence shows you \\*\\*her\\*\\* **_extreme_** \\\\\\*test\\\\\\* of", + username: "cadence [they]", + content: "\\* cadence \\[they\\] shows you \\*\\*her\\*\\* **_extreme_** \\\\\\*test\\\\\\* of", avatar_url: undefined }] ) @@ -410,9 +410,9 @@ test("event2message: rich reply to a sim user", async t => { } }), [{ - username: "cadence", + username: "cadence [they]", content: "<:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>: Slow news day.\nTesting this reply, ignore", - avatar_url: undefined + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" }] ) }) @@ -455,9 +455,9 @@ test("event2message: rich reply to a matrix user's long message with formatting" } }), [{ - username: "cadence", + username: "cadence [they]", content: "<:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 Ⓜ️**cadence**: i should have a little...\n**no you can't!!!**", - avatar_url: undefined + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" }] ) }) @@ -500,9 +500,9 @@ test("event2message: with layered rich replies, the preview should only be the r } }), [{ - username: "cadence", + username: "cadence [they]", content: "<:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 Ⓜ️**cadence**: two\nthree", - avatar_url: undefined + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" }] ) }) diff --git a/m2d/converters/utils.js b/m2d/converters/utils.js index 7b9c504..02ec147 100644 --- a/m2d/converters/utils.js +++ b/m2d/converters/utils.js @@ -19,4 +19,15 @@ function eventSenderIsFromDiscord(sender) { return false } +/** + * @param {string} mxc + * @returns {string?} + */ +function getPublicUrlForMxc(mxc) { + const avatarURLParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/) + if (avatarURLParts) return `https://matrix.cadence.moe/_matrix/media/r0/download/${avatarURLParts[1]}/${avatarURLParts[2]}` + else return null +} + module.exports.eventSenderIsFromDiscord = eventSenderIsFromDiscord +module.exports.getPublicUrlForMxc = getPublicUrlForMxc From 3f2a8d959c10891921e8dcb5e5e6f6d44f9fb727 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 26 Aug 2023 22:50:54 +1200 Subject: [PATCH 96/99] preemptively cache members as we find them --- m2d/converters/event-to-message.js | 2 +- m2d/event-dispatcher.js | 10 ++++++++++ types.d.ts | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index 8363d8f..f5d3c90 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -80,7 +80,7 @@ async function getMemberFromCacheOrHomeserver(roomID, mxid, api) { const row = db.prepare("SELECT displayname, avatar_url FROM member_cache WHERE room_id = ? AND mxid = ?").get(roomID, mxid) if (row) return row return api.getStateEvent(roomID, "m.room.member", mxid).then(event => { - db.prepare("INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES (?, ?, ?, ?)").run(roomID, mxid, event?.displayname || null, event?.avatar_url || null) + db.prepare("REPLACE INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES (?, ?, ?, ?)").run(roomID, mxid, event?.displayname || null, event?.avatar_url || null) return event }).catch(() => { return {displayname: null, avatar_url: null} diff --git a/m2d/event-dispatcher.js b/m2d/event-dispatcher.js index 3425fdb..9a575fc 100644 --- a/m2d/event-dispatcher.js +++ b/m2d/event-dispatcher.js @@ -91,3 +91,13 @@ async event => { const name = event.content.name || null db.prepare("UPDATE channel_room SET nick = ? WHERE room_id = ?").run(name, event.room_id) })) + +sync.addTemporaryListener(as, "type:m.room.member", guard("m.room.member", +/** + * @param {Ty.Event.StateOuter} event + */ +async event => { + if (event.state_key[0] !== "@") return + if (utils.eventSenderIsFromDiscord(event.sender)) return + db.prepare("REPLACE INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES (?, ?, ?, ?)").run(event.room_id, event.sender, event.content.displayname || null, event.content.avatar_url || null) +})) diff --git a/types.d.ts b/types.d.ts index 2bf0af0..5475904 100644 --- a/types.d.ts +++ b/types.d.ts @@ -80,7 +80,7 @@ export namespace Event { export type M_Room_Member = { membership: string - display_name?: string + displayname?: string avatar_url?: string } From 7c2fc1536dcc0c44ebb838149ccbb4058fadbfd7 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 26 Aug 2023 22:51:42 +1200 Subject: [PATCH 97/99] trying to make reaction emojis consistent --- m2d/actions/add-reaction.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/m2d/actions/add-reaction.js b/m2d/actions/add-reaction.js index 68828dd..49aa845 100644 --- a/m2d/actions/add-reaction.js +++ b/m2d/actions/add-reaction.js @@ -21,9 +21,20 @@ async function addReaction(event) { let encoded = encodeURIComponent(emoji) let encodedTrimmed = encoded.replace(/%EF%B8%8F/g, "") - console.log("add reaction from matrix:", emoji, encoded, encodedTrimmed) + // https://github.com/discord/discord-api-docs/issues/2723#issuecomment-807022205 ???????????? - return discord.snow.channel.createReaction(channelID, messageID, encoded) + const forceTrimmedList = [ + "%E2%AD%90" // ⭐ + ] + + let discordPreferredEncoding = + ( forceTrimmedList.includes(encodedTrimmed) ? encodedTrimmed + : encodedTrimmed !== encoded && [...emoji].length === 2 ? encoded + : encodedTrimmed) + + console.log("add reaction from matrix:", emoji, encoded, encodedTrimmed, "chosen:", discordPreferredEncoding) + + return discord.snow.channel.createReaction(channelID, messageID, discordPreferredEncoding) } module.exports.addReaction = addReaction From 58f5c3edf7852171d6dd0068c4f602cf7e734239 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 26 Aug 2023 22:59:22 +1200 Subject: [PATCH 98/99] m->d underline formatting support --- m2d/converters/event-to-message.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index f5d3c90..d6daed7 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -37,6 +37,13 @@ turndownService.addRule("strikethrough", { } }) +turndownService.addRule("underline", { + filter: ["u"], + replacement: function (content) { + return "__" + content + "__" + } +}) + turndownService.addRule("blockquote", { filter: "blockquote", replacement: function (content) { From cae8d7c2f2434b44555325244bb1c030e2294b67 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 26 Aug 2023 23:22:23 +1200 Subject: [PATCH 99/99] m->d mentioning bridged users and rooms works --- m2d/converters/event-to-message.js | 42 ++++++++++++- m2d/converters/event-to-message.test.js | 78 +++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index d6daed7..22ed377 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -21,6 +21,10 @@ const BLOCK_ELEMENTS = [ "TFOOT", "TH", "THEAD", "TR", "UL" ] +function cleanAttribute (attribute) { + return attribute ? attribute.replace(/(\n+\s*)+/g, "\n") : "" +} + const turndownService = new TurndownService({ hr: "----", headingStyle: "atx", @@ -53,6 +57,27 @@ turndownService.addRule("blockquote", { } }) +turndownService.addRule("inlineLink", { + filter: function (node, options) { + return ( + options.linkStyle === "inlined" && + node.nodeName === "A" && + node.getAttribute("href") + ) + }, + + replacement: function (content, node) { + if (node.getAttribute("data-user-id")) return `<@${node.getAttribute("data-user-id")}>` + if (node.getAttribute("data-channel-id")) return `<#${node.getAttribute("data-channel-id")}>` + const href = node.getAttribute("href") + let title = cleanAttribute(node.getAttribute("title")) + if (title) title = ` "` + title + `"` + let brackets = ["", ""] + if (href.startsWith("https://matrix.to")) brackets = ["<", ">"] + return "[" + content + "](" + brackets[0] + href + title + brackets[1] + ")" + } +}) + turndownService.addRule("fencedCodeBlock", { filter: function (node, options) { return ( @@ -153,6 +178,21 @@ async function eventToMessage(event, guild, di) { replyLine += contentPreview + "\n" })() + // Handling mentions of Discord users + input = input.replace(/("https:\/\/matrix.to\/#\/(@[^"]+)")>/g, (whole, attributeValue, mxid) => { + if (!utils.eventSenderIsFromDiscord(mxid)) return whole + const userID = db.prepare("SELECT discord_id FROM sim WHERE mxid = ?").pluck().get(mxid) + if (!userID) return whole + return `${attributeValue} data-user-id="${userID}">` + }) + + // Handling mentions of Discord rooms + input = input.replace(/("https:\/\/matrix.to\/#\/(![^"]+)")>/g, (whole, attributeValue, roomID) => { + const channelID = db.prepare("SELECT channel_id FROM channel_room WHERE room_id = ?").pluck().get(roomID) + if (!channelID) return whole + return `${attributeValue} data-channel-id="${channelID}">` + }) + // Element adds a bunch of
before but doesn't render them. I can't figure out how this even works in the browser, so let's just delete those. input = input.replace(/(?:\n|
\s*)*<\/blockquote>/g, "") @@ -174,7 +214,7 @@ async function eventToMessage(event, guild, di) { } }) - // @ts-ignore + // @ts-ignore bad type from turndown content = turndownService.turndown(input) // It's optimised for commonmark, we need to replace the space-space-newline with just newline diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index b595be8..61da83c 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -506,3 +506,81 @@ test("event2message: with layered rich replies, the preview should only be the r }] ) }) + +test("event2message: mentioning discord users works", async t => { + t.deepEqual( + await eventToMessage({ + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: `I'm just testing mentions` + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }), + [{ + username: "cadence [they]", + content: "I'm just <@114147806469554185> testing mentions", + avatar_url: undefined + }] + ) +}) + +test("event2message: mentioning matrix users works", async t => { + t.deepEqual( + await eventToMessage({ + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: `I'm just testing mentions` + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }), + [{ + username: "cadence [they]", + content: "I'm just [▲]() testing mentions", + avatar_url: undefined + }] + ) +}) + +test("event2message: mentioning bridged rooms works", async t => { + t.deepEqual( + await eventToMessage({ + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: `I'm just testing mentions` + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }), + [{ + username: "cadence [they]", + content: "I'm just [▲]() testing mentions", + avatar_url: undefined + }] + ) +})