From fab512a9bae51b59f50b575f0f9a8c41396c0ddb Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 4 Oct 2026 22:01:24 +1300 Subject: [PATCH 1/6] Fix Klipy gifs when url previews disabled --- src/d2m/converters/message-to-event.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 86b310c..c6d5e2c 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -783,9 +783,10 @@ async function messageToEvent(message, guild, options = {}, di) { } // Send Klipy GIFs in customised form + const urlPreviewEnabled = select("guild_space", "url_preview", {guild_id: guild?.id}).pluck().get() ?? 1 let isKlipyGIF = false let isOnlyKlipyGIF = false - if (message.embeds?.length === 1 && message.embeds[0].provider?.name === "Klipy" && message.embeds[0].video?.url) { + if (message.embeds?.length === 1 && message.embeds[0].provider?.name === "Klipy" && message.embeds[0].video?.url && urlPreviewEnabled) { isKlipyGIF = true if (message.content?.match(/^https?:\/\/klipy\.com[^ \n]+$/)) { isOnlyKlipyGIF = true @@ -1046,7 +1047,6 @@ async function messageToEvent(message, guild, options = {}, di) { } // Then embeds - const urlPreviewEnabled = select("guild_space", "url_preview", {guild_id: guild?.id}).pluck().get() ?? 1 for (const embed of message.embeds || []) { if (!urlPreviewEnabled && !message.author?.bot) { continue // show embeds for everyone if enabled, or bot users only if disabled (bots often send content in embeds) From 5794bee128c45af69a98741fe7a174a0c6e62ccb Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 4 Oct 2026 22:19:35 +1300 Subject: [PATCH 2/6] Support Discord game mentions --- package-lock.json | 274 ++++++++++---------- package.json | 2 +- src/d2m/converters/message-to-event.js | 8 + src/d2m/converters/message-to-event.test.js | 10 + test/data.js | 52 ++++ 5 files changed, 208 insertions(+), 138 deletions(-) diff --git a/package-lock.json b/package-lock.json index d9ff5cb..6ec8ce6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@chriscdn/promise-semaphore": "^3.0.1", - "@cloudrac3r/discord-markdown": "^2.7.0", + "@cloudrac3r/discord-markdown": "^2.8.0", "@cloudrac3r/giframe": "^0.4.3", "@cloudrac3r/html-template-tag": "^5.0.1", "@cloudrac3r/in-your-element": "^1.1.1", @@ -131,9 +131,9 @@ } }, "node_modules/@cloudrac3r/discord-markdown": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.7.0.tgz", - "integrity": "sha512-1iR9tKI2WJe8UNB+4VSh7D8m6RP7ugByuf8RNWyJwyhIrSlqQ8ljY1BKXodSvDg7seZkf7B7V2t5FfK7UpTw/A==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.8.0.tgz", + "integrity": "sha512-ciLxS6iaMxuEkEln7Y9rTpUBFf+gij8djNKS1QGrTfKoHcKABmDvcXQlPntdTx7odHjtMqV2RfvR/rnmQ25YPQ==", "license": "MIT", "dependencies": { "simple-markdown": "^0.7.3" @@ -302,9 +302,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", - "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", "cpu": [ "arm64" ], @@ -320,13 +320,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.3" + "@img/sharp-libvips-darwin-arm64": "1.3.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", - "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", "cpu": [ "x64" ], @@ -342,20 +342,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.3" + "@img/sharp-libvips-darwin-x64": "1.3.4" } }, "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", - "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", "license": "Apache-2.0", "optional": true, "os": [ "freebsd" ], "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -365,9 +365,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", - "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", "cpu": [ "arm64" ], @@ -381,9 +381,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", - "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", "cpu": [ "x64" ], @@ -397,9 +397,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", - "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", "cpu": [ "arm" ], @@ -413,9 +413,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", - "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", "cpu": [ "arm64" ], @@ -429,9 +429,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", - "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", "cpu": [ "ppc64" ], @@ -445,9 +445,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", - "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", "cpu": [ "riscv64" ], @@ -461,9 +461,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", - "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", "cpu": [ "s390x" ], @@ -477,9 +477,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", - "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", "cpu": [ "x64" ], @@ -493,9 +493,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", - "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", "cpu": [ "arm64" ], @@ -509,9 +509,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", - "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", "cpu": [ "x64" ], @@ -525,9 +525,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", - "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", "cpu": [ "arm" ], @@ -543,13 +543,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.3" + "@img/sharp-libvips-linux-arm": "1.3.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", - "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", "cpu": [ "arm64" ], @@ -565,13 +565,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.3" + "@img/sharp-libvips-linux-arm64": "1.3.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", - "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", "cpu": [ "ppc64" ], @@ -587,13 +587,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.3" + "@img/sharp-libvips-linux-ppc64": "1.3.4" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", - "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", "cpu": [ "riscv64" ], @@ -609,13 +609,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.3" + "@img/sharp-libvips-linux-riscv64": "1.3.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", - "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", "cpu": [ "s390x" ], @@ -631,13 +631,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.3" + "@img/sharp-libvips-linux-s390x": "1.3.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", - "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", "cpu": [ "x64" ], @@ -653,13 +653,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.3" + "@img/sharp-libvips-linux-x64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", - "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", "cpu": [ "arm64" ], @@ -675,13 +675,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", - "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", "cpu": [ "x64" ], @@ -697,13 +697,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", - "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -717,16 +717,16 @@ } }, "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", - "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", "cpu": [ "wasm32" ], "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -736,9 +736,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", - "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", "cpu": [ "arm64" ], @@ -755,9 +755,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", - "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", "cpu": [ "ia32" ], @@ -774,9 +774,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", - "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", "cpu": [ "x64" ], @@ -1039,9 +1039,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.20.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.2.tgz", - "integrity": "sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw==", + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", "dev": true, "license": "MIT", "dependencies": { @@ -1158,9 +1158,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1477,9 +1477,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.38.55", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.55.tgz", - "integrity": "sha512-ytuaRTzdnHUCXJ6KjL9MrItQX0xKncBKEeYI1Bst4+ud47eejH3cG6gaesYakjpPcUhh68XYS2YwGq3mmujFsA==", + "version": "0.38.56", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.56.tgz", + "integrity": "sha512-Hti4RZsGvF5C7SM8rF2MDHtIm0Hyfrlk84GpEA29nj91tvVVMvX+oQSCodu10t2ZcjFGofq8TX0YyC04B2E69Q==", "license": "MIT" }, "node_modules/doctypes": { @@ -1920,9 +1920,9 @@ } }, "node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -2264,9 +2264,9 @@ } }, "node_modules/sharp": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", - "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", "license": "Apache-2.0", "dependencies": { "@img/colour": "^1.1.0", @@ -2280,31 +2280,31 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.4", - "@img/sharp-darwin-x64": "0.35.4", - "@img/sharp-freebsd-wasm32": "0.35.4", - "@img/sharp-libvips-darwin-arm64": "1.3.3", - "@img/sharp-libvips-darwin-x64": "1.3.3", - "@img/sharp-libvips-linux-arm": "1.3.3", - "@img/sharp-libvips-linux-arm64": "1.3.3", - "@img/sharp-libvips-linux-ppc64": "1.3.3", - "@img/sharp-libvips-linux-riscv64": "1.3.3", - "@img/sharp-libvips-linux-s390x": "1.3.3", - "@img/sharp-libvips-linux-x64": "1.3.3", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", - "@img/sharp-libvips-linuxmusl-x64": "1.3.3", - "@img/sharp-linux-arm": "0.35.4", - "@img/sharp-linux-arm64": "0.35.4", - "@img/sharp-linux-ppc64": "0.35.4", - "@img/sharp-linux-riscv64": "0.35.4", - "@img/sharp-linux-s390x": "0.35.4", - "@img/sharp-linux-x64": "0.35.4", - "@img/sharp-linuxmusl-arm64": "0.35.4", - "@img/sharp-linuxmusl-x64": "0.35.4", - "@img/sharp-webcontainers-wasm32": "0.35.4", - "@img/sharp-win32-arm64": "0.35.4", - "@img/sharp-win32-ia32": "0.35.4", - "@img/sharp-win32-x64": "0.35.4" + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" }, "peerDependenciesMeta": { "@types/node": { @@ -2428,9 +2428,9 @@ } }, "node_modules/supertape": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/supertape/-/supertape-13.6.0.tgz", - "integrity": "sha512-W/lm1wk370QF0u+LrxS+BMUFNYMfRgjFC9dTKyFB522fa77pKJHwOBDiTl3uhNbsw0Sb1NQPRYk+eFICcqxedA==", + "version": "13.6.2", + "resolved": "https://registry.npmjs.org/supertape/-/supertape-13.6.2.tgz", + "integrity": "sha512-VPNYybfbgrTMhhOl4amTfEQ1NpyOPH+gq29rlHeBFyKmKMAcUa1ZEaUWbexY4YLUAzNvaFKOGVzogvZFKq4T9A==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 5fbf928..7c9ac57 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@chriscdn/promise-semaphore": "^3.0.1", - "@cloudrac3r/discord-markdown": "^2.7.0", + "@cloudrac3r/discord-markdown": "^2.8.0", "@cloudrac3r/giframe": "^0.4.3", "@cloudrac3r/html-template-tag": "^5.0.1", "@cloudrac3r/in-your-element": "^1.1.1", diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index c6d5e2c..5aa6529 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -86,6 +86,14 @@ function getDiscordParseCallbacks(message, guild, useHTML, spoilers = []) { return `@${role.name}:` } }, + game: node => { + const game = message.mention_games?.find(g => g.id === node.id) + if (!game?.name) { + return "@$" + node.id + } else { + return game.name + } + }, everyone: () => { if (message.mention_everyone) return "@room" return "@everyone" diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index aea24a8..41f40ba 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -192,6 +192,16 @@ test("message2event: manually constructed unknown roles should use fallback", as }]) }) +test("message2event: simple game mentions", async t => { + const events = await messageToEvent(data.message.simple_game_mention, data.guild.general, {}) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.mentions": {}, + msgtype: "m.text", + body: "do you play Minecraft" + }]) +}) + test("message2event: simple message link", async t => { let called = 0 const events = await messageToEvent(data.message.simple_message_link, data.guild.general, {}, { diff --git a/test/data.js b/test/data.js index 60e9bd8..f441778 100644 --- a/test/data.js +++ b/test/data.js @@ -1680,6 +1680,58 @@ module.exports = { flags: 0, components: [] }, + simple_game_mention: { + type: 0, + content: "do you play <@$1402418491272986635>", + mentions: [], + mention_roles: [], + attachments: [], + embeds: [], + timestamp: "2026-09-28T21:01:56.749000+00:00", + edited_timestamp: null, + flags: 0, + components: [], + id: "1554236746756268077", + channel_id: "1160894080998461480", + author: { + id: "177438227563675648", + username: "owlbot", + avatar: "bafbcafcc0a6c2698d59a2cb567a9ecf", + discriminator: "0", + public_flags: 0, + flags: 0, + banner: null, + accent_color: null, + global_name: null, + avatar_decoration_data: null, + collectibles: null, + display_name_styles: null, + vad_colors: null, + banner_color: null, + clan: { + identity_guild_id: "773319828295385108", + identity_enabled: true, + tag: "puzl", + badge: "1935451f265a42d57241c7bcc9555abe" + }, + primary_guild: { + identity_guild_id: "773319828295385108", + identity_enabled: true, + tag: "puzl", + badge: "1935451f265a42d57241c7bcc9555abe" + } + }, + pinned: false, + mention_everyone: false, + tts: false, + mention_games: [ + { + id: "1402418491272986635", + name: "Minecraft", + aliases: [ "Minecraft Windows 10 Edition", "Minecraft Launcher" ], + } + ] + }, unknown_role: { id: "1162374402785153106", type: 0, From 302427037b18a6a522207592d2788a0bfab9dd38 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 4 Oct 2026 22:24:13 +1300 Subject: [PATCH 3/6] Don't copy URL previews for GitHub --- src/d2m/converters/message-to-event.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 5aa6529..b3e3116 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -1076,6 +1076,10 @@ async function messageToEvent(message, guild, options = {}, di) { continue // If discord creates an embed preview for a discord channel link, don't copy that embed } + if (embed.provider?.name === "GitHub") { + continue // GitHub url previews are very large and fairly useless + } + if (embed.url && spoilers.some(sp => sp.match(/\bhttps?:\/\/[a-z]/))) { // If the original message had spoilered URLs, don't generate any embeds for links. // This logic is the same as the Discord desktop client. It doesn't match specific embeds to specific spoilered text, it's all or nothing. From c25b5a19c62d3c7e13d9d8c5ccdaafe55f9ba7c7 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 7 Oct 2026 22:52:42 +1300 Subject: [PATCH 4/6] Support Discord @time --- package-lock.json | 27 +++- package.json | 2 +- src/d2m/converters/message-to-event.js | 13 ++ .../message-to-event.test.components.js | 4 +- src/d2m/converters/message-to-event.test.js | 22 +++ src/discord/time.js | 51 +++++++ src/discord/time.test.js | 48 ++++++ src/web/pug/time.pug | 143 ++++++++++++++++++ src/web/routes/time.js | 31 ++++ src/web/server.js | 1 + test/data.js | 50 ++++++ test/test.js | 1 + 12 files changed, 383 insertions(+), 10 deletions(-) create mode 100644 src/discord/time.js create mode 100644 src/discord/time.test.js create mode 100644 src/web/pug/time.pug create mode 100644 src/web/routes/time.js diff --git a/package-lock.json b/package-lock.json index 6ec8ce6..32e7e88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@chriscdn/promise-semaphore": "^3.0.1", - "@cloudrac3r/discord-markdown": "^2.8.0", + "@cloudrac3r/discord-markdown": "^2.9.0", "@cloudrac3r/giframe": "^0.4.3", "@cloudrac3r/html-template-tag": "^5.0.1", "@cloudrac3r/in-your-element": "^1.1.1", @@ -55,6 +55,19 @@ "node": ">=22" } }, + "../discord-markdown": { + "name": "@cloudrac3r/discord-markdown", + "version": "2.9.0", + "extraneous": true, + "license": "MIT", + "dependencies": { + "simple-markdown": "^0.7.3" + }, + "devDependencies": { + "@cloudrac3r/tap-dot": "^2.0.2", + "supertape": "^13.6.2" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -131,9 +144,9 @@ } }, "node_modules/@cloudrac3r/discord-markdown": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.8.0.tgz", - "integrity": "sha512-ciLxS6iaMxuEkEln7Y9rTpUBFf+gij8djNKS1QGrTfKoHcKABmDvcXQlPntdTx7odHjtMqV2RfvR/rnmQ25YPQ==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.9.0.tgz", + "integrity": "sha512-0S22mNlT9rPhYnxP+qzKWayVe+hFWcEqxVCviF/pyzWAb9nLaBZkIVk0jHTImoA1MY0mqx1uMKjIcxVHkEZe3A==", "license": "MIT", "dependencies": { "simple-markdown": "^0.7.3" @@ -1049,9 +1062,9 @@ } }, "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", "license": "MIT", "dependencies": { "csstype": "^3.2.2" diff --git a/package.json b/package.json index 7c9ac57..72004cf 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@chriscdn/promise-semaphore": "^3.0.1", - "@cloudrac3r/discord-markdown": "^2.8.0", + "@cloudrac3r/discord-markdown": "^2.9.0", "@cloudrac3r/giframe": "^0.4.3", "@cloudrac3r/html-template-tag": "^5.0.1", "@cloudrac3r/in-your-element": "^1.1.1", diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index b3e3116..f192aff 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -18,6 +18,8 @@ const lottie = sync.require("../actions/lottie") const mxUtils = sync.require("../../matrix/utils") /** @type {import("../../discord/utils")} */ const dUtils = sync.require("../../discord/utils") +/** @type {import("../../discord/time")} */ +const discordTime = sync.require("../../discord/time") /** @type {import("./find-mentions")} */ const findMentions = sync.require("./find-mentions") /** @type {import("../../discord/interactions/poll-responses")} */ @@ -72,6 +74,17 @@ function getDiscordParseCallbacks(message, guild, useHTML, spoilers = []) { return `:${node.name}:` } }, + /** @param {{timestamp: number, style: string | null}} node */ + timestamp: node => { + const text = discordTime.convertDiscordTime(node.timestamp, node.style) + if (useHTML) { + let url = `${reg.ooye.bridge_origin}/time/${node.timestamp}` + if (node.style) url += `/${node.style}` + return tag`${text}` + } else { + return text + } + }, role: node => { const role = guild.roles.find(r => r.id === node.id) if (!role) { diff --git a/src/d2m/converters/message-to-event.test.components.js b/src/d2m/converters/message-to-event.test.components.js index 1ef83c3..47e27b5 100644 --- a/src/d2m/converters/message-to-event.test.components.js +++ b/src/d2m/converters/message-to-event.test.components.js @@ -44,7 +44,7 @@ test("message2event components: pk question mark output", async t => { + "\n| Same hat" + "\n| 🖼️ Image: https://bridge.example.org/download/discordcdn/934955898965729280/1466556006527012987/image.png" + "\n" - + "\n-# Original Message ID: 1466556003645657118 · ", + + "\n-# Original Message ID: 1466556003645657118 · 29 January 2026 at 10:09 pm (UTC)", format: "org.matrix.custom.html", formatted_body: "
" + "

Lillith (INX)

" @@ -73,7 +73,7 @@ test("message2event components: pk question mark output", async t => { + "
" + "

Same hat

" + `🖼️ Image: image.png
` - + "

Original Message ID: 1466556003645657118 · <t:1769724599:f>

", + + "

Original Message ID: 1466556003645657118 · 29 January 2026 at 10:09 pm (UTC)

", "m.mentions": {}, msgtype: "m.text", }]) diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 41f40ba..397be39 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -202,6 +202,28 @@ test("message2event: simple game mentions", async t => { }]) }) +test("message2event: timestamps", async t => { + const events = await messageToEvent(data.message.timestamps, data.guild.general, {}) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "4 Oct 2026, 9:33:33 am (UTC)" + + "\n4 October 2026 at 9:33 am (UTC)" + + "\nSunday, 4 October 2026 at 9:34 am (UTC)" + + "\n9:34:08 am (UTC)" + + "\n3 October 2026 at 9:30 am (UTC)" + + "\n24 December 2026 at 11:00 pm (UTC)", + format: "org.matrix.custom.html", + formatted_body: `4 Oct 2026, 9:33:33 am (UTC)` + + `
4 October 2026 at 9:33 am (UTC)` + + `
Sunday, 4 October 2026 at 9:34 am (UTC)` + + `
9:34:08 am (UTC)` + + `
3 October 2026 at 9:30 am (UTC)` + + `
24 December 2026 at 11:00 pm (UTC)`, + "m.mentions": {} + }]) +}) + test("message2event: simple message link", async t => { let called = 0 const events = await messageToEvent(data.message.simple_message_link, data.guild.general, {}, { diff --git a/src/discord/time.js b/src/discord/time.js new file mode 100644 index 0000000..d0f7a04 --- /dev/null +++ b/src/discord/time.js @@ -0,0 +1,51 @@ +// @ts-check + +const assert = require("assert").strict + +/* + https://docs.discord.com/developers/reference#message-formatting + + Trying to make these formats fairly culture independent since the Matrix text will show up the same for everyone. + That means no ambiguous short dates like 03/04/26. The shortest allowable date is "medium", which spells the month. + It also means no 24-hour time, since this is ambiguous to 12-hour time users. Times must have am/pm. + Relative time is not supported for text conversion and will fall back to the default, "f". + + See the test file for sample outputs. +*/ + +/** @type {Map} discord style letter abbreviation -> Intl.DateTimeFormat constructor options */ +const styles = new Map([ + ["t", {timeStyle: "short", timeZone: "UTC"}], + ["T", {timeStyle: "medium", timeZone: "UTC"}], + ["d", {dateStyle: "medium", timeZone: "UTC"}], + ["D", {dateStyle: "long", timeZone: "UTC"}], + ["f", {dateStyle: "long", timeStyle: "short", timeZone: "UTC"}], + ["F", {dateStyle: "full", timeStyle: "short", timeZone: "UTC"}], + ["s", {dateStyle: "medium", timeStyle: "short", timeZone: "UTC"}], + ["S", {dateStyle: "medium", timeStyle: "medium", timeZone: "UTC"}] +]) +const defaultStyle = "f" + +/** + * @param {string | null | undefined} [style] + * @returns {Intl.DateTimeFormatOptions} */ +function getFormatOptions(style) { + if (!style || !styles.has(style)) style = defaultStyle + const formatOptions = styles.get(style) + assert(formatOptions) + return formatOptions +} + +/** + * @param {number} timestamp unix time in seconds + * @param {string | null | undefined} [style] discord style letter abbreviation, if present + * @returns {string} + */ +function convertDiscordTime(timestamp, style) { + const formatter = new Intl.DateTimeFormat("en-NZ", getFormatOptions(style)) + const d = new Date(timestamp * 1000) + return formatter.format(d) + " (UTC)" +} + +module.exports.getFormatOptions = getFormatOptions +module.exports.convertDiscordTime = convertDiscordTime diff --git a/src/discord/time.test.js b/src/discord/time.test.js new file mode 100644 index 0000000..b889682 --- /dev/null +++ b/src/discord/time.test.js @@ -0,0 +1,48 @@ +// @ts-check + +const time = require("./time") +const {test} = require("supertape") + +const d = new Date("2026-02-14T14:23:06.000Z") +const seconds = d.getTime() / 1000 + +test("time: default style", t => { + t.equal(time.convertDiscordTime(seconds), "14 February 2026 at 2:23 pm (UTC)") +}) + +test("time: t style", t => { + t.equal(time.convertDiscordTime(seconds, "t"), "2:23 pm (UTC)") +}) + +test("time: T style", t => { + t.equal(time.convertDiscordTime(seconds, "T"), "2:23:06 pm (UTC)") +}) + +test("time: d style", t => { + t.equal(time.convertDiscordTime(seconds, "d"), "14 Feb 2026 (UTC)") +}) + +test("time: D style", t => { + t.equal(time.convertDiscordTime(seconds, "D"), "14 February 2026 (UTC)") +}) + +test("time: f style", t => { + t.equal(time.convertDiscordTime(seconds, "f"), "14 February 2026 at 2:23 pm (UTC)") +}) + +test("time: F style", t => { + t.equal(time.convertDiscordTime(seconds, "F"), "Saturday, 14 February 2026 at 2:23 pm (UTC)") +}) + +test("time: s style", t => { + t.equal(time.convertDiscordTime(seconds, "s"), "14 Feb 2026, 2:23 pm (UTC)") +}) + +test("time: S style", t => { + t.equal(time.convertDiscordTime(seconds, "S"), "14 Feb 2026, 2:23:06 pm (UTC)") +}) + +test("time: R style", t => { + // Discord would display this as relative time, but I don't want to do that in a static message. + t.equal(time.convertDiscordTime(seconds, "R"), "14 February 2026 at 2:23 pm (UTC)") +}) diff --git a/src/web/pug/time.pug b/src/web/pug/time.pug new file mode 100644 index 0000000..1d71c22 --- /dev/null +++ b/src/web/pug/time.pug @@ -0,0 +1,143 @@ +//- requires timestamp, text, style + +- const WIDTH = 600 + +doctype html +html(lang="en") + head + + + title= `Converting time ${text}...` + style. + body { + font-family: "Segoe UI", sans-serif; + margin: 0 auto; + padding: 4px; + font-size: 18px; + background-color: #f0f2f5; /* element compound tertiary off-white */ + max-width: !{WIDTH}px; + } + .only-script { + display: none; + } + .yes-script .only-script { + display: revert; + } + .local { + line-height: 1; + width: auto; + height: auto; + max-width: 100%; + } + .original { + color: #004c84; + } + .remaining { + font-weight: bold; + } + .primary-remaining { + font-size: 25px; + } + @media (prefers-color-scheme: dark) { + body { + color: #e3e3e3; + background-color: #131416; /* element compound bg but desaturated */ + } + .original { + color: #99c9ec; + } + .local { + fill: #fff; + } + .remaining { + color: #fff; + } + } + body + noscript + | You must enable JavaScript to convert to your local time. + .only-script + p In your local time, #[span.original= text] is... + if style === "R" + p.primary-remaining #[span#remaining.remaining]#[span#direction] #[span#tick.tick] + svg.local(viewBox=`0 -130 ${WIDTH} 253`) + text(style="font-size: 160px; font-weight: bold;")#localTime + text(y=70 style="font-size: 45px; font-weight: bold;")#localDate + text(y=115 style="font-size: 22px;")#localTz + if style !== "R" + p #[span#remaining.remaining]#[span#direction] #[span#tick.tick] + script. + document.body.classList.add("yes-script") + const WIDTH = !{WIDTH} + const timestamp = !{timestamp} + const style = "!{style}" + const formatOptions = !{JSON.stringify(formatOptions)} + const d = new Date(timestamp * 1000) + const UNITS = [ + ["week", 7*24*60*60*1000], + ["day", 24*60*60*1000], + ["hour", 60*60*1000], + ["minute", 60*1000], + ] + const showSeconds = formatOptions.timeStyle === "medium" || style === "R" + if (showSeconds) UNITS.push(["second", 1000]) + + if (formatOptions.timeStyle) { + localTime.textContent = new Intl.DateTimeFormat(undefined, {timeStyle: formatOptions.timeStyle}).format(d) + } else { + localTime.textContent = "Any time on" + localTime.style.fill = "#888" + } + if (formatOptions.dateStyle) { + localDate.textContent = new Intl.DateTimeFormat(undefined, {dateStyle: formatOptions.dateStyle}).format(d) + } else { + localDate.textContent = "On any date" + localDate.style.fill = "#888" + } + const localTzText = new Intl.DateTimeFormat(undefined, {timeZoneName: "long"}).formatToParts(d).find(p => p.type === "timeZoneName").value + const localTitleText = new Intl.DateTimeFormat(undefined, {...formatOptions, timeZone: undefined}).format(d) + document.title = localTitleText + localTz.textContent = localTzText + for (const st of [localTime, localDate, localTz]) { + const l = st.getComputedTextLength() + if (l > WIDTH) { + st.style.fontSize = Math.floor(parseInt(st.style.fontSize) * WIDTH / l) + "px"; + } + } + + let frame = 0 + function updateTimeRemaining() { + const now = new Date() + const timeForRemaining = new Date(d) + if (!formatOptions.dateStyle) { + timeForRemaining.setFullYear(now.getFullYear()) + timeForRemaining.setMonth(now.getMonth()) + timeForRemaining.setDate(now.getDate()) + } + let difference = Math.abs(timeForRemaining.getTime() - now.getTime()) + + let foundSignificantField = false + let remainingText = UNITS.map(([unit, duration], index) => { + const multiplier = Math.floor(difference / duration) + difference -= multiplier * duration + if (multiplier > 0 || foundSignificantField) { + foundSignificantField = true + return multiplier + "\u00a0" + unit + (multiplier !== 1 ? "s" : "") + } + }).filter(s => s).join(", ") + if (remainingText) { + remaining.textContent = remainingText + direction.textContent = + ( timeForRemaining.getTime() > now.getTime() + ? "\u00a0from\u00a0now." + : "\u00a0ago.") + } else { + remaining.textContent = "Right now!" + direction.textContent = "" + } + if (!showSeconds) { + tick.textContent = String.fromCodePoint(11028 + ++frame % 2) + } + } + updateTimeRemaining() + setInterval(updateTimeRemaining, 1000) diff --git a/src/web/routes/time.js b/src/web/routes/time.js new file mode 100644 index 0000000..ad89b58 --- /dev/null +++ b/src/web/routes/time.js @@ -0,0 +1,31 @@ +// @ts-check + +const {defineEventHandler, getValidatedRouterParams, H3Event, setResponseHeader} = require("h3") +const {as, db, sync} = require("../../passthrough") +const {z} = require("zod") + +/** @type {import("../pug-sync")} */ +const pugSync = sync.require("../pug-sync") +/** @type {import("../../discord/time")} */ +const discordTime = sync.require("../../discord/time") + +const schema = { + time: z.object({ + timestamp: z.coerce.number(), + style: z.string().optional() + }) +} + +as.router.get("/time/:timestamp", defineEventHandler(async event => { + const {timestamp} = await getValidatedRouterParams(event, schema.time.parse) + const formatOptions = discordTime.getFormatOptions() + const text = discordTime.convertDiscordTime(timestamp) + return pugSync.render(event, "time.pug", {text, timestamp, formatOptions}) +})) + +as.router.get("/time/:timestamp/:style", defineEventHandler(async event => { + const {timestamp, style} = await getValidatedRouterParams(event, schema.time.parse) + const formatOptions = discordTime.getFormatOptions(style) + const text = discordTime.convertDiscordTime(timestamp, style) + return pugSync.render(event, "time.pug", {text, timestamp, style, formatOptions}) +})) diff --git a/src/web/server.js b/src/web/server.js index 3c06ede..83aa588 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -141,3 +141,4 @@ sync.require("./routes/oauth") sync.require("./routes/opt-out") sync.require("./routes/password") sync.require("./routes/stats") +sync.require("./routes/time") diff --git a/test/data.js b/test/data.js index f441778..bcc9fdb 100644 --- a/test/data.js +++ b/test/data.js @@ -1732,6 +1732,56 @@ module.exports = { } ] }, + timestamps: { + type: 0, + content: "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "", + mentions: [], + mention_roles: [], + attachments: [], + embeds: [], + timestamp: "2026-10-04T09:34:27.909000+00:00", + edited_timestamp: null, + flags: 0, + components: [], + id: "1556238063774990499", + channel_id: "1160894080998461480", + author: { + id: "772659086046658620", + username: "cadence.worm", + avatar: "466df0c98b1af1e1388f595b4c1ad1b9", + discriminator: "0", + public_flags: 0, + flags: 0, + banner: null, + accent_color: null, + global_name: "cadence", + avatar_decoration_data: null, + collectibles: null, + display_name_styles: null, + vad_colors: null, + banner_color: null, + clan: { + identity_guild_id: "532245108070809601", + identity_enabled: true, + tag: "doll", + badge: "dba08126b4e810a0e096cc7cd5bc37f0" + }, + primary_guild: { + identity_guild_id: "532245108070809601", + identity_enabled: true, + tag: "doll", + badge: "dba08126b4e810a0e096cc7cd5bc37f0" + } + }, + pinned: false, + mention_everyone: false, + tts: false + }, unknown_role: { id: "1162374402785153106", type: 0, diff --git a/test/test.js b/test/test.js index 5079ed3..74d99b1 100644 --- a/test/test.js +++ b/test/test.js @@ -132,6 +132,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../src/db/orm.test") require("../src/js/errors.test") require("../src/web/server.test") + require("../src/discord/time.test") require("../src/discord/utils.test") require("../src/matrix/kstate.test") require("../src/matrix/api.test") From b49c6e8fd4a51900fc70da9dce30cadf6971e448 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 8 Oct 2026 02:19:26 +1300 Subject: [PATCH 5/6] Add //time command for m->d scheduling --- docs/developer-orientation.md | 4 +- package-lock.json | 20 ++++ package.json | 2 + src/matrix/matrix-command-handler.js | 137 +++++++++++++++++++++++++-- start.js | 5 + 5 files changed, 159 insertions(+), 9 deletions(-) diff --git a/docs/developer-orientation.md b/docs/developer-orientation.md index 839b4af..27ddb7d 100644 --- a/docs/developer-orientation.md +++ b/docs/developer-orientation.md @@ -89,7 +89,7 @@ Whether you read those or not, I'm more than happy to help you 1-on-1 with codin # Dependency justification -Total transitive production dependencies: 112 +Total transitive production dependencies: 114 ### 🦕 @@ -108,6 +108,7 @@ Total transitive production dependencies: 112 * (0) @cloudrac3r/mixin-deep: This is my fork. (It fixes a bug in regular mixin-deep.) * (0) @cloudrac3r/pngjs: Lottie stickers are converted to bitmaps with the vendored Rlottie WASM build, then the bitmaps are converted to PNG with pngjs. * (0) @cloudrac3r/stream-type: Determine type of Matrix files that don't specify it in info. Switched from stream-mime-type to this. +* (0) @cloudrac3r/tell-me-when: Parse English natural language dates and times for the m->d //time command. * (0) @cloudrac3r/turndown: This HTML-to-Markdown converter looked the most suitable. I forked it to change the escaping logic to match the way Discord works. * (3) @stackoverflow/stacks: Stack Overflow design language and icons. * (0) ansi-colors: Helps with interactive prompting for the initial setup, and it's already pulled in by enquirer. @@ -125,6 +126,7 @@ Total transitive production dependencies: 112 * (1) mime-types: List of mime type mappings. Needed to serve static files. * (0) prettier-bytes: It does what I want and has no dependencies. * (0) snowtransfer: Discord API library with bring-your-own-caching that I trust. +* (0) temporal-polyfill-lite: Needed for the //time command. The Temporal API has only been stabilised as of Node.js v26. * (0) try-to-catch: Not strictly necessary, but it's already pulled in by supertape, so I may as well. * (0) uqr: QR code SVG generator. Used on the website to scan in an invite link. * (0) xxhash-wasm: Used where cryptographically secure hashing is not required. diff --git a/package-lock.json b/package-lock.json index 32e7e88..5a60f1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@cloudrac3r/pngjs": "^7.0.3", "@cloudrac3r/pug": "^4.0.4", "@cloudrac3r/stream-type": "^1.0.0", + "@cloudrac3r/tell-me-when": "^2.0.0", "@cloudrac3r/turndown": "^7.1.5", "@stackoverflow/stacks": "^2.5.4", "@stackoverflow/stacks-icons": "^6.0.2", @@ -39,6 +40,7 @@ "prettier-bytes": "^1.0.4", "sharp": "^0.35.3", "snowtransfer": "^0.19.0", + "temporal-polyfill-lite": "^0.4.3", "try-to-catch": "^4.0.5", "uqr": "^0.1.2", "xxhash-wasm": "^1.0.2", @@ -280,6 +282,18 @@ "node": ">=8.0.0" } }, + "node_modules/@cloudrac3r/tell-me-when": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cloudrac3r/tell-me-when/-/tell-me-when-2.0.0.tgz", + "integrity": "sha512-U+TEOTe6f/vSIeD/QP0VPrkLrhnFtQs2euxBOAaqC8rzQKu8nNoKSFuhipT6DyuCZH48B58+qrPMmH2w2IgMhA==", + "license": "MIT", + "bin": { + "tell-me-when": "dist/src/cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@cloudrac3r/turndown": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/@cloudrac3r/turndown/-/turndown-7.1.5.tgz", @@ -2516,6 +2530,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/temporal-polyfill-lite": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/temporal-polyfill-lite/-/temporal-polyfill-lite-0.4.3.tgz", + "integrity": "sha512-Lw/de9tydKmrU0icQy6Z+abX5mzDk4iqyt6Xp2iDDxY6SGISiuhuP3TcfWlYveM1FN1I7FjT5nG+Kh9eCiSnVg==", + "license": "MIT" + }, "node_modules/test-exclude": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz", diff --git a/package.json b/package.json index 72004cf..4c9de0e 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@cloudrac3r/pngjs": "^7.0.3", "@cloudrac3r/pug": "^4.0.4", "@cloudrac3r/stream-type": "^1.0.0", + "@cloudrac3r/tell-me-when": "^2.0.0", "@cloudrac3r/turndown": "^7.1.5", "@stackoverflow/stacks": "^2.5.4", "@stackoverflow/stacks-icons": "^6.0.2", @@ -48,6 +49,7 @@ "prettier-bytes": "^1.0.4", "sharp": "^0.35.3", "snowtransfer": "^0.19.0", + "temporal-polyfill-lite": "^0.4.3", "try-to-catch": "^4.0.5", "uqr": "^0.1.2", "xxhash-wasm": "^1.0.2", diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index aa80b9c..de81899 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -3,8 +3,9 @@ const assert = require("assert").strict const DiscordTypes = require("discord-api-types/v10") const Ty = require("../types") -const {pipeline} = require("stream").promises const sharp = require("sharp") +const tw = require("@cloudrac3r/tell-me-when") +const {tag} = require("@cloudrac3r/html-template-tag") const {discord, sync, db, select} = require("../passthrough") /** @type {import("./api")}) */ @@ -13,8 +14,9 @@ const api = sync.require("./api") const mxUtils = sync.require("./utils") /** @type {import("../discord/utils")} */ const dUtils = sync.require("../discord/utils") -/** @type {import("./kstate")} */ -const ks = sync.require("./kstate") +/** @type {import("../d2m/actions/retrigger")} */ +const retrigger = sync.require("../d2m/actions/retrigger") + const {reg} = require("./read-registration") const PREFIXES = ["//", "/"] @@ -33,7 +35,17 @@ function getSlotCount(tier) { return TIER_EMOJI_SLOTS.get(tier) || 50 } -let buttons = [] +/** + * @typedef Button + * @property {string} roomID + * @property {string} eventID + * @property {string} mxid + * @property {string} key + * @property {(any) => any} resolve + * @property {number} created + */ + +const buttonStore = sync.remember(() => /** @type {{buttons: Button[]}} */ ({buttons: []})) /** * @param {string} roomID where to add the button @@ -51,22 +63,23 @@ async function addButton(roomID, eventID, key, mxid) { } }) return new Promise(resolve => { - buttons.push({roomID, eventID, mxid, key, resolve, created: Date.now()}) + buttonStore.buttons.push({roomID, eventID, mxid, key, 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) + buttonStore.buttons = buttonStore.buttons.filter(b => now - b.created < 2*60*60*1000) }, 10*60*1000).unref() /** @param {Ty.Event.Outer} event */ function onReactionAdd(event) { - const button = buttons.find(b => b.roomID === event.room_id && b.mxid === event.sender && b.eventID === event.content["m.relates_to"]?.event_id && b.key === event.content["m.relates_to"]?.key) + const button = buttonStore.buttons.find(b => b.roomID === event.room_id && b.mxid === event.sender && b.eventID === event.content["m.relates_to"]?.event_id && b.key === event.content["m.relates_to"]?.key) if (button) { - buttons = buttons.filter(b => b !== button) // remove button data so it can't be clicked again + buttonStore.buttons = buttonStore.buttons.filter(b => b !== button) // remove button data so it can't be clicked again button.resolve(event) + return true } } @@ -318,6 +331,114 @@ const commands = [{ }) } ) +}, { + aliases: ["time"], + execute: replyctx( + async (event, realBody, words, ctx) => { + // Guard + /** @type {string} */ // @ts-ignore + const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get() + const channel = discord.channels.get(channelID) + const guildID = channel?.["guild_id"] + if (!guildID) { + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + body: "This room isn't bridged to the other side." + }) + } + + // Get user timezone + const profile = await api.getProfile(event.sender) + const senderTimeZone = profile?.["m.tz"] + if (!senderTimeZone) { + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + body: "This command converts a time in your local time zone for everybody else. But I don't know your time zone. Please set your time zone (hopefully possible in your client's settings/account menu) and try again." + }) + } + + try { + var now = Temporal.Now.zonedDateTimeISO(senderTimeZone) + } catch (e) { + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + ...new mxUtils.MatrixStringBuilder().add( + `The time zone on your account, "${senderTimeZone}", is not valid. It needs to be an IANA time zone value, with a slash. See yours here: https://time.is/your_time_zone`, + tag`The time zone on your account, ${senderTimeZone}, is not valid. It needs to be an IANA time zone value with a slash. See yours here.` + ).get() + }) + } + + const input = words.slice(1).join(" ").trim() + if (!input) { + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + body: "This command converts a time in your local time zone for everybody else. Use this command with a time or date (English natural language)." + }) + } + + try { + var parsed = tw.parse(input) + var when = tw.tellMeWhen(input, {now}) + } catch (e) { + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + body: "I didn't understand your input. If you think your input was reasonable, please report this as a bug." + }) + } + + const messageID = select("event_message", "message_id", {event_id: event.event_id}).pluck().get() + + const hasDate = parsed.some(x => Array.isArray(x) && x[0].match(/year|month|week|date|day/i)) + const hasTime = parsed.some(x => Array.isArray(x) && x[0].match(/hour|minute|second/i)) + const colonStyle = + ( hasDate && hasTime ? ":F" + : hasDate ? ":D" + : ":t") + + /** @param {Temporal.ZonedDateTime} z */ + function convertOne(z) { + return "" + } + + if (Array.isArray(when)) { // convert range + var content = `From ${convertOne(when[0])} to ${convertOne(when[1])}` + } else { // convert single time + var content = `${convertOne(when)}` + } + + /** @type {DiscordTypes.RESTPostAPIChannelMessageJSONBody} */ + const message = { + flags: DiscordTypes.MessageFlags.IsComponentsV2, + components: [{ + type: DiscordTypes.ComponentType.Container, + components: [{ + type: DiscordTypes.ComponentType.TextDisplay, + content: `## ${content}\n-# Wrong? 🗑️ to delete` + }] + }] + } + if (messageID) { + Object.assign(message, { + message_reference: { + type: DiscordTypes.MessageReferenceType.Default, + message_id: messageID, + } + }) + } + const sent = await discord.snow.channel.createMessage(channelID, message) + if (!await retrigger.waitForMessage(sent.id)) return + const sentEventID = select("event_message", "event_id", {message_id: sent.id}, "ORDER BY reaction_part").pluck().get() + assert(sentEventID) + addButton(event.room_id, sentEventID, "🗑️", event.sender).then(() => { + discord.snow.channel.deleteMessage(channelID, sent.id).catch(() => {}) + }) + } + ) }] diff --git a/start.js b/start.js index 2ab79cb..728c8af 100755 --- a/start.js +++ b/start.js @@ -1,6 +1,11 @@ #!/usr/bin/env node // @ts-check +// Temporal API stabilised in Node.js 26. Previous versions can use `--harmony-temporal`, but that's jank, so have a polyfill. +if (!("Temporal" in globalThis)) { + require("temporal-polyfill-lite/global") +} + const fs = require("fs") const sqlite = require("better-sqlite3") const migrate = require("./src/db/migrate") From a5032776731a458f4678ea5f0ab6514374fde121 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 8 Oct 2026 02:54:46 +1300 Subject: [PATCH 6/6] Use hyperlinked times for scheduled events --- src/d2m/converters/message-to-event.js | 43 +++++++++++++++++--------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index f192aff..4314a0e 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -883,32 +883,47 @@ async function messageToEvent(message, guild, options = {}, di) { // Then scheduled events if (message.content && di?.snow) { - for (const match of [...message.content.matchAll(/discord\.gg\/([A-Za-z0-9]+)\?event=([0-9]{18,})/g)]) { // snowflake has minimum 18 because the events feature is at least that old + for (const match of [...message.content.matchAll(/discord\.gg\/([A-Za-z0-9]+)\?event=([0-9]{18,})|\/events\/([0-9]+)\/([0-9]{18,})/g)]) { // snowflake has minimum 18 because the events feature is at least that old + /** @type {DiscordTypes.APIGuildScheduledEvent | undefined} */ + let event let invite - try { - invite = await di.snow.invite.getInvite(match[1], {guild_scheduled_event_id: match[2]}) - } catch (e) { - // Skip expired/invalid invites and events - if (e.message === "Unknown Invite") { - break - } else { + + if (match[1]) { + try { + invite = await di.snow.invite.getInvite(match[1], {guild_scheduled_event_id: match[2]}) + event = invite.guild_scheduled_event + } catch (e) { + // Skip expired/invalid invites and events + if (e.message === "Unknown Invite") { + break + } else { + throw e + } + } + } else if (match[3]) { + try { + event = await di.snow.guildScheduledEvent.getGuildScheduledEvent(match[3], match[4]) + } catch (e) { throw e } } - const event = invite.guild_scheduled_event if (!event) continue // the event ID provided was not valid - const formatter = new Intl.DateTimeFormat("en-NZ", {month: "long", day: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "shortGeneric", timeZone: reg.ooye.time_zone}) // 9 June at 3:00 pm NZT const rep = new mxUtils.MatrixStringBuilder() + const unixSeconds = Math.floor(new Date(event.scheduled_start_time).getTime() / 1000) // Add time if (event.scheduled_end_time) { - // @ts-ignore - no definition available for formatRange - rep.addParagraph(`Scheduled Event - ${formatter.formatRange(new Date(event.scheduled_start_time), new Date(event.scheduled_end_time))}`) + const formatter = new Intl.DateTimeFormat("en-NZ", {month: "long", day: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "shortGeneric", timeZone: reg.ooye.time_zone}) // 9 June at 3:00 pm NZT + var displayTime = formatter.formatRange(new Date(event.scheduled_start_time), new Date(event.scheduled_end_time)) } else { - rep.addParagraph(`Scheduled Event - ${formatter.format(new Date(event.scheduled_start_time))}`) + var displayTime = discordTime.convertDiscordTime(unixSeconds, "F") } + rep.addParagraph( + `Scheduled Event - ${displayTime}`, + tag`Scheduled Event - ${displayTime}` + ) // Add details rep.addLine(`## ${event.name}`, tag`${event.name}`) @@ -917,7 +932,7 @@ async function messageToEvent(message, guild, options = {}, di) { // Add location if (event.entity_metadata?.location) { rep.addParagraph(`📍 ${event.entity_metadata.location}`) - } else if (invite.channel?.name) { + } else if (invite?.channel?.name) { const roomID = select("channel_room", "room_id", {channel_id: invite.channel.id}).pluck().get() if (roomID) { const via = await getViaServersMemo(roomID)