Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main'
Browse files Browse the repository at this point in the history
  • Loading branch information
dolciss committed Nov 18, 2024
2 parents 3ceea38 + c14c54b commit 103bfe7
Show file tree
Hide file tree
Showing 12 changed files with 1,073 additions and 586 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,7 @@ dist

# Sqlite Database
*.sqlite

# intellij
.idea/
*.iml
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 Bluesky PBLLC
Copyright (c) 2023–2024 Bluesky PBC

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This is a starter kit for creating ATProto Feed Generators. It's not feature com

Feed Generators are services that provide custom algorithms to users through the AT Protocol.

They work very simply: the server receives a request from a user's server and returns a list of [post URIs](https://atproto.com/specs/at-uri-scheme) with some optional metadata attached. Those posts are then hydrated into full views by the requesting server and sent back to the client. This route is described in the [`app.bsky.feed.getFeedSkeleton` lexicon](https://atproto.com/lexicons/app-bsky-feed#appbskyfeedgetfeedskeleton).
They work very simply: the server receives a request from a user's server and returns a list of [post URIs](https://atproto.com/specs/at-uri-scheme) with some optional metadata attached. Those posts are then hydrated into full views by the requesting server and sent back to the client. This route is described in the [`app.bsky.feed.getFeedSkeleton` lexicon](https://docs.bsky.app/docs/api/app-bsky-feed-get-feed-skeleton).

A Feed Generator service can host one or more algorithms. The service itself is identified by DID, while each algorithm that it hosts is declared by a record in the repo of the account that created it. For instance, feeds offered by Bluesky will likely be declared in `@bsky.app`'s repo. Therefore, a given algorithm is identified by the at-uri of the declaration record. This declaration record includes a pointer to the service's DID along with some profile information for the feed.

Expand Down
15 changes: 10 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"license": "MIT",
"scripts": {
"publishFeed": "ts-node scripts/publishFeedGen.ts",
"deleteFeed": "ts-node scripts/deleteFeedGen.ts",
"unpublishFeed": "ts-node scripts/unpublishFeedGen.ts",
"start": "ts-node src/index.ts --operation all",
"start-feedgen": "ts-node src/index.ts --operation feed",
"start-indexer": "ts-node src/index.ts --operation index",
Expand All @@ -20,19 +20,24 @@
"@atproto/lexicon": "^0.4.0",
"@atproto/repo": "^0.4.0",
"@atproto/syntax": "^0.3.0",
"@atproto/xrpc-server": "^0.5.1",
"better-sqlite3": "^8.3.0",
"@atproto/xrpc-server": "^0.6.0",
"better-sqlite3": "^11.3.0",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"kysely": "^0.22.0",
"kysely": "^0.27.4",
"multiformats": "^9.9.0",
"yargs": "^17.7.2"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.4",
"@types/better-sqlite3": "^7.6.11",
"@types/express": "^4.17.17",
"@types/node": "^20.1.2",
"inquirer": "^12.0.1",
"ts-node": "^10.9.1",
"typescript": "^5.0.4"
},
"engines": {
"node": ">= 18",
"yarn": "1"
}
}
40 changes: 0 additions & 40 deletions scripts/deleteFeedGen.ts

This file was deleted.

103 changes: 63 additions & 40 deletions scripts/publishFeedGen.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,75 @@
import dotenv from 'dotenv'
import inquirer from 'inquirer'
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 = 'l-tan.dolciss.net'
const handle = 'l-tan.bsky.social'

// YOUR bluesky password, or preferably an App Password (found in your client settings)
// Ex: abcd-1234-efgh-5678
if (!process.env.FEEDGEN_PUBLISH_APP_PASSWORD) {
throw new Error('Please provide an app password in the .env file')
if (!process.env.FEEDGEN_SERVICE_DID && !process.env.FEEDGEN_HOSTNAME) {
throw new Error('Please provide a hostname in the .env file')
}
const password = process.env.FEEDGEN_PUBLISH_APP_PASSWORD

// A short name for the record that will show in urls
// Lowercase with no spaces.
// Ex: whats-hot
const recordName = 'rp-next-post'
//const recordName = 'rp-next-post-p'

// A display name for your feed
// Ex: What's Hot
const displayName = 'RepostNextPost'
//const displayName = 'RepostNextPost+'
const answers = await inquirer
.prompt([
{
type: 'input',
name: 'handle',
message: 'Enter your Bluesky handle:',
//default: 'l-tan.dolciss.net',
default: 'l-tan.bsky.social',
required: true,
},
{
type: 'password',
name: 'password',
message: 'Enter your Bluesky password (preferably an App Password):',
},
{
type: 'input',
name: 'service',
message: 'Optionally, enter a custom PDS service to sign in with:',
default: 'https://bsky.social',
required: false,
},
{
type: 'input',
name: 'recordName',
message: 'Enter a short name or the record. This will be shown in the feed\'s URL:',
default: 'rp-next-post',
//default: 'rp-next-post-p',
required: true,
},
{
type: 'input',
name: 'displayName',
message: 'Enter a display name for your feed:',
default: 'RepostNextPost',
//default: 'RepostNextPost+',
required: true,
},
{
type: 'input',
name: 'description',
message: 'Optionally, enter a brief description of your feed:',
//default: '「リツイート直後のツイートを表示するやつ」にインスパイアされた「リポスト直後のポストが流れてくるフィード」です(個人運営のため突然のエラー等ご容赦ください)\nThis feed displays the Post immediately after the Repost (under private management)\nリポストも含めた「RepostNextPost+」もあります',
//default: '「リポスト直後のポスト(とリポスト)が流れてくるフィード」です\n㊟同じポストのリポストが省略されないクライアントでご利用ください\n(個人運営のため突然のエラー等ご容赦ください)\nThis feed displays the Post immediately after the Repost (and repost) (under private management)',
default: 'RepostNextPostのテストです',
required: false,
},
{
type: 'input',
name: 'avatar',
message: 'Optionally, enter a local path to an avatar that will be used for the feed:',
//default: './scripts/repost.png',
//default: './scripts/repost-p.png',
default: '',
required: false,
},
])

// (Optional) A description of your feed
// Ex: Top trending content from the whole network
//const description = '「リツイート直後のツイートを表示するやつ」にインスパイアされた「リポスト直後のポストが流れてくるフィード」です(個人運営のため突然のエラー等ご容赦ください)\nThis feed displays the Post immediately after the Repost (under private management)\nリポストも含めた「RepostNextPost+」もあります'
//const description = '「リポスト直後のポスト(とリポスト)が流れてくるフィード」です\n㊟同じポストのリポストが省略されないクライアントでご利用ください\n(個人運営のため突然のエラー等ご容赦ください)\nThis feed displays the Post immediately after the Repost (and repost) (under private management)'
const description = 'RepostNextPostのテストです'
const { handle, password, recordName, displayName, description, avatar, service } = answers

// (Optional) A description facets
/*
Expand Down Expand Up @@ -62,25 +98,12 @@ const run = async () => {
*/
const descriptionFacets = undefined

// (Optional) The path to an image to be used as your feed's avatar
// Ex: ~/path/to/avatar.jpeg
//const avatar: string = './scripts/repost.png'
//const avatar: string = './scripts/repost-p.png'
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 })
const agent = new AtpAgent({ service: service ? service : 'https://bsky.social' })
await agent.login({ identifier: handle, password})

let avatarRef: BlobRef | undefined
if (avatar) {
Expand Down
68 changes: 68 additions & 0 deletions scripts/unpublishFeedGen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import dotenv from 'dotenv'
import { AtpAgent, BlobRef } from '@atproto/api'
import fs from 'fs/promises'
import { ids } from '../src/lexicon/lexicons'
import inquirer from 'inquirer'

const run = async () => {
dotenv.config()

const answers = await inquirer
.prompt([
{
type: 'input',
name: 'handle',
message: 'Enter your Bluesky handle',
//default: 'l-tan.dolciss.net',
default: 'l-tan.bsky.social',
required: true,
},
{
type: 'password',
name: 'password',
message: 'Enter your Bluesky password (preferably an App Password):',
},
{
type: 'input',
name: 'service',
message: 'Optionally, enter a custom PDS service to sign in with:',
default: 'https://bsky.social',
required: false,
},
{
type: 'input',
name: 'recordName',
message: 'Enter the short name for the record you want to delete:',
default: 'rp-next-post',
//default: 'rp-next-post-p',
required: true,
},
{
type: 'confirm',
name: 'confirm',
message: 'Are you sure you want to delete this record? Any likes that your feed has will be lost:',
default: false,
}
])

const { handle, password, recordName, service, confirm } = answers

if (!confirm) {
console.log('Aborting...')
return
}

// only update this if in a test environment
const agent = new AtpAgent({ service: service ? service : 'https://bsky.social' })
await agent.login({ identifier: handle, password })

await agent.api.com.atproto.repo.deleteRecord({
repo: agent.session?.did ?? '',
collection: ids.AppBskyFeedGenerator,
rkey: recordName,
})

console.log('All done 🎉')
}

run()
6 changes: 4 additions & 2 deletions src/algos/rp-next-post-p.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ export const handler = async (ctx: AppContext, params: QueryParams, requester: s
}
const timeStr = new Date(parseInt(indexedAt, 10)).toISOString()
builder = builder
.where('post.indexedAt', '<', timeStr)
.orWhere((qb) => qb.where('post.indexedAt', '=', timeStr))
.where((eb) => eb.or([
eb('post.indexedAt', '<', timeStr),
eb('post.indexedAt', '=', timeStr)
]))
.where('post.cid', '<', cid)
}
const res = await builder.execute()
Expand Down
6 changes: 4 additions & 2 deletions src/algos/rp-next-post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ export const handler = async (ctx: AppContext, params: QueryParams, requester: s
}
const timeStr = new Date(parseInt(indexedAt, 10)).toISOString()
builder = builder
.where('post.indexedAt', '<', timeStr)
.orWhere((qb) => qb.where('post.indexedAt', '=', timeStr))
.where((eb) => eb.or([
eb('post.indexedAt', '<', timeStr),
eb('post.indexedAt', '=', timeStr)
]))
.where('post.cid', '<', cid)
}
const res = await builder.execute()
Expand Down
7 changes: 4 additions & 3 deletions src/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import express from 'express'
import { verifyJwt, AuthRequiredError } from '@atproto/xrpc-server'
import { verifyJwt, AuthRequiredError, parseReqNsid } from '@atproto/xrpc-server'
import { DidResolver } from '@atproto/identity'

export const validateAuth = async (
Expand All @@ -12,8 +12,9 @@ export const validateAuth = async (
throw new AuthRequiredError()
}
const jwt = authorization.replace('Bearer ', '').trim()
const payload = await verifyJwt(jwt, serviceDid, async (did: string) => {
const nsid = parseReqNsid(req)
const parsed = await verifyJwt(jwt, serviceDid, nsid, async (did: string) => {
return didResolver.resolveAtprotoKey(did)
})
return payload.iss
return parsed.iss
}
5 changes: 4 additions & 1 deletion src/util/subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ export abstract class FirehoseSubscriptionBase {
}
} catch (err) {
console.error('repo subscription errored', err)
setTimeout(() => this.run(subscriptionReconnectDelay), subscriptionReconnectDelay)
setTimeout(
() => this.run(subscriptionReconnectDelay),
subscriptionReconnectDelay,
)
}
}

Expand Down
Loading

0 comments on commit 103bfe7

Please sign in to comment.