From f110ec735def72383ac36e97cc565abace86eaef Mon Sep 17 00:00:00 2001 From: Nick Hale <4175918+njhale@users.noreply.github.com> Date: Thu, 6 Mar 2025 20:36:37 -0500 Subject: [PATCH] fix: bsky create post links and tags Links and tags in posts created by the `Bluesky` bundle's `Create Post` tool are not being rendered correctly on bsky.app. This is because the posts were missing metadata that Bluesky requires to render them. To correct this, resolve and include the missing metadata with posts. Signed-off-by: Nick Hale <4175918+njhale@users.noreply.github.com> --- bluesky/package-lock.json | 325 +++++++++++++++++++++++++++++++++++--- bluesky/package.json | 3 +- bluesky/src/embed.ts | 64 ++++++++ bluesky/src/posts.ts | 19 ++- bluesky/src/tools.ts | 1 - bluesky/tool.gpt | 1 - 6 files changed, 380 insertions(+), 33 deletions(-) create mode 100644 bluesky/src/embed.ts diff --git a/bluesky/package-lock.json b/bluesky/package-lock.json index 5331ca3d..d538d21b 100644 --- a/bluesky/package-lock.json +++ b/bluesky/package-lock.json @@ -5,7 +5,8 @@ "packages": { "": { "dependencies": { - "@atproto/api": "^0.13.16", + "@atproto/api": "^0.14.7", + "cheerio": "^1.0.0", "ts-node-dev": "^2.0.0" }, "devDependencies": { @@ -21,15 +22,15 @@ } }, "node_modules/@atproto/api": { - "version": "0.13.16", - "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.13.16.tgz", - "integrity": "sha512-fWWPifh7DTiKs7v2n/trZSeqvHMQckJACbA0KjZuLksgAaQWJCO+X9rsegrAUmE2aPenvLLnK2NaPaYnj5WJBw==", + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.14.7.tgz", + "integrity": "sha512-YG2kvAtsgtajLlLrorYuHcxGgepG0c/RUB2/iJyBnwKjGqDLG8joOETf38JSNiGzs6NJbNKa9NHG6BQKourxBA==", "license": "MIT", "dependencies": { - "@atproto/common-web": "^0.3.1", - "@atproto/lexicon": "^0.4.3", - "@atproto/syntax": "^0.3.1", - "@atproto/xrpc": "^0.6.4", + "@atproto/common-web": "^0.4.0", + "@atproto/lexicon": "^0.4.7", + "@atproto/syntax": "^0.3.3", + "@atproto/xrpc": "^0.6.9", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", @@ -37,9 +38,9 @@ } }, "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==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.0.tgz", + "integrity": "sha512-ZYL0P9myHybNgwh/hBY0HaBzqiLR1B5/ie5bJpLQAg0whRzNA28t8/nU2vh99tbsWcAF0LOD29M8++LyENJLNQ==", "license": "MIT", "dependencies": { "graphemer": "^1.4.0", @@ -49,31 +50,31 @@ } }, "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==", + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.7.tgz", + "integrity": "sha512-/x6h3tAiDNzSi4eXtC8ke65B7UzsagtlGRHmUD95698x5lBRpDnpizj0fZWTZVYed5qnOmz/ZEue+v3wDmO61g==", "license": "MIT", "dependencies": { - "@atproto/common-web": "^0.3.1", - "@atproto/syntax": "^0.3.1", + "@atproto/common-web": "^0.4.0", + "@atproto/syntax": "^0.3.3", "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==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.3.3.tgz", + "integrity": "sha512-F1LZweesNYdBbZBXVa72N/cSvchG8Q1tG4/209ZXbIuM3FwQtkgn+zgmmV4P4ORmhOeXPBNXvMBpcqiwx/gEQQ==", "license": "MIT" }, "node_modules/@atproto/xrpc": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.6.4.tgz", - "integrity": "sha512-9ZAJ8nsXTqC4XFyS0E1Wlg7bAvonhXQNQ3Ocs1L1LIwFLXvsw/4fNpIHXxvXvqTCVeyHLbImOnE9UiO1c/qIYA==", + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.6.9.tgz", + "integrity": "sha512-vQGA7++DYMNaHx3C7vEjT+2X6hYYLG7JNbBnDLWu0km1/1KYXgRkAz4h+FfYqg1mvzvIorHU7DAs5wevkJDDlw==", "license": "MIT", "dependencies": { - "@atproto/lexicon": "^0.4.3", + "@atproto/lexicon": "^0.4.7", "zod": "^3.23.8" } }, @@ -852,6 +853,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -950,6 +957,48 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1033,6 +1082,34 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -1183,6 +1260,61 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dynamic-dedupe": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", @@ -1192,6 +1324,31 @@ "xtend": "^4.0.0" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.23.5", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.5.tgz", @@ -2278,6 +2435,37 @@ "node": ">= 0.4" } }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2855,6 +3043,18 @@ "node": ">=0.10.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-inspect": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", @@ -3021,6 +3221,43 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3286,6 +3523,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -3831,6 +4074,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", + "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", @@ -3853,6 +4105,27 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "license": "MIT" }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3954,9 +4227,9 @@ } }, "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==", + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/bluesky/package.json b/bluesky/package.json index a06e2f79..a5a76f2e 100644 --- a/bluesky/package.json +++ b/bluesky/package.json @@ -15,7 +15,8 @@ "typescript": "^5.4.5" }, "dependencies": { - "@atproto/api": "^0.13.16", + "@atproto/api": "^0.14.7", + "cheerio": "^1.0.0", "ts-node-dev": "^2.0.0" } } diff --git a/bluesky/src/embed.ts b/bluesky/src/embed.ts new file mode 100644 index 00000000..af3b7128 --- /dev/null +++ b/bluesky/src/embed.ts @@ -0,0 +1,64 @@ +import { AtpAgent, RichText } from "@atproto/api" +import * as cheerio from "cheerio" + +type Metadata = { + title: string + description: string + image: string +} + +/** + * Extract metadata from a URL's HTML content using cheerio + * @param url - The URL to extract metadata from + * @returns Metadata containing title, description, and image + */ +export const getUrlMetadata = async (url: string): Promise => { + const response = await fetch(url, { headers: { "User-Agent": "Mozilla/5.0" } }) + const html = await response.text() + const $ = cheerio.load(html) + + const title = $('meta[property="og:title"]').attr("content") || $("title").text() || "Untitled" + const description = $('meta[property="og:description"]').attr("content") || $('meta[name="description"]').attr("content") || "" + const image = $('meta[property="og:image"]').attr("content") || "" + + return { title, description, image } +} + +/** + * Make a best-effort attempt the Bluesky embed card for the first link in the RichText's facets. + * Returns undefined if the facets have no links or the first link's metadata could not be retrieved. + * @param rt - The RichText object containing potential URLs + * @param agent - The Bluesky agent + * @returns The embed card if possible + */ +export const getFirstEmbedCard = async (rt: RichText, agent: AtpAgent) => { + // Extract first URL from facets + const url = rt.facets?.find(facet => facet.features.some(f => f.$type === "app.bsky.richtext.facet#link")) + ?.features.find(f => f.$type === "app.bsky.richtext.facet#link" && 'uri' in f)?.uri + + if (!url) return + + try { + const metadata = await getUrlMetadata(url) + let thumbBlob + + if (metadata.image) { + const blob = await fetch(metadata.image).then(r => r.blob()) + const { data } = await agent.uploadBlob(blob, { encoding: "image/jpeg" }) + thumbBlob = data.blob + } + + return { + $type: "app.bsky.embed.external", + external: { + uri: url, + title: metadata.title, + description: metadata.description, + thumb: thumbBlob, + }, + } + } catch (error) { + console.error("Error fetching embed card:", error) + return + } +} \ No newline at end of file diff --git a/bluesky/src/posts.ts b/bluesky/src/posts.ts index 5f425474..701adcff 100644 --- a/bluesky/src/posts.ts +++ b/bluesky/src/posts.ts @@ -1,4 +1,5 @@ -import { AtpAgent, AppBskyFeedSearchPosts } from '@atproto/api' +import { RichText, AtpAgent, AppBskyFeedSearchPosts } from '@atproto/api' +import { getFirstEmbedCard } from "./embed.ts" export async function searchPosts ( agent: AtpAgent, @@ -53,14 +54,24 @@ export async function searchPosts ( console.log(JSON.stringify(response.data.posts)) } -export async function createPost(agent: AtpAgent, text?: string, tags?: string): Promise { +export async function createPost(agent: AtpAgent, text?: string): Promise { if (!text) { throw new Error('Text is required') } + // Replace all instances of \\n with \n + // The LLM sometimes double escapes newlines which breaks rending. + text = text.replace(/\\n/g, '\n') + + const rt = new RichText({ text }) + await rt.detectFacets(agent) + await agent.post({ - text, - tags: tags?.split(',').map(tag => tag.trim().replace(/^#/, '')) ?? [], + text: rt.text, + facets: rt.facets, + // Attempt to get the embed card for the first link in the text. + // This renders a preview of the page content in the Bluesky post. + embed: await getFirstEmbedCard(rt, agent), }) console.log('Post created') diff --git a/bluesky/src/tools.ts b/bluesky/src/tools.ts index 4203a82b..036ddc6c 100644 --- a/bluesky/src/tools.ts +++ b/bluesky/src/tools.ts @@ -38,7 +38,6 @@ try { await createPost( agent, process.env.TEXT, - process.env.TAGS, ) break case 'deletePost': diff --git a/bluesky/tool.gpt b/bluesky/tool.gpt index aa964001..b80b8b2d 100644 --- a/bluesky/tool.gpt +++ b/bluesky/tool.gpt @@ -37,7 +37,6 @@ Credential: ./credential JSON Response: true Share Context: Current Date and Time from ../time Param: text: The text of the post. -Param: tags: (optional) A comma separated list of tags to add to the post. For example, `#apple,#banana` will add both `#apple` and `#banana` to the post. #!/usr/bin/env npm --silent --prefix ${GPTSCRIPT_TOOL_DIR} run tool -- createPost