Add describeFeedGenerator route + multiple feeds (#19)

* describeFeedGenerator route + multiple feeds

* tweak readme
This commit is contained in:
Daniel Holmgren 2023-05-19 10:31:28 -05:00 committed by GitHub
parent 285ef14a68
commit 3606414b79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 710 additions and 161 deletions

View File

@ -36,11 +36,11 @@ Next you will need to do two things:
This will subscribe to the repo subscription stream on startup, parse events & index them according to your provided logic.
2. Implement feed generation logic in `src/feed-generation.ts`
The types are in place and you will just need to return something that satisfies the `SkeletonFeedPost[]` type.
2. Implement feed generation logic in `src/algos`
For inspiration, we've provided a very simple feed algorithm ("whats alf") that returns all posts related to the titular character of the TV show ALF.
For inspiration, we've provided a very simple feed algorithm (`whats-alf`) that returns all posts related to the titular character of the TV show ALF.
You can either edit it or add another algorithm alongside it. The types are in place an dyou will just need to return something that satisfies the `SkeletonFeedPost[]` type.
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.

14
src/algos/index.ts Normal file
View File

@ -0,0 +1,14 @@
import { AppContext } from '../config'
import {
QueryParams,
OutputSchema as AlgoOutput,
} from '../lexicon/types/app/bsky/feed/getFeedSkeleton'
import * as whatsAlf from './whats-alf'
type AlgoHandler = (ctx: AppContext, params: QueryParams) => Promise<AlgoOutput>
const algos: Record<string, AlgoHandler> = {
[whatsAlf.uri]: whatsAlf.handler,
}
export default algos

42
src/algos/whats-alf.ts Normal file
View File

@ -0,0 +1,42 @@
import { InvalidRequestError } from '@atproto/xrpc-server'
import { QueryParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton'
import { AppContext } from '../config'
export const uri = 'at://did:example:alice/app.bsky.feed.generator/whats-alf'
export const handler = async (ctx: AppContext, params: QueryParams) => {
let builder = ctx.db
.selectFrom('post')
.selectAll()
.orderBy('indexedAt', 'desc')
.orderBy('cid', 'desc')
.limit(params.limit)
if (params.cursor) {
const [indexedAt, cid] = params.cursor.split('::')
if (!indexedAt || !cid) {
throw new InvalidRequestError('malformed cursor')
}
const timeStr = new Date(parseInt(indexedAt, 10)).toISOString()
builder = builder
.where('post.indexedAt', '<', timeStr)
.orWhere((qb) => qb.where('post.indexedAt', '=', timeStr))
.where('post.cid', '<', cid)
}
const res = await builder.execute()
const feed = res.map((row) => ({
post: row.uri,
}))
let cursor: string | undefined
const last = res.at(-1)
if (last) {
cursor = `${new Date(last.indexedAt).getTime()}::${last.cid}`
}
return {
cursor,
feed,
}
}

View File

@ -1,64 +0,0 @@
import { InvalidRequestError } from '@atproto/xrpc-server'
import { Server } from './lexicon'
import { AppContext } from './config'
import { validateAuth } from './auth'
export default function (server: Server, ctx: AppContext) {
server.app.bsky.feed.getFeedSkeleton(async ({ params, req }) => {
if (
params.feed !== 'at://did:example:alice/app.bsky.feed.generator/whats-alf'
) {
throw new InvalidRequestError(
'Unsupported algorithm',
'UnsupportedAlgorithm',
)
}
/**
* Example of how to check auth if giving user-specific results:
*
* const requesterDid = await validateAuth(
* req,
* ctx.cfg.serviceDid,
* ctx.didResolver,
* )
*/
let builder = ctx.db
.selectFrom('post')
.selectAll()
.orderBy('indexedAt', 'desc')
.orderBy('cid', 'desc')
.limit(params.limit)
if (params.cursor) {
const [indexedAt, cid] = params.cursor.split('::')
if (!indexedAt || !cid) {
throw new InvalidRequestError('malformed cursor')
}
const timeStr = new Date(parseInt(indexedAt, 10)).toISOString()
builder = builder
.where('post.indexedAt', '<', timeStr)
.orWhere((qb) => qb.where('post.indexedAt', '=', timeStr))
.where('post.cid', '<', cid)
}
const res = await builder.execute()
const feed = res.map((row) => ({
post: row.uri,
}))
let cursor: string | undefined
const last = res.at(-1)
if (last) {
cursor = `${new Date(last.indexedAt).getTime()}::${last.cid}`
}
return {
encoding: 'application/json',
body: {
cursor,
feed,
},
}
})
}

View File

@ -67,23 +67,27 @@ import * as ComAtprotoSyncListRepos from './types/com/atproto/sync/listRepos'
import * as ComAtprotoSyncNotifyOfUpdate from './types/com/atproto/sync/notifyOfUpdate'
import * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCrawl'
import * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos'
import * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences'
import * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile'
import * as AppBskyActorGetProfiles from './types/app/bsky/actor/getProfiles'
import * as AppBskyActorGetSuggestions from './types/app/bsky/actor/getSuggestions'
import * as AppBskyActorPutPreferences from './types/app/bsky/actor/putPreferences'
import * as AppBskyActorSearchActors from './types/app/bsky/actor/searchActors'
import * as AppBskyActorSearchActorsTypeahead from './types/app/bsky/actor/searchActorsTypeahead'
import * as AppBskyFeedBookmarkFeed from './types/app/bsky/feed/bookmarkFeed'
import * as AppBskyFeedDescribeFeedGenerator from './types/app/bsky/feed/describeFeedGenerator'
import * as AppBskyFeedGetActorFeeds from './types/app/bsky/feed/getActorFeeds'
import * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed'
import * as AppBskyFeedGetBookmarkedFeeds from './types/app/bsky/feed/getBookmarkedFeeds'
import * as AppBskyFeedGetFeed from './types/app/bsky/feed/getFeed'
import * as AppBskyFeedGetFeedGenerator from './types/app/bsky/feed/getFeedGenerator'
import * as AppBskyFeedGetFeedSkeleton from './types/app/bsky/feed/getFeedSkeleton'
import * as AppBskyFeedGetLikes from './types/app/bsky/feed/getLikes'
import * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread'
import * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts'
import * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy'
import * as AppBskyFeedGetSavedFeeds from './types/app/bsky/feed/getSavedFeeds'
import * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline'
import * as AppBskyFeedUnbookmarkFeed from './types/app/bsky/feed/unbookmarkFeed'
import * as AppBskyFeedSaveFeed from './types/app/bsky/feed/saveFeed'
import * as AppBskyFeedUnsaveFeed from './types/app/bsky/feed/unsaveFeed'
import * as AppBskyGraphGetBlocks from './types/app/bsky/graph/getBlocks'
import * as AppBskyGraphGetFollowers from './types/app/bsky/graph/getFollowers'
import * as AppBskyGraphGetFollows from './types/app/bsky/graph/getFollows'
@ -730,6 +734,13 @@ export class ActorNS {
this._server = server
}
getPreferences<AV extends AuthVerifier>(
cfg: ConfigOf<AV, AppBskyActorGetPreferences.Handler<ExtractAuth<AV>>>,
) {
const nsid = 'app.bsky.actor.getPreferences' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
getProfile<AV extends AuthVerifier>(
cfg: ConfigOf<AV, AppBskyActorGetProfile.Handler<ExtractAuth<AV>>>,
) {
@ -751,6 +762,13 @@ export class ActorNS {
return this._server.xrpc.method(nsid, cfg)
}
putPreferences<AV extends AuthVerifier>(
cfg: ConfigOf<AV, AppBskyActorPutPreferences.Handler<ExtractAuth<AV>>>,
) {
const nsid = 'app.bsky.actor.putPreferences' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
searchActors<AV extends AuthVerifier>(
cfg: ConfigOf<AV, AppBskyActorSearchActors.Handler<ExtractAuth<AV>>>,
) {
@ -784,10 +802,13 @@ export class FeedNS {
this._server = server
}
bookmarkFeed<AV extends AuthVerifier>(
cfg: ConfigOf<AV, AppBskyFeedBookmarkFeed.Handler<ExtractAuth<AV>>>,
describeFeedGenerator<AV extends AuthVerifier>(
cfg: ConfigOf<
AV,
AppBskyFeedDescribeFeedGenerator.Handler<ExtractAuth<AV>>
>,
) {
const nsid = 'app.bsky.feed.bookmarkFeed' // @ts-ignore
const nsid = 'app.bsky.feed.describeFeedGenerator' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
@ -805,13 +826,6 @@ export class FeedNS {
return this._server.xrpc.method(nsid, cfg)
}
getBookmarkedFeeds<AV extends AuthVerifier>(
cfg: ConfigOf<AV, AppBskyFeedGetBookmarkedFeeds.Handler<ExtractAuth<AV>>>,
) {
const nsid = 'app.bsky.feed.getBookmarkedFeeds' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
getFeed<AV extends AuthVerifier>(
cfg: ConfigOf<AV, AppBskyFeedGetFeed.Handler<ExtractAuth<AV>>>,
) {
@ -819,6 +833,13 @@ export class FeedNS {
return this._server.xrpc.method(nsid, cfg)
}
getFeedGenerator<AV extends AuthVerifier>(
cfg: ConfigOf<AV, AppBskyFeedGetFeedGenerator.Handler<ExtractAuth<AV>>>,
) {
const nsid = 'app.bsky.feed.getFeedGenerator' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
getFeedSkeleton<AV extends AuthVerifier>(
cfg: ConfigOf<AV, AppBskyFeedGetFeedSkeleton.Handler<ExtractAuth<AV>>>,
) {
@ -854,6 +875,13 @@ export class FeedNS {
return this._server.xrpc.method(nsid, cfg)
}
getSavedFeeds<AV extends AuthVerifier>(
cfg: ConfigOf<AV, AppBskyFeedGetSavedFeeds.Handler<ExtractAuth<AV>>>,
) {
const nsid = 'app.bsky.feed.getSavedFeeds' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
getTimeline<AV extends AuthVerifier>(
cfg: ConfigOf<AV, AppBskyFeedGetTimeline.Handler<ExtractAuth<AV>>>,
) {
@ -861,10 +889,17 @@ export class FeedNS {
return this._server.xrpc.method(nsid, cfg)
}
unbookmarkFeed<AV extends AuthVerifier>(
cfg: ConfigOf<AV, AppBskyFeedUnbookmarkFeed.Handler<ExtractAuth<AV>>>,
saveFeed<AV extends AuthVerifier>(
cfg: ConfigOf<AV, AppBskyFeedSaveFeed.Handler<ExtractAuth<AV>>>,
) {
const nsid = 'app.bsky.feed.unbookmarkFeed' // @ts-ignore
const nsid = 'app.bsky.feed.saveFeed' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
unsaveFeed<AV extends AuthVerifier>(
cfg: ConfigOf<AV, AppBskyFeedUnsaveFeed.Handler<ExtractAuth<AV>>>,
) {
const nsid = 'app.bsky.feed.unsaveFeed' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
}

View File

@ -835,6 +835,15 @@ export const schemaDict = {
resolved: {
type: 'boolean',
},
actionType: {
type: 'string',
knownValues: [
'com.atproto.admin.defs#takedown',
'com.atproto.admin.defs#flag',
'com.atproto.admin.defs#acknowledge',
'com.atproto.admin.defs#escalate',
],
},
limit: {
type: 'integer',
minimum: 1,
@ -2114,6 +2123,10 @@ export const schemaDict = {
type: 'string',
format: 'handle',
},
did: {
type: 'string',
format: 'did',
},
inviteCode: {
type: 'string',
},
@ -2165,6 +2178,12 @@ export const schemaDict = {
{
name: 'UnsupportedDomain',
},
{
name: 'UnresolvableDid',
},
{
name: 'IncompatibleDidDoc',
},
],
},
},
@ -3509,6 +3528,66 @@ export const schemaDict = {
},
},
},
preferences: {
type: 'array',
items: {
type: 'union',
refs: [
'lex:app.bsky.actor.defs#adultContentPref',
'lex:app.bsky.actor.defs#contentLabelPref',
],
},
},
adultContentPref: {
type: 'object',
required: ['enabled'],
properties: {
enabled: {
type: 'boolean',
default: false,
},
},
},
contentLabelPref: {
type: 'object',
required: ['label', 'visibility'],
properties: {
label: {
type: 'string',
},
visibility: {
type: 'string',
knownValues: ['show', 'warn', 'hide'],
},
},
},
},
},
AppBskyActorGetPreferences: {
lexicon: 1,
id: 'app.bsky.actor.getPreferences',
defs: {
main: {
type: 'query',
description: 'Get private preferences attached to the account.',
parameters: {
type: 'params',
properties: {},
},
output: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['preferences'],
properties: {
preferences: {
type: 'ref',
ref: 'lex:app.bsky.actor.defs#preferences',
},
},
},
},
},
},
},
AppBskyActorGetProfile: {
@ -3655,6 +3734,29 @@ export const schemaDict = {
},
},
},
AppBskyActorPutPreferences: {
lexicon: 1,
id: 'app.bsky.actor.putPreferences',
defs: {
main: {
type: 'procedure',
description: 'Sets the private preferences attached to the account.',
input: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['preferences'],
properties: {
preferences: {
type: 'ref',
ref: 'lex:app.bsky.actor.defs#preferences',
},
},
},
},
},
},
},
AppBskyActorSearchActors: {
lexicon: 1,
id: 'app.bsky.actor.searchActors',
@ -3899,6 +4001,7 @@ export const schemaDict = {
'lex:app.bsky.embed.record#viewRecord',
'lex:app.bsky.embed.record#viewNotFound',
'lex:app.bsky.embed.record#viewBlocked',
'lex:app.bsky.feed.defs#generatorView',
],
},
},
@ -4008,29 +4111,6 @@ export const schemaDict = {
},
},
},
AppBskyFeedBookmarkFeed: {
lexicon: 1,
id: 'app.bsky.feed.bookmarkFeed',
defs: {
main: {
type: 'procedure',
description: 'Bookmark a 3rd party feed for use across clients',
input: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['feed'],
properties: {
feed: {
type: 'string',
format: 'at-uri',
},
},
},
},
},
},
},
AppBskyFeedDefs: {
lexicon: 1,
id: 'app.bsky.feed.defs',
@ -4215,12 +4295,16 @@ export const schemaDict = {
},
generatorView: {
type: 'object',
required: ['uri', 'creator', 'indexedAt'],
required: ['uri', 'cid', 'creator', 'indexedAt'],
properties: {
uri: {
type: 'string',
format: 'at-uri',
},
cid: {
type: 'string',
format: 'cid',
},
did: {
type: 'string',
format: 'did',
@ -4247,6 +4331,10 @@ export const schemaDict = {
avatar: {
type: 'string',
},
likeCount: {
type: 'integer',
minimum: 0,
},
viewer: {
type: 'ref',
ref: 'lex:app.bsky.feed.defs#generatorViewerState',
@ -4260,7 +4348,7 @@ export const schemaDict = {
generatorViewerState: {
type: 'object',
properties: {
subscribed: {
saved: {
type: 'boolean',
},
like: {
@ -4295,6 +4383,62 @@ export const schemaDict = {
},
},
},
AppBskyFeedDescribeFeedGenerator: {
lexicon: 1,
id: 'app.bsky.feed.describeFeedGenerator',
defs: {
main: {
type: 'query',
description:
'Returns information about a given feed generator including TOS & offered feed URIs',
output: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['did', 'feeds'],
properties: {
did: {
type: 'string',
format: 'did',
},
feeds: {
type: 'array',
items: {
type: 'ref',
ref: 'lex:app.bsky.feed.describeFeedGenerator#feed',
},
},
links: {
type: 'ref',
ref: 'lex:app.bsky.feed.describeFeedGenerator#links',
},
},
},
},
},
feed: {
type: 'object',
required: ['uri'],
properties: {
uri: {
type: 'string',
format: 'at-uri',
},
},
},
links: {
type: 'object',
properties: {
privacyPolicy: {
type: 'string',
},
termsOfService: {
type: 'string',
},
},
},
},
},
AppBskyFeedGenerator: {
lexicon: 1,
id: 'app.bsky.feed.generator',
@ -4446,50 +4590,6 @@ export const schemaDict = {
},
},
},
AppBskyFeedGetBookmarkedFeeds: {
lexicon: 1,
id: 'app.bsky.feed.getBookmarkedFeeds',
defs: {
main: {
type: 'query',
description:
"Retrieve a list of the authenticated user's bookmarked feeds",
parameters: {
type: 'params',
properties: {
limit: {
type: 'integer',
minimum: 1,
maximum: 100,
default: 50,
},
cursor: {
type: 'string',
},
},
},
output: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['feeds'],
properties: {
cursor: {
type: 'string',
},
feeds: {
type: 'array',
items: {
type: 'ref',
ref: 'lex:app.bsky.feed.defs#generatorView',
},
},
},
},
},
},
},
},
AppBskyFeedGetFeed: {
lexicon: 1,
id: 'app.bsky.feed.getFeed',
@ -4536,6 +4636,51 @@ export const schemaDict = {
},
},
},
errors: [
{
name: 'UnknownFeed',
},
],
},
},
},
AppBskyFeedGetFeedGenerator: {
lexicon: 1,
id: 'app.bsky.feed.getFeedGenerator',
defs: {
main: {
type: 'query',
description:
'Get information about a specific feed offered by a feed generator, such as its online status',
parameters: {
type: 'params',
required: ['feed'],
properties: {
feed: {
type: 'string',
format: 'at-uri',
},
},
},
output: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['view', 'isOnline', 'isValid'],
properties: {
view: {
type: 'ref',
ref: 'lex:app.bsky.feed.defs#generatorView',
},
isOnline: {
type: 'boolean',
},
isValid: {
type: 'boolean',
},
},
},
},
},
},
},
@ -4584,6 +4729,11 @@ export const schemaDict = {
},
},
},
errors: [
{
name: 'UnknownFeed',
},
],
},
},
},
@ -4807,6 +4957,49 @@ export const schemaDict = {
},
},
},
AppBskyFeedGetSavedFeeds: {
lexicon: 1,
id: 'app.bsky.feed.getSavedFeeds',
defs: {
main: {
type: 'query',
description: "Retrieve a list of the authenticated user's saved feeds",
parameters: {
type: 'params',
properties: {
limit: {
type: 'integer',
minimum: 1,
maximum: 100,
default: 50,
},
cursor: {
type: 'string',
},
},
},
output: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['feeds'],
properties: {
cursor: {
type: 'string',
},
feeds: {
type: 'array',
items: {
type: 'ref',
ref: 'lex:app.bsky.feed.defs#generatorView',
},
},
},
},
},
},
},
},
AppBskyFeedGetTimeline: {
lexicon: 1,
id: 'app.bsky.feed.getTimeline',
@ -5002,13 +5195,36 @@ export const schemaDict = {
},
},
},
AppBskyFeedUnbookmarkFeed: {
AppBskyFeedSaveFeed: {
lexicon: 1,
id: 'app.bsky.feed.unbookmarkFeed',
id: 'app.bsky.feed.saveFeed',
defs: {
main: {
type: 'procedure',
description: 'Remove a bookmark for a 3rd party feed',
description: 'Save a 3rd party feed for use across clients',
input: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['feed'],
properties: {
feed: {
type: 'string',
format: 'at-uri',
},
},
},
},
},
},
},
AppBskyFeedUnsaveFeed: {
lexicon: 1,
id: 'app.bsky.feed.unsaveFeed',
defs: {
main: {
type: 'procedure',
description: 'Unsave a 3rd party feed',
input: {
encoding: 'application/json',
schema: {
@ -6029,33 +6245,37 @@ export const ids = {
ComAtprotoSyncRequestCrawl: 'com.atproto.sync.requestCrawl',
ComAtprotoSyncSubscribeRepos: 'com.atproto.sync.subscribeRepos',
AppBskyActorDefs: 'app.bsky.actor.defs',
AppBskyActorGetPreferences: 'app.bsky.actor.getPreferences',
AppBskyActorGetProfile: 'app.bsky.actor.getProfile',
AppBskyActorGetProfiles: 'app.bsky.actor.getProfiles',
AppBskyActorGetSuggestions: 'app.bsky.actor.getSuggestions',
AppBskyActorProfile: 'app.bsky.actor.profile',
AppBskyActorPutPreferences: 'app.bsky.actor.putPreferences',
AppBskyActorSearchActors: 'app.bsky.actor.searchActors',
AppBskyActorSearchActorsTypeahead: 'app.bsky.actor.searchActorsTypeahead',
AppBskyEmbedExternal: 'app.bsky.embed.external',
AppBskyEmbedImages: 'app.bsky.embed.images',
AppBskyEmbedRecord: 'app.bsky.embed.record',
AppBskyEmbedRecordWithMedia: 'app.bsky.embed.recordWithMedia',
AppBskyFeedBookmarkFeed: 'app.bsky.feed.bookmarkFeed',
AppBskyFeedDefs: 'app.bsky.feed.defs',
AppBskyFeedDescribeFeedGenerator: 'app.bsky.feed.describeFeedGenerator',
AppBskyFeedGenerator: 'app.bsky.feed.generator',
AppBskyFeedGetActorFeeds: 'app.bsky.feed.getActorFeeds',
AppBskyFeedGetAuthorFeed: 'app.bsky.feed.getAuthorFeed',
AppBskyFeedGetBookmarkedFeeds: 'app.bsky.feed.getBookmarkedFeeds',
AppBskyFeedGetFeed: 'app.bsky.feed.getFeed',
AppBskyFeedGetFeedGenerator: 'app.bsky.feed.getFeedGenerator',
AppBskyFeedGetFeedSkeleton: 'app.bsky.feed.getFeedSkeleton',
AppBskyFeedGetLikes: 'app.bsky.feed.getLikes',
AppBskyFeedGetPostThread: 'app.bsky.feed.getPostThread',
AppBskyFeedGetPosts: 'app.bsky.feed.getPosts',
AppBskyFeedGetRepostedBy: 'app.bsky.feed.getRepostedBy',
AppBskyFeedGetSavedFeeds: 'app.bsky.feed.getSavedFeeds',
AppBskyFeedGetTimeline: 'app.bsky.feed.getTimeline',
AppBskyFeedLike: 'app.bsky.feed.like',
AppBskyFeedPost: 'app.bsky.feed.post',
AppBskyFeedRepost: 'app.bsky.feed.repost',
AppBskyFeedUnbookmarkFeed: 'app.bsky.feed.unbookmarkFeed',
AppBskyFeedSaveFeed: 'app.bsky.feed.saveFeed',
AppBskyFeedUnsaveFeed: 'app.bsky.feed.unsaveFeed',
AppBskyGraphBlock: 'app.bsky.graph.block',
AppBskyGraphDefs: 'app.bsky.graph.defs',
AppBskyGraphFollow: 'app.bsky.graph.follow',

View File

@ -103,3 +103,44 @@ export function isViewerState(v: unknown): v is ViewerState {
export function validateViewerState(v: unknown): ValidationResult {
return lexicons.validate('app.bsky.actor.defs#viewerState', v)
}
export type Preferences = (
| AdultContentPref
| ContentLabelPref
| { $type: string; [k: string]: unknown }
)[]
export interface AdultContentPref {
enabled: boolean
[k: string]: unknown
}
export function isAdultContentPref(v: unknown): v is AdultContentPref {
return (
isObj(v) &&
hasProp(v, '$type') &&
v.$type === 'app.bsky.actor.defs#adultContentPref'
)
}
export function validateAdultContentPref(v: unknown): ValidationResult {
return lexicons.validate('app.bsky.actor.defs#adultContentPref', v)
}
export interface ContentLabelPref {
label: string
visibility: 'show' | 'warn' | 'hide' | (string & {})
[k: string]: unknown
}
export function isContentLabelPref(v: unknown): v is ContentLabelPref {
return (
isObj(v) &&
hasProp(v, '$type') &&
v.$type === 'app.bsky.actor.defs#contentLabelPref'
)
}
export function validateContentLabelPref(v: unknown): ValidationResult {
return lexicons.validate('app.bsky.actor.defs#contentLabelPref', v)
}

View File

@ -0,0 +1,40 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import express from 'express'
import { ValidationResult, BlobRef } from '@atproto/lexicon'
import { lexicons } from '../../../../lexicons'
import { isObj, hasProp } from '../../../../util'
import { CID } from 'multiformats/cid'
import { HandlerAuth } from '@atproto/xrpc-server'
import * as AppBskyActorDefs from './defs'
export interface QueryParams {}
export type InputSchema = undefined
export interface OutputSchema {
preferences: AppBskyActorDefs.Preferences
[k: string]: unknown
}
export type HandlerInput = undefined
export interface HandlerSuccess {
encoding: 'application/json'
body: OutputSchema
}
export interface HandlerError {
status: number
message?: string
}
export type HandlerOutput = HandlerError | HandlerSuccess
export type Handler<HA extends HandlerAuth = never> = (ctx: {
auth: HA
params: QueryParams
input: HandlerInput
req: express.Request
res: express.Response
}) => Promise<HandlerOutput> | HandlerOutput

View File

@ -0,0 +1,36 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import express from 'express'
import { ValidationResult, BlobRef } from '@atproto/lexicon'
import { lexicons } from '../../../../lexicons'
import { isObj, hasProp } from '../../../../util'
import { CID } from 'multiformats/cid'
import { HandlerAuth } from '@atproto/xrpc-server'
import * as AppBskyActorDefs from './defs'
export interface QueryParams {}
export interface InputSchema {
preferences: AppBskyActorDefs.Preferences
[k: string]: unknown
}
export interface HandlerInput {
encoding: 'application/json'
body: InputSchema
}
export interface HandlerError {
status: number
message?: string
}
export type HandlerOutput = HandlerError | void
export type Handler<HA extends HandlerAuth = never> = (ctx: {
auth: HA
params: QueryParams
input: HandlerInput
req: express.Request
res: express.Response
}) => Promise<HandlerOutput> | HandlerOutput

View File

@ -6,6 +6,7 @@ import { lexicons } from '../../../../lexicons'
import { isObj, hasProp } from '../../../../util'
import { CID } from 'multiformats/cid'
import * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef'
import * as AppBskyFeedDefs from '../feed/defs'
import * as AppBskyActorDefs from '../actor/defs'
import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs'
import * as AppBskyEmbedImages from './images'
@ -35,6 +36,7 @@ export interface View {
| ViewRecord
| ViewNotFound
| ViewBlocked
| AppBskyFeedDefs.GeneratorView
| { $type: string; [k: string]: unknown }
[k: string]: unknown
}

View File

@ -188,12 +188,14 @@ export function validateBlockedPost(v: unknown): ValidationResult {
export interface GeneratorView {
uri: string
cid: string
did?: string
creator: AppBskyActorDefs.ProfileView
displayName?: string
description?: string
descriptionFacets?: AppBskyRichtextFacet.Main[]
avatar?: string
likeCount?: number
viewer?: GeneratorViewerState
indexedAt: string
[k: string]: unknown
@ -212,7 +214,7 @@ export function validateGeneratorView(v: unknown): ValidationResult {
}
export interface GeneratorViewerState {
subscribed?: boolean
saved?: boolean
like?: string
[k: string]: unknown
}

View File

@ -0,0 +1,76 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import express from 'express'
import { ValidationResult, BlobRef } from '@atproto/lexicon'
import { lexicons } from '../../../../lexicons'
import { isObj, hasProp } from '../../../../util'
import { CID } from 'multiformats/cid'
import { HandlerAuth } from '@atproto/xrpc-server'
export interface QueryParams {}
export type InputSchema = undefined
export interface OutputSchema {
did: string
feeds: Feed[]
links?: Links
[k: string]: unknown
}
export type HandlerInput = undefined
export interface HandlerSuccess {
encoding: 'application/json'
body: OutputSchema
}
export interface HandlerError {
status: number
message?: string
}
export type HandlerOutput = HandlerError | HandlerSuccess
export type Handler<HA extends HandlerAuth = never> = (ctx: {
auth: HA
params: QueryParams
input: HandlerInput
req: express.Request
res: express.Response
}) => Promise<HandlerOutput> | HandlerOutput
export interface Feed {
uri: string
[k: string]: unknown
}
export function isFeed(v: unknown): v is Feed {
return (
isObj(v) &&
hasProp(v, '$type') &&
v.$type === 'app.bsky.feed.describeFeedGenerator#feed'
)
}
export function validateFeed(v: unknown): ValidationResult {
return lexicons.validate('app.bsky.feed.describeFeedGenerator#feed', v)
}
export interface Links {
privacyPolicy?: string
termsOfService?: string
[k: string]: unknown
}
export function isLinks(v: unknown): v is Links {
return (
isObj(v) &&
hasProp(v, '$type') &&
v.$type === 'app.bsky.feed.describeFeedGenerator#links'
)
}
export function validateLinks(v: unknown): ValidationResult {
return lexicons.validate('app.bsky.feed.describeFeedGenerator#links', v)
}

View File

@ -33,6 +33,7 @@ export interface HandlerSuccess {
export interface HandlerError {
status: number
message?: string
error?: 'UnknownFeed'
}
export type HandlerOutput = HandlerError | HandlerSuccess

View File

@ -0,0 +1,44 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import express from 'express'
import { ValidationResult, BlobRef } from '@atproto/lexicon'
import { lexicons } from '../../../../lexicons'
import { isObj, hasProp } from '../../../../util'
import { CID } from 'multiformats/cid'
import { HandlerAuth } from '@atproto/xrpc-server'
import * as AppBskyFeedDefs from './defs'
export interface QueryParams {
feed: string
}
export type InputSchema = undefined
export interface OutputSchema {
view: AppBskyFeedDefs.GeneratorView
isOnline: boolean
isValid: boolean
[k: string]: unknown
}
export type HandlerInput = undefined
export interface HandlerSuccess {
encoding: 'application/json'
body: OutputSchema
}
export interface HandlerError {
status: number
message?: string
}
export type HandlerOutput = HandlerError | HandlerSuccess
export type Handler<HA extends HandlerAuth = never> = (ctx: {
auth: HA
params: QueryParams
input: HandlerInput
req: express.Request
res: express.Response
}) => Promise<HandlerOutput> | HandlerOutput

View File

@ -33,6 +33,7 @@ export interface HandlerSuccess {
export interface HandlerError {
status: number
message?: string
error?: 'UnknownFeed'
}
export type HandlerOutput = HandlerError | HandlerSuccess

View File

@ -12,6 +12,12 @@ import * as ComAtprotoAdminDefs from './defs'
export interface QueryParams {
subject?: string
resolved?: boolean
actionType?:
| 'com.atproto.admin.defs#takedown'
| 'com.atproto.admin.defs#flag'
| 'com.atproto.admin.defs#acknowledge'
| 'com.atproto.admin.defs#escalate'
| (string & {})
limit: number
cursor?: string
}

View File

@ -13,6 +13,7 @@ export interface QueryParams {}
export interface InputSchema {
email: string
handle: string
did?: string
inviteCode?: string
password: string
recoveryKey?: string
@ -46,6 +47,8 @@ export interface HandlerError {
| 'InvalidInviteCode'
| 'HandleNotAvailable'
| 'UnsupportedDomain'
| 'UnresolvableDid'
| 'IncompatibleDidDoc'
}
export type HandlerOutput = HandlerError | HandlerSuccess

View File

@ -0,0 +1,16 @@
import { Server } from '../lexicon'
import { AppContext } from '../config'
import algos from '../algos'
export default function (server: Server, ctx: AppContext) {
server.app.bsky.feed.describeFeedGenerator(async () => {
const feeds = Object.keys(algos).map((uri) => ({ uri }))
return {
encoding: 'application/json',
body: {
did: ctx.cfg.serviceDid,
feeds,
},
}
})
}

View File

@ -0,0 +1,32 @@
import { InvalidRequestError } from '@atproto/xrpc-server'
import { Server } from '../lexicon'
import { AppContext } from '../config'
import algos from '../algos'
import { validateAuth } from '../auth'
export default function (server: Server, ctx: AppContext) {
server.app.bsky.feed.getFeedSkeleton(async ({ params, req }) => {
const algo = algos[params.feed]
if (!algo) {
throw new InvalidRequestError(
'Unsupported algorithm',
'UnsupportedAlgorithm',
)
}
/**
* Example of how to check auth if giving user-specific results:
*
* const requesterDid = await validateAuth(
* req,
* ctx.cfg.serviceDid,
* ctx.didResolver,
* )
*/
const body = await algo(ctx, params)
return {
encoding: 'application/json',
body: body,
}
})
}

View File

@ -3,7 +3,8 @@ import events from 'events'
import express from 'express'
import { DidResolver, MemoryCache } from '@atproto/did-resolver'
import { createServer } from './lexicon'
import feedGeneration from './feed-generation'
import feedGeneration from './methods/feed-generation'
import describeGenerator from './methods/describe-generator'
import { createDb, Database, migrateToLatest } from './db'
import { FirehoseSubscription } from './subscription'
import { AppContext, Config } from './config'
@ -60,6 +61,7 @@ export class FeedGenerator {
cfg,
}
feedGeneration(server, ctx)
describeGenerator(server, ctx)
app.use(server.xrpc.router)
app.use(wellKnown(cfg.hostname))