From 2f620bd46d7f85e4c5b8e8580b84e9647f2e55a9 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Fri, 19 May 2023 10:33:12 -0500 Subject: [PATCH] Publish script (#21) * describeFeedGenerator route + multiple feeds * tweak readme * improve env * publish script * create -> put * readme * handle blob encoding * add check that feeds are available --- README.md | 8 +++- package.json | 2 + scripts/publishFeedGen.ts | 89 +++++++++++++++++++++++++++++++++++++++ scripts/publishMany.ts | 81 +++++++++++++++++++++++++++++++++++ yarn.lock | 43 +++++++++++++++++++ 5 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 scripts/publishFeedGen.ts create mode 100644 scripts/publishMany.ts diff --git a/README.md b/README.md index cc3156d..bebc99b 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,13 @@ Next you will need to do two things: We've taken care of setting this server up with a did:web. However, you're free to switch this out for did:plc if you like - you may want to if you expect this Feed Generator to be long-standing and possibly migrating domains. -Once the custom algorithms feature launches, you'll be able to publish your feed in-app by providing the DID of your service. +### Publishing your feed + +To publish your feed, go to the script at `scripts/publishFeedGen.ts` & fill in the variables at the top. Examples are included and some are optional. To publish your feed generator, simply run `yarn publishFeed`. + +To update your feed's display data (name, avatar, description, etc), just update the relevant variables & re-run the script. + +After successfully running the script, you should be able to see your feed from within the app, as well as share it by embedding a link in a post (similar to a quote post). ## Running the Server diff --git a/package.json b/package.json index c058179..c234f4c 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,12 @@ "author": "dholms ", "license": "MIT", "scripts": { + "publishFeed": "ts-node scripts/publishFeedGen.ts", "start": "ts-node src/index.ts", "build": "tsc" }, "dependencies": { + "@atproto/api": "^0.3.7", "@atproto/did-resolver": "^0.1.0", "@atproto/lexicon": "^0.1.0", "@atproto/repo": "^0.1.0", diff --git a/scripts/publishFeedGen.ts b/scripts/publishFeedGen.ts new file mode 100644 index 0000000..9f226ba --- /dev/null +++ b/scripts/publishFeedGen.ts @@ -0,0 +1,89 @@ +import dotenv from 'dotenv' +import { AtpAgent, BlobRef } from '@atproto/api' +import fs from 'fs/promises' +import { ids } from '../src/lexicon/lexicons' + +const run = async () => { + dotenv.config() + + // YOUR bluesky handle + // Ex: user.bsky.social + const handle = '' + + // YOUR bluesky password, or preferably an App Password (found in your client settings) + // Ex: abcd-1234-efgh-5678 + const password = '' + + // A short name for the record that will show in urls + // Lowercase with no spaces. + // Ex: whats-hot + const recordName = '' + + // A display name for your feed + // Ex: What's Hot + const displayName = '' + + // (Optional) A description of your feed + // Ex: Top trending content from the whole network + const description = '' + + // (Optional) The path to an image to be used as your feed's avatar + // Ex: ~/path/to/avatar.jpeg + const avatar: string = '' + + // ------------------------------------- + // NO NEED TO TOUCH ANYTHING BELOW HERE + // ------------------------------------- + + if (!process.env.FEEDGEN_SERVICE_DID && !process.env.FEEDGEN_HOSTNAME) { + throw new Error('Please provide a hostname in the .env file') + } + const feedGenDid = + process.env.FEEDGEN_SERVICE_DID ?? `did:web:${process.env.FEEDGEN_HOSTNAME}` + + // only update this if in a test environment + const agent = new AtpAgent({ service: 'https://bsky.social' }) + await agent.login({ identifier: handle, password }) + + try { + await agent.api.app.bsky.feed.describeFeedGenerator() + } catch (err) { + throw new Error( + 'The bluesky server is not ready to accept published custom feeds yet', + ) + } + + let avatarRef: BlobRef | undefined + if (avatar) { + let encoding: string + if (avatar.endsWith('png')) { + encoding = 'image/png' + } else if (avatar.endsWith('jpg') || avatar.endsWith('jpeg')) { + encoding = 'image/jpeg' + } else { + throw new Error('expected png or jpeg') + } + const img = await fs.readFile(avatar) + const blobRes = await agent.api.com.atproto.repo.uploadBlob(img, { + encoding, + }) + avatarRef = blobRes.data.blob + } + + await agent.api.com.atproto.repo.putRecord({ + repo: agent.session?.did ?? '', + collection: ids.AppBskyFeedGenerator, + rkey: recordName, + record: { + did: feedGenDid, + displayName: displayName, + description: description, + avatar: avatarRef, + createdAt: new Date().toISOString(), + }, + }) + + console.log('All done 🎉') +} + +run() diff --git a/scripts/publishMany.ts b/scripts/publishMany.ts new file mode 100644 index 0000000..ff96685 --- /dev/null +++ b/scripts/publishMany.ts @@ -0,0 +1,81 @@ +import { AtpAgent, BlobRef } from '@atproto/api' +import fs from 'fs/promises' +import { ids } from '../src/lexicon/lexicons' + +const run = async () => { + const handle = 'bsky.app' + const password = 'abcd-1234-4321-dcba' // ask emily for app password + const feedGenDid = '' + + const agent = new AtpAgent({ service: 'https://bsky.social' }) + await agent.login({ identifier: handle, password }) + + await publishGen( + agent, + feedGenDid, + 'whats-hot', + `What's Hot`, + 'Top trending content from the whole network', + './whats-hot.jpg', + ) + + await publishGen( + agent, + feedGenDid, + 'hot-classic', + `What's Hot Classic`, + `The original What's Hot experience`, + './hot-classic.jpg', + ) + + await publishGen( + agent, + feedGenDid, + 'bsky-team', + `Bluesky Team`, + 'Posts from members of the Bluesky Team', + './bsky-team.jpg', + ) + + await publishGen( + agent, + feedGenDid, + 'with-friends', + `Popular With Friends`, + 'A mix of popular content from accounts you follow and content that your follows like.', + './with-friends.jpg', + ) + + console.log('All done 🎉') +} + +const publishGen = async ( + agent: AtpAgent, + feedGenDid: string, + recordName: string, + displayName: string, + description: string, + avatar: string, +) => { + let avatarRef: BlobRef | undefined + if (avatar) { + const img = await fs.readFile(avatar) + const blobRes = await agent.api.com.atproto.repo.uploadBlob(img) + avatarRef = blobRes.data.blob + } + + await agent.api.com.atproto.repo.putRecord({ + repo: agent.session?.did ?? '', + collection: ids.AppBskyFeedGenerator, + rkey: recordName, + record: { + did: feedGenDid, + displayName: displayName, + description: description, + avatar: avatarRef, + createdAt: new Date().toISOString(), + }, + }) +} + +run() diff --git a/yarn.lock b/yarn.lock index ef4f11f..32c0bde 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,17 @@ # yarn lockfile v1 +"@atproto/api@^0.3.7": + version "0.3.7" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.3.7.tgz#5cc4b0ccc5c6690eb0e5a3ae138a84ce20697e2f" + integrity sha512-JHN3rHNGro4AaJWU64hsmpTUzd2+FbfMBiDkqyBmoKtj972ueBJeH8tz6WdnPcsIRfCj1kRthKFj2yJwgt6aSQ== + dependencies: + "@atproto/common-web" "*" + "@atproto/uri" "*" + "@atproto/xrpc" "*" + tlds "^1.234.0" + typed-emitter "^2.1.0" + "@atproto/common-web@*": version "0.1.0" resolved "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.1.0.tgz" @@ -108,6 +119,14 @@ ws "^8.12.0" zod "^3.14.2" +"@atproto/xrpc@*": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.1.0.tgz#798569095538ac060475ae51f1b4c071ff8776d6" + integrity sha512-LhBeZkQwPezjEtricGYnG62udFglOqlnmMSS0KyWgEAPi4KMp4H2F4jNoXcf5NPtZ9S4N4hJaErHX4PJYv2lfA== + dependencies: + "@atproto/lexicon" "*" + zod "^3.14.2" + "@cbor-extract/cbor-extract-darwin-arm64@2.1.1": version "2.1.1" resolved "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.1.1.tgz" @@ -1011,6 +1030,13 @@ real-require@^0.2.0: resolved "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz" integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== +rxjs@^7.5.2: + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== + dependencies: + tslib "^2.1.0" + safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" @@ -1147,6 +1173,11 @@ thread-stream@^2.0.0: dependencies: real-require "^0.2.0" +tlds@^1.234.0: + version "1.238.0" + resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.238.0.tgz#ffe7c19c8940c35b497cda187a6927f9450325a4" + integrity sha512-lFPF9pZFhLrPodaJ0wt9QIN0l8jOxqmUezGZnm7BfkDSVd9q667oVIJukLVzhF+4oW7uDlrLlfJrL5yu9RWwew== + toidentifier@1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" @@ -1171,6 +1202,11 @@ ts-node@^10.9.1: v8-compile-cache-lib "^3.0.1" yn "3.1.1" +tslib@^2.1.0: + version "2.5.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.2.tgz#1b6f07185c881557b0ffa84b111a0106989e8338" + integrity sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA== + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz" @@ -1186,6 +1222,13 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +typed-emitter@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/typed-emitter/-/typed-emitter-2.1.0.tgz#ca78e3d8ef1476f228f548d62e04e3d4d3fd77fb" + integrity sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA== + optionalDependencies: + rxjs "^7.5.2" + typescript@^5.0.4: version "5.0.4" resolved "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz"