From 71975b4f9d0febeeb9f8ba8e824bfa7ab22118f0 Mon Sep 17 00:00:00 2001 From: Paul Robert Lloyd Date: Mon, 13 May 2024 01:07:49 +0200 Subject: [PATCH] feat(syndicator-atproto): at protocol syndicator --- indiekit.config.js | 7 + package-lock.json | 453 ++++++-------------- packages/syndicator-atproto/README.md | 33 ++ packages/syndicator-atproto/assets/icon.svg | 4 + packages/syndicator-atproto/index.js | 97 +++++ packages/syndicator-atproto/lib/atproto.js | 49 +++ packages/syndicator-atproto/lib/utils.js | 79 ++++ packages/syndicator-atproto/package.json | 47 ++ packages/syndicator-atproto/test/index.js | 56 +++ 9 files changed, 498 insertions(+), 327 deletions(-) create mode 100644 packages/syndicator-atproto/README.md create mode 100644 packages/syndicator-atproto/assets/icon.svg create mode 100644 packages/syndicator-atproto/index.js create mode 100644 packages/syndicator-atproto/lib/atproto.js create mode 100644 packages/syndicator-atproto/lib/utils.js create mode 100644 packages/syndicator-atproto/package.json create mode 100644 packages/syndicator-atproto/test/index.js diff --git a/indiekit.config.js b/indiekit.config.js index 483f836ed..8dd706663 100644 --- a/indiekit.config.js +++ b/indiekit.config.js @@ -26,6 +26,7 @@ const config = { "@indiekit/post-type-video", "@indiekit/preset-eleventy", "@indiekit/store-github", + "@indiekit/syndicator-atproto", "@indiekit/syndicator-internet-archive", "@indiekit/syndicator-mastodon", ], @@ -86,6 +87,12 @@ const config = { endpoint: process.env.S3_ENDPOINT, bucket: process.env.S3_BUCKET, }, + "@indiekit/syndicator-atproto": { + checked: true, + url: process.env.ATPROTO_URL, + user: process.env.ATPROTO_USER, + password: process.env.ATPROTO_PASSWORD, + }, "@indiekit/syndicator-internet-archive": { checked: false, accessKey: process.env.INTERNET_ARCHIVE_ACCESS_KEY, diff --git a/package-lock.json b/package-lock.json index 6a40dfa1e..7c1cd87c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -347,6 +347,62 @@ "node": ">= 14.0.0" } }, + "node_modules/@atproto/api": { + "version": "0.12.29", + "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.12.29.tgz", + "integrity": "sha512-PyzPLjGWR0qNOMrmj3Nt3N5NuuANSgOk/33Bu3j+rFjjPrHvk9CI6iQPU6zuDaDCoyOTRJRafw8X/aMQw+ilgw==", + "license": "MIT", + "dependencies": { + "@atproto/common-web": "^0.3.0", + "@atproto/lexicon": "^0.4.0", + "@atproto/syntax": "^0.3.0", + "@atproto/xrpc": "^0.5.0", + "await-lock": "^2.2.2", + "multiformats": "^9.9.0", + "tlds": "^1.234.0" + } + }, + "node_modules/@atproto/common-web": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.3.1.tgz", + "integrity": "sha512-N7wiTnus5vAr+lT//0y8m/FaHHLJ9LpGuEwkwDAeV3LCiPif4m/FS8x/QOYrx1PdZQwKso95RAPzCGWQBH5j6Q==", + "license": "MIT", + "dependencies": { + "graphemer": "^1.4.0", + "multiformats": "^9.9.0", + "uint8arrays": "3.0.0", + "zod": "^3.23.8" + } + }, + "node_modules/@atproto/lexicon": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.3.tgz", + "integrity": "sha512-lFVZXe1S1pJP0dcxvJuHP3r/a+EAIBwwU7jUK+r8iLhIja+ml6NmYv8KeFHmIJATh03spEQ9s02duDmFVdCoXg==", + "license": "MIT", + "dependencies": { + "@atproto/common-web": "^0.3.1", + "@atproto/syntax": "^0.3.1", + "iso-datestring-validator": "^2.2.2", + "multiformats": "^9.9.0", + "zod": "^3.23.8" + } + }, + "node_modules/@atproto/syntax": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.3.1.tgz", + "integrity": "sha512-fzW0Mg1QUOVCWUD3RgEsDt6d1OZ6DdFmbKcDdbzUfh0t4rhtRAC05KbZYmxuMPWDAiJ4BbbQ5dkAc/mNypMXkw==", + "license": "MIT" + }, + "node_modules/@atproto/xrpc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.5.0.tgz", + "integrity": "sha512-swu+wyOLvYW4l3n+VAuJbHcPcES+tin2Lsrp8Bw5aIXIICiuFn1YMFlwK9JwVUzTH21Py1s1nHEjr4CJeElJog==", + "license": "MIT", + "dependencies": { + "@atproto/lexicon": "^0.4.0", + "zod": "^3.21.4" + } + }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", "license": "Apache-2.0", @@ -1931,28 +1987,6 @@ "@img/sharp-libvips-darwin-arm64": "1.0.4" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" - } - }, "node_modules/@img/sharp-libvips-darwin-arm64": { "version": "1.0.4", "cpu": [ @@ -1967,307 +2001,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.2.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@indiekit-test/config": { "resolved": "helpers/config", "link": true @@ -2452,6 +2185,10 @@ "resolved": "packages/store-s3", "link": true }, + "node_modules/@indiekit/syndicator-atproto": { + "resolved": "packages/syndicator-atproto", + "link": true + }, "node_modules/@indiekit/syndicator-internet-archive": { "resolved": "packages/syndicator-internet-archive", "link": true @@ -5450,6 +5187,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/await-lock": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz", + "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==", + "license": "MIT" + }, "node_modules/aws-sdk-client-mock": { "version": "4.1.0", "dev": true, @@ -10080,6 +9823,12 @@ "version": "4.2.11", "license": "ISC" }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "license": "MIT" + }, "node_modules/h3": { "version": "1.13.0", "license": "MIT", @@ -11201,6 +10950,12 @@ "node": ">=6.0" } }, + "node_modules/iso-datestring-validator": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz", + "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==", + "license": "MIT" + }, "node_modules/isobject": { "version": "3.0.1", "license": "MIT", @@ -11820,8 +11575,6 @@ }, "node_modules/lightningcss-linux-x64-gnu": { "version": "1.28.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.28.1.tgz", - "integrity": "sha512-7vWDISaMUn+oo2TwRdf2hl/BLdPxvywv9JKEqNZB/0K7bXwV4XE9wN/C2sAp1gGuh6QBA8lpjF4JIPt3HNlCHA==", "cpu": [ "x64" ], @@ -13301,6 +13054,12 @@ "version": "2.1.3", "license": "MIT" }, + "node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", + "license": "(Apache-2.0 AND MIT)" + }, "node_modules/multimatch": { "version": "5.0.0", "license": "MIT", @@ -15731,8 +15490,6 @@ }, "node_modules/sharp": { "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -16889,6 +16646,15 @@ "dev": true, "license": "MIT" }, + "node_modules/tlds": { + "version": "1.255.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.255.0.tgz", + "integrity": "sha512-tcwMRIioTcF/FcxLev8MJWxCp+GUALRhFEqbDoZrnowmKSGqPrl5pqS+Sut2m8BgJ6S4FExCSSpGffZ0Tks6Aw==", + "license": "MIT", + "bin": { + "tlds": "bin.js" + } + }, "node_modules/tldts": { "version": "6.1.61", "dev": true, @@ -17221,6 +16987,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/uint8arrays": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz", + "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", + "license": "MIT", + "dependencies": { + "multiformats": "^9.4.2" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "dev": true, @@ -18151,6 +17926,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zwitch": { "version": "2.0.4", "dev": true, @@ -18796,6 +18580,21 @@ "node": ">=20" } }, + "packages/syndicator-atproto": { + "name": "@indiekit/syndicator-atproto", + "version": "1.0.0-beta.16", + "license": "MIT", + "dependencies": { + "@atproto/api": "^0.12.9", + "@indiekit/error": "^1.0.0-beta.15", + "@indiekit/util": "^1.0.0-beta.16", + "brevity": "^0.2.9", + "html-to-text": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, "packages/syndicator-internet-archive": { "name": "@indiekit/syndicator-internet-archive", "version": "1.0.0-beta.15", diff --git a/packages/syndicator-atproto/README.md b/packages/syndicator-atproto/README.md new file mode 100644 index 000000000..afb5ab657 --- /dev/null +++ b/packages/syndicator-atproto/README.md @@ -0,0 +1,33 @@ +# @indiekit/syndicator-atproto + +[AT Protocol](https://atproto.com) syndicator for Indiekit. + +## Installation + +`npm i @indiekit/syndicator-atproto` + +## Usage + +Add `@indiekit/syndicator-atproto` to your list of plug-ins, specifying options as required: + +```json +{ + "plugins": ["@indiekit/syndicator-atproto"], + "@indiekit/syndicator-atproto": { + "url": "https://bsky.social", + "user": "username.bsky.social", + "password": "password", + "checked": true + } +} +``` + +## Options + +| Option | Type | Description | +| :----------------- | :-------- | :------------------------------------------------------------------------------------------------------------ | +| `password` | `string` | Your AT protocol password. _Required_, defaults to `process.env.ATPROTO_PASSWORD`. | +| `url` | `string` | Your AT protocol service, i.e. `https://bsky.social`. _Required_. | +| `user` | `string` | Your AT protocol identifier (without the `@`). _Required_. | +| `checked` | `boolean` | Tell a Micropub client whether this syndicator should be enabled by default. _Optional_, defaults to `false`. | +| `includePermalink` | `boolean` | Always include a link to the original post. _Optional_, defaults to `false`. | diff --git a/packages/syndicator-atproto/assets/icon.svg b/packages/syndicator-atproto/assets/icon.svg new file mode 100644 index 000000000..8ed5e1d70 --- /dev/null +++ b/packages/syndicator-atproto/assets/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/syndicator-atproto/index.js b/packages/syndicator-atproto/index.js new file mode 100644 index 000000000..b22eaa8a0 --- /dev/null +++ b/packages/syndicator-atproto/index.js @@ -0,0 +1,97 @@ +import process from "node:process"; +import { IndiekitError } from "@indiekit/error"; +import { atproto } from "./lib/atproto.js"; + +const defaults = { + checked: false, + includePermalink: false, + password: process.env.ATPROTO_PASSWORD, + user: "", +}; + +export default class AtProtoSyndicator { + /** + * @param {object} [options] - Plug-in options + * @param {boolean} [options.includePermalink] - Include permalink in status + * @param {string} [options.password] - Password + * @param {string} [options.url] - Server URL + * @param {string} [options.user] - Username + * @param {boolean} [options.checked] - Check syndicator in UI + */ + constructor(options = {}) { + this.name = "AT Protocol syndicator"; + this.options = { ...defaults, ...options }; + } + + get #url() { + return this.options?.url ? new URL(this.options.url) : false; + } + + get #user() { + return this.options?.user + ? `@${this.options.user.replace("@", "")}` + : false; + } + + get environment() { + return ["ATPROTO_PASSWORD"]; + } + + get info() { + const service = { + name: "AT Protocol", + photo: "/assets/@indiekit-atproto/icon.svg", + }; + const user = this.#user; + const url = this.#url; + + if (!url) { + return { + error: "Service URL required", + service, + }; + } + + if (!user) { + return { + error: "User identifier required", + service, + }; + } + + const uid = `${url.protocol}//${this.options.user.replace("@", "")}`; + service.url = url.href; + + return { + checked: this.options.checked, + name: user, + uid, + service, + user: { + name: user, + url: uid, + }, + }; + } + + async syndicate(properties, publication) { + try { + return await atproto({ + identifier: this.options.user, + password: this.options.password, + includePermalink: this.options.includePermalink, + service: `${this.#url.protocol}//${this.#url.hostname}`, + }).post(properties, publication.me); + } catch (error) { + throw new IndiekitError(error.message, { + cause: error, + plugin: this.name, + status: error.statusCode, + }); + } + } + + init(Indiekit) { + Indiekit.addSyndicator(this); + } +} diff --git a/packages/syndicator-atproto/lib/atproto.js b/packages/syndicator-atproto/lib/atproto.js new file mode 100644 index 000000000..0c7aa926c --- /dev/null +++ b/packages/syndicator-atproto/lib/atproto.js @@ -0,0 +1,49 @@ +import { BskyAgent, RichText } from "@atproto/api"; +import { getStatusText } from "./utils.js"; + +/** + * Syndicate post to an AT Protocol service + * @param {object} options - Syndicator options + * @param {string} options.identifier - User identifier + * @param {string} options.password - Password + * @param {boolean} options.includePermalink - Include permalink in status + * @param {string} options.service - Service URL + * @returns {object} Post functions + */ +export const atproto = ({ + identifier, + password, + includePermalink, + service, +}) => ({ + async client() { + const agent = new BskyAgent({ service }); + + await agent.login({ identifier, password }); + + return agent; + }, + + /** + * Post to AT Protocol + * @param {object} properties - JF2 properties + * @returns {Promise} URL of syndicated status + */ + async post(properties) { + const client = await this.client(); + const text = getStatusText(properties, { + includePermalink, + service, + }); + + const rt = new RichText({ text }); + await rt.detectFacets(client); + + return client.post({ + $type: "app.bsky.feed.post", + text: rt.text, + facets: rt.facets, + createdAt: new Date().toISOString(), + }); + }, +}); diff --git a/packages/syndicator-atproto/lib/utils.js b/packages/syndicator-atproto/lib/utils.js new file mode 100644 index 000000000..f61ea45f2 --- /dev/null +++ b/packages/syndicator-atproto/lib/utils.js @@ -0,0 +1,79 @@ +import brevity from "brevity"; +import { htmlToText } from "html-to-text"; + +/** + * Get status parameters from given JF2 properties + * @param {object} properties - JF2 properties + * @param {object} [options] - Options + * @param {boolean} [options.includePermalink] - Include permalink in status + * @param {string} [options.service] - Server URL + * @returns {string} Status text + */ +export const getStatusText = (properties, options = {}) => { + const { includePermalink, service } = options; + + let text; + if (properties.content && properties.content.html) { + text = htmlToStatusText(properties.content.html, service); + } + + // Truncate status if longer than 300 characters + text = brevity.shorten( + text, + properties.url, + includePermalink // https://indieweb.org/permashortlink + ? properties.url + : false, + false, // https://indieweb.org/permashortcitation + 300, + ); + + // Show permalink below status, not within brackets + text = text.replace(`(${properties.url})`, `\n\n${properties.url}`); + + return text; +}; + +/** + * Convert HTML to plain text, appending last link href if present + * @param {string} html - HTML + * @param {string} serverUrl - Server URL, i.e. https://mastodon.social + * @returns {string} Text + */ +export const htmlToStatusText = (html, serverUrl) => { + // Get all the link references + let hrefs = [...html.matchAll(/href="(https?:\/\/.+?)"/g)]; + + // Remove any links to Mastodon server + // HTML may contain Mastodon usernames or hashtag links + hrefs = hrefs.filter((href) => { + const hrefHostname = new URL(href[1]).hostname; + const serverHostname = new URL(serverUrl).hostname; + return hrefHostname !== serverHostname; + }); + + // Get the last link mentioned, or return false + const lastHref = hrefs.length > 0 ? hrefs.at(-1)[1] : false; + + // Convert HTML to plain text, removing any links + const text = htmlToText(html, { + selectors: [ + { + selector: "a", + options: { + ignoreHref: true, + }, + }, + { + selector: "img", + format: "skip", + }, + ], + wordwrap: false, + }); + + // Append the last link if present + const statusText = lastHref ? `${text} ${lastHref}` : text; + + return statusText; +}; diff --git a/packages/syndicator-atproto/package.json b/packages/syndicator-atproto/package.json new file mode 100644 index 000000000..81e050c6e --- /dev/null +++ b/packages/syndicator-atproto/package.json @@ -0,0 +1,47 @@ +{ + "name": "@indiekit/syndicator-atproto", + "version": "1.0.0-beta.16", + "description": "AT Protocol syndicator for Indiekit", + "keywords": [ + "indiekit", + "indiekit-plugin", + "indieweb", + "syndication", + "at-protocol", + "atproto" + ], + "homepage": "https://getindiekit.com", + "author": { + "name": "Paul Robert Lloyd", + "url": "https://paulrobertlloyd.com" + }, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "type": "module", + "main": "index.js", + "files": [ + "assets", + "lib", + "index.js" + ], + "bugs": { + "url": "https://github.com/getindiekit/indiekit/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/getindiekit/indiekit.git", + "directory": "packages/syndicator-atproto" + }, + "dependencies": { + "@atproto/api": "^0.12.9", + "@indiekit/error": "^1.0.0-beta.15", + "@indiekit/util": "^1.0.0-beta.16", + "brevity": "^0.2.9", + "html-to-text": "^9.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/syndicator-atproto/test/index.js b/packages/syndicator-atproto/test/index.js new file mode 100644 index 000000000..cd9efd33c --- /dev/null +++ b/packages/syndicator-atproto/test/index.js @@ -0,0 +1,56 @@ +import { strict as assert } from "node:assert"; +import { describe, it } from "node:test"; +import { Indiekit } from "@indiekit/indiekit"; +import AtProtoSyndicator from "../index.js"; + +describe("syndicator-atproto", () => { + const atproto = new AtProtoSyndicator({ + password: "password", + url: "https://butterfly.example", + user: "username.butterfly.example", + }); + + it("Gets plug-in environment", () => { + assert.deepEqual(atproto.environment, ["ATPROTO_PASSWORD"]); + }); + + it("Gets plug-in info", () => { + assert.equal(atproto.name, "AT Protocol syndicator"); + assert.equal(atproto.info.checked, false); + assert.equal(atproto.info.name, "@username.butterfly.example"); + assert.equal(atproto.info.uid, "https://username.butterfly.example"); + assert.ok(atproto.info.service); + }); + + it("Returns error information if no server URL provided", async () => { + const result = new AtProtoSyndicator({ + password: "password", + user: "username", + }); + + assert.equal(result.info.error, "Service URL required"); + }); + + it("Returns error information if no username provided", () => { + const result = new AtProtoSyndicator({ + password: "password", + url: "https://username.butterfly.example", + }); + + assert.equal(result.info.error, "User identifier required"); + }); + + it("Initiates plug-in", async () => { + const indiekit = await Indiekit.initialize({ config: {} }); + atproto.init(indiekit); + + assert.equal( + indiekit.publication.syndicationTargets[0].info.name, + "@username.butterfly.example", + ); + }); + + it.todo("Returns syndicated URL"); + + it.todo("Throws error getting syndicated URL if access token invalid"); +});