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:
parent
745023cfc2
commit
2f620bd46d
@ -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.
|
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
|
## Running the Server
|
||||||
|
|
||||||
|
@ -7,10 +7,12 @@
|
|||||||
"author": "dholms <dtholmgren@gmail.com>",
|
"author": "dholms <dtholmgren@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"publishFeed": "ts-node scripts/publishFeedGen.ts",
|
||||||
"start": "ts-node src/index.ts",
|
"start": "ts-node src/index.ts",
|
||||||
"build": "tsc"
|
"build": "tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@atproto/api": "^0.3.7",
|
||||||
"@atproto/did-resolver": "^0.1.0",
|
"@atproto/did-resolver": "^0.1.0",
|
||||||
"@atproto/lexicon": "^0.1.0",
|
"@atproto/lexicon": "^0.1.0",
|
||||||
"@atproto/repo": "^0.1.0",
|
"@atproto/repo": "^0.1.0",
|
||||||
|
89
scripts/publishFeedGen.ts
Normal file
89
scripts/publishFeedGen.ts
Normal 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
81
scripts/publishMany.ts
Normal 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()
|
43
yarn.lock
43
yarn.lock
@ -2,6 +2,17 @@
|
|||||||
# yarn lockfile v1
|
# 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@*":
|
"@atproto/common-web@*":
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.1.0.tgz"
|
resolved "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.1.0.tgz"
|
||||||
@ -108,6 +119,14 @@
|
|||||||
ws "^8.12.0"
|
ws "^8.12.0"
|
||||||
zod "^3.14.2"
|
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":
|
"@cbor-extract/cbor-extract-darwin-arm64@2.1.1":
|
||||||
version "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"
|
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"
|
resolved "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz"
|
||||||
integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==
|
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:
|
safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@~5.2.0:
|
||||||
version "5.2.1"
|
version "5.2.1"
|
||||||
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz"
|
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz"
|
||||||
@ -1147,6 +1173,11 @@ thread-stream@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
real-require "^0.2.0"
|
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:
|
toidentifier@1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz"
|
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"
|
v8-compile-cache-lib "^3.0.1"
|
||||||
yn "3.1.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:
|
tunnel-agent@^0.6.0:
|
||||||
version "0.6.0"
|
version "0.6.0"
|
||||||
resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz"
|
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"
|
media-typer "0.3.0"
|
||||||
mime-types "~2.1.24"
|
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:
|
typescript@^5.0.4:
|
||||||
version "5.0.4"
|
version "5.0.4"
|
||||||
resolved "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz"
|
resolved "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz"
|
||||||
|
Loading…
Reference in New Issue
Block a user