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. 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` 2. Implement feed generation logic in `src/algos`
The types are in place and you will just need to return something that satisfies the `SkeletonFeedPost[]` type.
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. 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 ComAtprotoSyncNotifyOfUpdate from './types/com/atproto/sync/notifyOfUpdate'
import * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCrawl' import * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCrawl'
import * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos' 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 AppBskyActorGetProfile from './types/app/bsky/actor/getProfile'
import * as AppBskyActorGetProfiles from './types/app/bsky/actor/getProfiles' import * as AppBskyActorGetProfiles from './types/app/bsky/actor/getProfiles'
import * as AppBskyActorGetSuggestions from './types/app/bsky/actor/getSuggestions' 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 AppBskyActorSearchActors from './types/app/bsky/actor/searchActors'
import * as AppBskyActorSearchActorsTypeahead from './types/app/bsky/actor/searchActorsTypeahead' 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 AppBskyFeedGetActorFeeds from './types/app/bsky/feed/getActorFeeds'
import * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed' 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 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 AppBskyFeedGetFeedSkeleton from './types/app/bsky/feed/getFeedSkeleton'
import * as AppBskyFeedGetLikes from './types/app/bsky/feed/getLikes' import * as AppBskyFeedGetLikes from './types/app/bsky/feed/getLikes'
import * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread' import * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread'
import * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts' import * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts'
import * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy' 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 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 AppBskyGraphGetBlocks from './types/app/bsky/graph/getBlocks'
import * as AppBskyGraphGetFollowers from './types/app/bsky/graph/getFollowers' import * as AppBskyGraphGetFollowers from './types/app/bsky/graph/getFollowers'
import * as AppBskyGraphGetFollows from './types/app/bsky/graph/getFollows' import * as AppBskyGraphGetFollows from './types/app/bsky/graph/getFollows'
@ -730,6 +734,13 @@ export class ActorNS {
this._server = server 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>( getProfile<AV extends AuthVerifier>(
cfg: ConfigOf<AV, AppBskyActorGetProfile.Handler<ExtractAuth<AV>>>, cfg: ConfigOf<AV, AppBskyActorGetProfile.Handler<ExtractAuth<AV>>>,
) { ) {
@ -751,6 +762,13 @@ export class ActorNS {
return this._server.xrpc.method(nsid, cfg) 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>( searchActors<AV extends AuthVerifier>(
cfg: ConfigOf<AV, AppBskyActorSearchActors.Handler<ExtractAuth<AV>>>, cfg: ConfigOf<AV, AppBskyActorSearchActors.Handler<ExtractAuth<AV>>>,
) { ) {
@ -784,10 +802,13 @@ export class FeedNS {
this._server = server this._server = server
} }
bookmarkFeed<AV extends AuthVerifier>( describeFeedGenerator<AV extends AuthVerifier>(
cfg: ConfigOf<AV, AppBskyFeedBookmarkFeed.Handler<ExtractAuth<AV>>>, 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) return this._server.xrpc.method(nsid, cfg)
} }
@ -805,13 +826,6 @@ export class FeedNS {
return this._server.xrpc.method(nsid, cfg) 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>( getFeed<AV extends AuthVerifier>(
cfg: ConfigOf<AV, AppBskyFeedGetFeed.Handler<ExtractAuth<AV>>>, cfg: ConfigOf<AV, AppBskyFeedGetFeed.Handler<ExtractAuth<AV>>>,
) { ) {
@ -819,6 +833,13 @@ export class FeedNS {
return this._server.xrpc.method(nsid, cfg) 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>( getFeedSkeleton<AV extends AuthVerifier>(
cfg: ConfigOf<AV, AppBskyFeedGetFeedSkeleton.Handler<ExtractAuth<AV>>>, cfg: ConfigOf<AV, AppBskyFeedGetFeedSkeleton.Handler<ExtractAuth<AV>>>,
) { ) {
@ -854,6 +875,13 @@ export class FeedNS {
return this._server.xrpc.method(nsid, cfg) 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>( getTimeline<AV extends AuthVerifier>(
cfg: ConfigOf<AV, AppBskyFeedGetTimeline.Handler<ExtractAuth<AV>>>, cfg: ConfigOf<AV, AppBskyFeedGetTimeline.Handler<ExtractAuth<AV>>>,
) { ) {
@ -861,10 +889,17 @@ export class FeedNS {
return this._server.xrpc.method(nsid, cfg) return this._server.xrpc.method(nsid, cfg)
} }
unbookmarkFeed<AV extends AuthVerifier>( saveFeed<AV extends AuthVerifier>(
cfg: ConfigOf<AV, AppBskyFeedUnbookmarkFeed.Handler<ExtractAuth<AV>>>, 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) return this._server.xrpc.method(nsid, cfg)
} }
} }

View File

@ -835,6 +835,15 @@ export const schemaDict = {
resolved: { resolved: {
type: 'boolean', 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: { limit: {
type: 'integer', type: 'integer',
minimum: 1, minimum: 1,
@ -2114,6 +2123,10 @@ export const schemaDict = {
type: 'string', type: 'string',
format: 'handle', format: 'handle',
}, },
did: {
type: 'string',
format: 'did',
},
inviteCode: { inviteCode: {
type: 'string', type: 'string',
}, },
@ -2165,6 +2178,12 @@ export const schemaDict = {
{ {
name: 'UnsupportedDomain', 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: { 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: { AppBskyActorSearchActors: {
lexicon: 1, lexicon: 1,
id: 'app.bsky.actor.searchActors', id: 'app.bsky.actor.searchActors',
@ -3899,6 +4001,7 @@ export const schemaDict = {
'lex:app.bsky.embed.record#viewRecord', 'lex:app.bsky.embed.record#viewRecord',
'lex:app.bsky.embed.record#viewNotFound', 'lex:app.bsky.embed.record#viewNotFound',
'lex:app.bsky.embed.record#viewBlocked', '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: { AppBskyFeedDefs: {
lexicon: 1, lexicon: 1,
id: 'app.bsky.feed.defs', id: 'app.bsky.feed.defs',
@ -4215,12 +4295,16 @@ export const schemaDict = {
}, },
generatorView: { generatorView: {
type: 'object', type: 'object',
required: ['uri', 'creator', 'indexedAt'], required: ['uri', 'cid', 'creator', 'indexedAt'],
properties: { properties: {
uri: { uri: {
type: 'string', type: 'string',
format: 'at-uri', format: 'at-uri',
}, },
cid: {
type: 'string',
format: 'cid',
},
did: { did: {
type: 'string', type: 'string',
format: 'did', format: 'did',
@ -4247,6 +4331,10 @@ export const schemaDict = {
avatar: { avatar: {
type: 'string', type: 'string',
}, },
likeCount: {
type: 'integer',
minimum: 0,
},
viewer: { viewer: {
type: 'ref', type: 'ref',
ref: 'lex:app.bsky.feed.defs#generatorViewerState', ref: 'lex:app.bsky.feed.defs#generatorViewerState',
@ -4260,7 +4348,7 @@ export const schemaDict = {
generatorViewerState: { generatorViewerState: {
type: 'object', type: 'object',
properties: { properties: {
subscribed: { saved: {
type: 'boolean', type: 'boolean',
}, },
like: { 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: { AppBskyFeedGenerator: {
lexicon: 1, lexicon: 1,
id: 'app.bsky.feed.generator', 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: { AppBskyFeedGetFeed: {
lexicon: 1, lexicon: 1,
id: 'app.bsky.feed.getFeed', 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: { AppBskyFeedGetTimeline: {
lexicon: 1, lexicon: 1,
id: 'app.bsky.feed.getTimeline', id: 'app.bsky.feed.getTimeline',
@ -5002,13 +5195,36 @@ export const schemaDict = {
}, },
}, },
}, },
AppBskyFeedUnbookmarkFeed: { AppBskyFeedSaveFeed: {
lexicon: 1, lexicon: 1,
id: 'app.bsky.feed.unbookmarkFeed', id: 'app.bsky.feed.saveFeed',
defs: { defs: {
main: { main: {
type: 'procedure', 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: { input: {
encoding: 'application/json', encoding: 'application/json',
schema: { schema: {
@ -6029,33 +6245,37 @@ export const ids = {
ComAtprotoSyncRequestCrawl: 'com.atproto.sync.requestCrawl', ComAtprotoSyncRequestCrawl: 'com.atproto.sync.requestCrawl',
ComAtprotoSyncSubscribeRepos: 'com.atproto.sync.subscribeRepos', ComAtprotoSyncSubscribeRepos: 'com.atproto.sync.subscribeRepos',
AppBskyActorDefs: 'app.bsky.actor.defs', AppBskyActorDefs: 'app.bsky.actor.defs',
AppBskyActorGetPreferences: 'app.bsky.actor.getPreferences',
AppBskyActorGetProfile: 'app.bsky.actor.getProfile', AppBskyActorGetProfile: 'app.bsky.actor.getProfile',
AppBskyActorGetProfiles: 'app.bsky.actor.getProfiles', AppBskyActorGetProfiles: 'app.bsky.actor.getProfiles',
AppBskyActorGetSuggestions: 'app.bsky.actor.getSuggestions', AppBskyActorGetSuggestions: 'app.bsky.actor.getSuggestions',
AppBskyActorProfile: 'app.bsky.actor.profile', AppBskyActorProfile: 'app.bsky.actor.profile',
AppBskyActorPutPreferences: 'app.bsky.actor.putPreferences',
AppBskyActorSearchActors: 'app.bsky.actor.searchActors', AppBskyActorSearchActors: 'app.bsky.actor.searchActors',
AppBskyActorSearchActorsTypeahead: 'app.bsky.actor.searchActorsTypeahead', AppBskyActorSearchActorsTypeahead: 'app.bsky.actor.searchActorsTypeahead',
AppBskyEmbedExternal: 'app.bsky.embed.external', AppBskyEmbedExternal: 'app.bsky.embed.external',
AppBskyEmbedImages: 'app.bsky.embed.images', AppBskyEmbedImages: 'app.bsky.embed.images',
AppBskyEmbedRecord: 'app.bsky.embed.record', AppBskyEmbedRecord: 'app.bsky.embed.record',
AppBskyEmbedRecordWithMedia: 'app.bsky.embed.recordWithMedia', AppBskyEmbedRecordWithMedia: 'app.bsky.embed.recordWithMedia',
AppBskyFeedBookmarkFeed: 'app.bsky.feed.bookmarkFeed',
AppBskyFeedDefs: 'app.bsky.feed.defs', AppBskyFeedDefs: 'app.bsky.feed.defs',
AppBskyFeedDescribeFeedGenerator: 'app.bsky.feed.describeFeedGenerator',
AppBskyFeedGenerator: 'app.bsky.feed.generator', AppBskyFeedGenerator: 'app.bsky.feed.generator',
AppBskyFeedGetActorFeeds: 'app.bsky.feed.getActorFeeds', AppBskyFeedGetActorFeeds: 'app.bsky.feed.getActorFeeds',
AppBskyFeedGetAuthorFeed: 'app.bsky.feed.getAuthorFeed', AppBskyFeedGetAuthorFeed: 'app.bsky.feed.getAuthorFeed',
AppBskyFeedGetBookmarkedFeeds: 'app.bsky.feed.getBookmarkedFeeds',
AppBskyFeedGetFeed: 'app.bsky.feed.getFeed', AppBskyFeedGetFeed: 'app.bsky.feed.getFeed',
AppBskyFeedGetFeedGenerator: 'app.bsky.feed.getFeedGenerator',
AppBskyFeedGetFeedSkeleton: 'app.bsky.feed.getFeedSkeleton', AppBskyFeedGetFeedSkeleton: 'app.bsky.feed.getFeedSkeleton',
AppBskyFeedGetLikes: 'app.bsky.feed.getLikes', AppBskyFeedGetLikes: 'app.bsky.feed.getLikes',
AppBskyFeedGetPostThread: 'app.bsky.feed.getPostThread', AppBskyFeedGetPostThread: 'app.bsky.feed.getPostThread',
AppBskyFeedGetPosts: 'app.bsky.feed.getPosts', AppBskyFeedGetPosts: 'app.bsky.feed.getPosts',
AppBskyFeedGetRepostedBy: 'app.bsky.feed.getRepostedBy', AppBskyFeedGetRepostedBy: 'app.bsky.feed.getRepostedBy',
AppBskyFeedGetSavedFeeds: 'app.bsky.feed.getSavedFeeds',
AppBskyFeedGetTimeline: 'app.bsky.feed.getTimeline', AppBskyFeedGetTimeline: 'app.bsky.feed.getTimeline',
AppBskyFeedLike: 'app.bsky.feed.like', AppBskyFeedLike: 'app.bsky.feed.like',
AppBskyFeedPost: 'app.bsky.feed.post', AppBskyFeedPost: 'app.bsky.feed.post',
AppBskyFeedRepost: 'app.bsky.feed.repost', 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', AppBskyGraphBlock: 'app.bsky.graph.block',
AppBskyGraphDefs: 'app.bsky.graph.defs', AppBskyGraphDefs: 'app.bsky.graph.defs',
AppBskyGraphFollow: 'app.bsky.graph.follow', 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 { export function validateViewerState(v: unknown): ValidationResult {
return lexicons.validate('app.bsky.actor.defs#viewerState', v) 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 { isObj, hasProp } from '../../../../util'
import { CID } from 'multiformats/cid' import { CID } from 'multiformats/cid'
import * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef' import * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef'
import * as AppBskyFeedDefs from '../feed/defs'
import * as AppBskyActorDefs from '../actor/defs' import * as AppBskyActorDefs from '../actor/defs'
import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs'
import * as AppBskyEmbedImages from './images' import * as AppBskyEmbedImages from './images'
@ -35,6 +36,7 @@ export interface View {
| ViewRecord | ViewRecord
| ViewNotFound | ViewNotFound
| ViewBlocked | ViewBlocked
| AppBskyFeedDefs.GeneratorView
| { $type: string; [k: string]: unknown } | { $type: string; [k: string]: unknown }
[k: string]: unknown [k: string]: unknown
} }

View File

@ -188,12 +188,14 @@ export function validateBlockedPost(v: unknown): ValidationResult {
export interface GeneratorView { export interface GeneratorView {
uri: string uri: string
cid: string
did?: string did?: string
creator: AppBskyActorDefs.ProfileView creator: AppBskyActorDefs.ProfileView
displayName?: string displayName?: string
description?: string description?: string
descriptionFacets?: AppBskyRichtextFacet.Main[] descriptionFacets?: AppBskyRichtextFacet.Main[]
avatar?: string avatar?: string
likeCount?: number
viewer?: GeneratorViewerState viewer?: GeneratorViewerState
indexedAt: string indexedAt: string
[k: string]: unknown [k: string]: unknown
@ -212,7 +214,7 @@ export function validateGeneratorView(v: unknown): ValidationResult {
} }
export interface GeneratorViewerState { export interface GeneratorViewerState {
subscribed?: boolean saved?: boolean
like?: string like?: string
[k: string]: unknown [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 { export interface HandlerError {
status: number status: number
message?: string message?: string
error?: 'UnknownFeed'
} }
export type HandlerOutput = HandlerError | HandlerSuccess 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 { export interface HandlerError {
status: number status: number
message?: string message?: string
error?: 'UnknownFeed'
} }
export type HandlerOutput = HandlerError | HandlerSuccess export type HandlerOutput = HandlerError | HandlerSuccess

View File

@ -12,6 +12,12 @@ import * as ComAtprotoAdminDefs from './defs'
export interface QueryParams { export interface QueryParams {
subject?: string subject?: string
resolved?: boolean 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 limit: number
cursor?: string cursor?: string
} }

View File

@ -13,6 +13,7 @@ export interface QueryParams {}
export interface InputSchema { export interface InputSchema {
email: string email: string
handle: string handle: string
did?: string
inviteCode?: string inviteCode?: string
password: string password: string
recoveryKey?: string recoveryKey?: string
@ -46,6 +47,8 @@ export interface HandlerError {
| 'InvalidInviteCode' | 'InvalidInviteCode'
| 'HandleNotAvailable' | 'HandleNotAvailable'
| 'UnsupportedDomain' | 'UnsupportedDomain'
| 'UnresolvableDid'
| 'IncompatibleDidDoc'
} }
export type HandlerOutput = HandlerError | HandlerSuccess 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 express from 'express'
import { DidResolver, MemoryCache } from '@atproto/did-resolver' import { DidResolver, MemoryCache } from '@atproto/did-resolver'
import { createServer } from './lexicon' 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 { createDb, Database, migrateToLatest } from './db'
import { FirehoseSubscription } from './subscription' import { FirehoseSubscription } from './subscription'
import { AppContext, Config } from './config' import { AppContext, Config } from './config'
@ -60,6 +61,7 @@ export class FeedGenerator {
cfg, cfg,
} }
feedGeneration(server, ctx) feedGeneration(server, ctx)
describeGenerator(server, ctx)
app.use(server.xrpc.router) app.use(server.xrpc.router)
app.use(wellKnown(cfg.hostname)) app.use(wellKnown(cfg.hostname))