diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 24c7f2d..92bc241 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -107,7 +107,7 @@ async function attachmentToEvent(mentions, attachment) { msgtype: "m.text", body: `${emoji} Uploaded SPOILER file: ${attachment.url} (${pb(attachment.size)})`, format: "org.matrix.custom.html", - formatted_body: `
${emoji} Uploaded SPOILER file: ${attachment.url} (${pb(attachment.size)})` + formatted_body: `
${emoji} Uploaded SPOILER file: ${attachment.url} (${pb(attachment.size)})` } } // for large files, always link them instead of uploading so I don't use up all the space in the content repo diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index f3ac895..9c28f0b 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -176,7 +176,7 @@ test("message2event: spoiler attachment", async t => { msgtype: "m.text", body: "📄 Uploaded SPOILER file: https://cdn.discordapp.com/attachments/1100319550446252084/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci (74 KB)", format: "org.matrix.custom.html", - formatted_body: "
📄 Uploaded SPOILER file: View (74 KB)" + formatted_body: "
📄 Uploaded SPOILER file: https://cdn.discordapp.com/attachments/1100319550446252084/1147465564307079258/SPOILER_69-GNDP-CADENCE.nfs.gci (74 KB)" }]) }) diff --git a/d2m/converters/user-to-mxid.js b/d2m/converters/user-to-mxid.js index 35a3b5e..4d53a9c 100644 --- a/d2m/converters/user-to-mxid.js +++ b/d2m/converters/user-to-mxid.js @@ -5,6 +5,10 @@ const assert = require("assert") const passthrough = require("../../passthrough") const {select} = passthrough +const SPECIAL_USER_MAPPINGS = new Map([ + ["1081004946872352958", ["clyde_ai", "clyde"]] +]) + /** * Downcased and stripped username. Can only include a basic set of characters. * https://spec.matrix.org/v1.6/appendices/#user-identifiers @@ -30,7 +34,7 @@ function downcaseUsername(user) { /** @param {string[]} preferences */ function* generateLocalpartAlternatives(preferences) { const best = preferences[0] - assert.ok(best) + assert(best) // First, suggest the preferences... for (const localpart of preferences) { yield localpart @@ -50,15 +54,18 @@ function* generateLocalpartAlternatives(preferences) { * @returns {string} */ function userToSimName(user) { - assert.notEqual(user.discriminator, "0000", "cannot create user for a webhook") + if (!SPECIAL_USER_MAPPINGS.has(user.id)) { // skip this check for known special users + assert.notEqual(user.discriminator, "0000", `cannot create user for a webhook: ${JSON.stringify(user)}`) + } // 1. Is sim user already registered? const existing = select("sim", "sim_name", {user_id: user.id}).pluck().get() if (existing) return existing // 2. Register based on username (could be new or old format) + // (Unless it's a special user, in which case copy their provided mappings.) const downcased = downcaseUsername(user) - const preferences = [downcased] + const preferences = SPECIAL_USER_MAPPINGS.get(user.id) || [downcased] if (user.discriminator.length === 4) { // Old style tag? If user.username is unavailable, try the full tag next preferences.push(downcased + user.discriminator) } diff --git a/d2m/converters/user-to-mxid.test.js b/d2m/converters/user-to-mxid.test.js index e709473..b080115 100644 --- a/d2m/converters/user-to-mxid.test.js +++ b/d2m/converters/user-to-mxid.test.js @@ -1,6 +1,7 @@ const {test} = require("supertape") const tryToCatch = require("try-to-catch") const assert = require("assert") +const data = require("../../test/data") const {userToSimName} = require("./user-to-mxid") test("user2name: cannot create user for a webhook", async t => { @@ -39,3 +40,7 @@ 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") }) + +test("user2name: works on special user", t => { + t.equal(userToSimName(data.user.clyde_ai), "clyde_ai") +}) diff --git a/m2d/actions/send-event.js b/m2d/actions/send-event.js index f26abb0..1b60ffe 100644 --- a/m2d/actions/send-event.js +++ b/m2d/actions/send-event.js @@ -15,6 +15,8 @@ const channelWebhook = sync.require("./channel-webhook") const eventToMessage = sync.require("../converters/event-to-message") /** @type {import("../../matrix/api")}) */ const api = sync.require("../../matrix/api") +/** @type {import("../../d2m/actions/register-user")} */ +const registerUser = sync.require("../../d2m/actions/register-user") /** * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | Readable}[], pendingFiles?: ({name: string, url: string} | {name: string, url: string, key: string, iv: string} | {name: string, buffer: Buffer | Readable})[]}} message @@ -73,7 +75,7 @@ async function sendEvent(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 {messagesToEdit, messagesToSend, messagesToDelete} = await eventToMessage.eventToMessage(event, guild, {api, snow: discord.snow}) + let {messagesToEdit, messagesToSend, messagesToDelete, ensureJoined} = await eventToMessage.eventToMessage(event, guild, {api, snow: discord.snow}) messagesToEdit = await Promise.all(messagesToEdit.map(async e => { e.message = await resolvePendingFiles(e.message) @@ -107,6 +109,10 @@ async function sendEvent(event) { messageResponses.push(messageResponse) } + for (const user of ensureJoined) { + registerUser.ensureSimJoined(user, event.room_id) + } + return messageResponses } diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index ae40abf..cf51d86 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -505,12 +505,13 @@ async function eventToMessage(event, guild, di) { content = displayNameRunoff + replyLine + content // Handling written @mentions: we need to look for candidate Discord members to join to the room - let writtenMentionMatch = content.match(/(?:^|[^"<>/A-Za-z0-9])@([A-Za-z][A-Za-z0-9._\[\]\(\)-]+):?/) + let writtenMentionMatch = content.match(/(?:^|[^"<>/A-Za-z0-9])@([A-Za-z][A-Za-z0-9._\[\]\(\)-]+):?/d) // /d flag for indices requires node.js 16+ if (writtenMentionMatch) { const results = await di.snow.guild.searchGuildMembers(guild.id, {query: writtenMentionMatch[1]}) if (results[0]) { assert(results[0].user) - content = content.slice(0, writtenMentionMatch.index) + `<@${results[0].user.id}>` + content.slice(writtenMentionMatch.index + writtenMentionMatch[0].length) + // @ts-ignore - typescript doesn't know about indices yet + content = content.slice(0, writtenMentionMatch.indices[1][0]-1) + `<@${results[0].user.id}>` + content.slice(writtenMentionMatch.indices[1][1]) ensureJoined.push(results[0].user) } } diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index 1af5e42..40eb83e 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -2179,7 +2179,7 @@ test("event2message: guessed @mentions may join members to mention", async t => sender: "@cadence:cadence.moe", content: { msgtype: "m.text", - body: "@subtext: what food would you like to order?" + body: "hey @subtext, what food would you like to order?" }, event_id: "$u5gSwSzv_ZQS3eM00mnTBCor8nx_A_AwuQz7e59PZk8", room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" @@ -2202,7 +2202,7 @@ test("event2message: guessed @mentions may join members to mention", async t => messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "<@321876634777218072> what food would you like to order?", + content: "hey <@321876634777218072>, what food would you like to order?", avatar_url: undefined }], ensureJoined: [subtext.user] diff --git a/matrix/api.js b/matrix/api.js index d6fd28b..b59d6ef 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -19,15 +19,17 @@ 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) + if (mxid) 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() + let result = u.pathname + const str = u.searchParams.toString() + if (str) result += "?" + str + return result } /** @@ -88,7 +90,7 @@ async function getEvent(roomID, eventID) { */ async function getEventForTimestamp(roomID, ts) { /** @type {{event_id: string, origin_server_ts: number}} */ - const root = await mreq.mreq("GET", path(`/client/v3/rooms/${roomID}/timestamp_to_event`, null, {ts})) + const root = await mreq.mreq("GET", path(`/client/v1/rooms/${roomID}/timestamp_to_event`, null, {ts})) return root } diff --git a/matrix/api.test.js b/matrix/api.test.js index 6c74e50..82565eb 100644 --- a/matrix/api.test.js +++ b/matrix/api.test.js @@ -20,3 +20,7 @@ test("api path: existing query parameters with mxid", t => { test("api path: real world mxid", t => { t.equal(path("/hello/world", "@cookie_monster:cadence.moe"), "/hello/world?user_id=%40cookie_monster%3Acadence.moe") }) + +test("api path: extras number works", t => { + t.equal(path(`/client/v3/rooms/!example/timestamp_to_event`, null, {ts: 1687324651120}), "/client/v3/rooms/!example/timestamp_to_event?ts=1687324651120") +}) diff --git a/package-lock.json b/package-lock.json index d50f151..3367e93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@chriscdn/promise-semaphore": "^2.0.1", "better-sqlite3": "^9.0.0", "chunk-text": "^2.0.1", - "cloudstorm": ">=0.9.0", + "cloudstorm": "^0.9.5", "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#de519353668c87ecc8c543e9749093481bc72ff8", "entities": "^4.5.0", "giframe": "github:cloudrac3r/giframe#v0.4.1", @@ -783,9 +783,9 @@ } }, "node_modules/cloudstorm": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.9.2.tgz", - "integrity": "sha512-dXyK8/SseyhAvblPDbDILCb6ghpoJnBAiBx1ig5/yQ54TvOXlZJ4MC+So7EJDdaHkTgnf38F8qNyBNN29sMMcQ==", + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.9.5.tgz", + "integrity": "sha512-WKaCsTDobR5c3YOmAchIa4QhPyWkUtYP3wNC/h6iE4bXE1DdN432FD3u3cuD3fX1Km9fPgpGBi4m6KYf5GFkJg==", "dependencies": { "snowtransfer": "^0.9.0" }, diff --git a/package.json b/package.json index ceb6c96..01070d2 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "@chriscdn/promise-semaphore": "^2.0.1", "better-sqlite3": "^9.0.0", "chunk-text": "^2.0.1", - "cloudstorm": ">=0.9.0", + "cloudstorm": "^0.9.5", "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#de519353668c87ecc8c543e9749093481bc72ff8", "entities": "^4.5.0", "giframe": "github:cloudrac3r/giframe#v0.4.1", diff --git a/test/data.js b/test/data.js index 648c2c8..3e78802 100644 --- a/test/data.js +++ b/test/data.js @@ -183,6 +183,23 @@ module.exports = { safety_alerts_channel_id: null } }, + user: { + clyde_ai: { + id: "1081004946872352958", + username: "clyde", + avatar: "a_6170487d32fdfe9f988720ad80e6ab8c", + discriminator: "0000", + public_flags: 0, + premium_type: 2, + flags: 0, + bot: true, + banner: null, + accent_color: null, + global_name: "Clyde", + avatar_decoration_data: null, + banner_color: null + } + }, member: { kumaccino: { avatar: null,