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
This commit is contained in:
Daniel Holmgren 2023-05-19 10:33:12 -05:00 committed by GitHub
parent 745023cfc2
commit 2f620bd46d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 222 additions and 1 deletions

View File

@ -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

View File

@ -7,10 +7,12 @@
"author": "dholms <dtholmgren@gmail.com>",
"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",

89
scripts/publishFeedGen.ts Normal file
View File

@ -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()

81
scripts/publishMany.ts Normal file
View File

@ -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()

View File

@ -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"