From 3606414b792ebac16e27bd92e049ce7aa8fbaa38 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Fri, 19 May 2023 10:31:28 -0500 Subject: [PATCH] Add describeFeedGenerator route + multiple feeds (#19) * describeFeedGenerator route + multiple feeds * tweak readme --- README.md | 8 +- src/algos/index.ts | 14 + src/algos/whats-alf.ts | 42 ++ src/feed-generation.ts | 64 --- src/lexicon/index.ts | 67 +++- src/lexicon/lexicons.ts | 370 ++++++++++++++---- src/lexicon/types/app/bsky/actor/defs.ts | 41 ++ .../types/app/bsky/actor/getPreferences.ts | 40 ++ .../types/app/bsky/actor/putPreferences.ts | 36 ++ src/lexicon/types/app/bsky/embed/record.ts | 2 + src/lexicon/types/app/bsky/feed/defs.ts | 4 +- .../app/bsky/feed/describeFeedGenerator.ts | 76 ++++ src/lexicon/types/app/bsky/feed/getFeed.ts | 1 + .../types/app/bsky/feed/getFeedGenerator.ts | 44 +++ .../types/app/bsky/feed/getFeedSkeleton.ts | 1 + ...getBookmarkedFeeds.ts => getSavedFeeds.ts} | 0 .../feed/{bookmarkFeed.ts => saveFeed.ts} | 0 .../feed/{unbookmarkFeed.ts => unsaveFeed.ts} | 0 .../com/atproto/admin/getModerationReports.ts | 6 + .../types/com/atproto/server/createAccount.ts | 3 + src/methods/describe-generator.ts | 16 + src/methods/feed-generation.ts | 32 ++ src/server.ts | 4 +- 23 files changed, 710 insertions(+), 161 deletions(-) create mode 100644 src/algos/index.ts create mode 100644 src/algos/whats-alf.ts delete mode 100644 src/feed-generation.ts create mode 100644 src/lexicon/types/app/bsky/actor/getPreferences.ts create mode 100644 src/lexicon/types/app/bsky/actor/putPreferences.ts create mode 100644 src/lexicon/types/app/bsky/feed/describeFeedGenerator.ts create mode 100644 src/lexicon/types/app/bsky/feed/getFeedGenerator.ts rename src/lexicon/types/app/bsky/feed/{getBookmarkedFeeds.ts => getSavedFeeds.ts} (100%) rename src/lexicon/types/app/bsky/feed/{bookmarkFeed.ts => saveFeed.ts} (100%) rename src/lexicon/types/app/bsky/feed/{unbookmarkFeed.ts => unsaveFeed.ts} (100%) create mode 100644 src/methods/describe-generator.ts create mode 100644 src/methods/feed-generation.ts diff --git a/README.md b/README.md index b389e86..cc3156d 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/algos/index.ts b/src/algos/index.ts new file mode 100644 index 0000000..910c0e9 --- /dev/null +++ b/src/algos/index.ts @@ -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 + +const algos: Record = { + [whatsAlf.uri]: whatsAlf.handler, +} + +export default algos diff --git a/src/algos/whats-alf.ts b/src/algos/whats-alf.ts new file mode 100644 index 0000000..0afcadd --- /dev/null +++ b/src/algos/whats-alf.ts @@ -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, + } +} diff --git a/src/feed-generation.ts b/src/feed-generation.ts deleted file mode 100644 index e5c0578..0000000 --- a/src/feed-generation.ts +++ /dev/null @@ -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, - }, - } - }) -} diff --git a/src/lexicon/index.ts b/src/lexicon/index.ts index 7759d83..02e3ada 100644 --- a/src/lexicon/index.ts +++ b/src/lexicon/index.ts @@ -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( + cfg: ConfigOf>>, + ) { + const nsid = 'app.bsky.actor.getPreferences' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + getProfile( cfg: ConfigOf>>, ) { @@ -751,6 +762,13 @@ export class ActorNS { return this._server.xrpc.method(nsid, cfg) } + putPreferences( + cfg: ConfigOf>>, + ) { + const nsid = 'app.bsky.actor.putPreferences' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + searchActors( cfg: ConfigOf>>, ) { @@ -784,10 +802,13 @@ export class FeedNS { this._server = server } - bookmarkFeed( - cfg: ConfigOf>>, + describeFeedGenerator( + cfg: ConfigOf< + AV, + AppBskyFeedDescribeFeedGenerator.Handler> + >, ) { - 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( - cfg: ConfigOf>>, - ) { - const nsid = 'app.bsky.feed.getBookmarkedFeeds' // @ts-ignore - return this._server.xrpc.method(nsid, cfg) - } - getFeed( cfg: ConfigOf>>, ) { @@ -819,6 +833,13 @@ export class FeedNS { return this._server.xrpc.method(nsid, cfg) } + getFeedGenerator( + cfg: ConfigOf>>, + ) { + const nsid = 'app.bsky.feed.getFeedGenerator' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + getFeedSkeleton( cfg: ConfigOf>>, ) { @@ -854,6 +875,13 @@ export class FeedNS { return this._server.xrpc.method(nsid, cfg) } + getSavedFeeds( + cfg: ConfigOf>>, + ) { + const nsid = 'app.bsky.feed.getSavedFeeds' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + getTimeline( cfg: ConfigOf>>, ) { @@ -861,10 +889,17 @@ export class FeedNS { return this._server.xrpc.method(nsid, cfg) } - unbookmarkFeed( - cfg: ConfigOf>>, + saveFeed( + cfg: ConfigOf>>, ) { - const nsid = 'app.bsky.feed.unbookmarkFeed' // @ts-ignore + const nsid = 'app.bsky.feed.saveFeed' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + unsaveFeed( + cfg: ConfigOf>>, + ) { + const nsid = 'app.bsky.feed.unsaveFeed' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } } diff --git a/src/lexicon/lexicons.ts b/src/lexicon/lexicons.ts index d242054..70be29c 100644 --- a/src/lexicon/lexicons.ts +++ b/src/lexicon/lexicons.ts @@ -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', diff --git a/src/lexicon/types/app/bsky/actor/defs.ts b/src/lexicon/types/app/bsky/actor/defs.ts index 162e45a..deb26d7 100644 --- a/src/lexicon/types/app/bsky/actor/defs.ts +++ b/src/lexicon/types/app/bsky/actor/defs.ts @@ -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) +} diff --git a/src/lexicon/types/app/bsky/actor/getPreferences.ts b/src/lexicon/types/app/bsky/actor/getPreferences.ts new file mode 100644 index 0000000..018905c --- /dev/null +++ b/src/lexicon/types/app/bsky/actor/getPreferences.ts @@ -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 = (ctx: { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +}) => Promise | HandlerOutput diff --git a/src/lexicon/types/app/bsky/actor/putPreferences.ts b/src/lexicon/types/app/bsky/actor/putPreferences.ts new file mode 100644 index 0000000..ba0531c --- /dev/null +++ b/src/lexicon/types/app/bsky/actor/putPreferences.ts @@ -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 = (ctx: { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +}) => Promise | HandlerOutput diff --git a/src/lexicon/types/app/bsky/embed/record.ts b/src/lexicon/types/app/bsky/embed/record.ts index 3b13e3d..c45049b 100644 --- a/src/lexicon/types/app/bsky/embed/record.ts +++ b/src/lexicon/types/app/bsky/embed/record.ts @@ -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 } diff --git a/src/lexicon/types/app/bsky/feed/defs.ts b/src/lexicon/types/app/bsky/feed/defs.ts index 45a1520..1272c22 100644 --- a/src/lexicon/types/app/bsky/feed/defs.ts +++ b/src/lexicon/types/app/bsky/feed/defs.ts @@ -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 } diff --git a/src/lexicon/types/app/bsky/feed/describeFeedGenerator.ts b/src/lexicon/types/app/bsky/feed/describeFeedGenerator.ts new file mode 100644 index 0000000..82f1d15 --- /dev/null +++ b/src/lexicon/types/app/bsky/feed/describeFeedGenerator.ts @@ -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 = (ctx: { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +}) => Promise | 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) +} diff --git a/src/lexicon/types/app/bsky/feed/getFeed.ts b/src/lexicon/types/app/bsky/feed/getFeed.ts index 59114f9..850a44a 100644 --- a/src/lexicon/types/app/bsky/feed/getFeed.ts +++ b/src/lexicon/types/app/bsky/feed/getFeed.ts @@ -33,6 +33,7 @@ export interface HandlerSuccess { export interface HandlerError { status: number message?: string + error?: 'UnknownFeed' } export type HandlerOutput = HandlerError | HandlerSuccess diff --git a/src/lexicon/types/app/bsky/feed/getFeedGenerator.ts b/src/lexicon/types/app/bsky/feed/getFeedGenerator.ts new file mode 100644 index 0000000..0a2005d --- /dev/null +++ b/src/lexicon/types/app/bsky/feed/getFeedGenerator.ts @@ -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 = (ctx: { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +}) => Promise | HandlerOutput diff --git a/src/lexicon/types/app/bsky/feed/getFeedSkeleton.ts b/src/lexicon/types/app/bsky/feed/getFeedSkeleton.ts index efbb2ea..d19a275 100644 --- a/src/lexicon/types/app/bsky/feed/getFeedSkeleton.ts +++ b/src/lexicon/types/app/bsky/feed/getFeedSkeleton.ts @@ -33,6 +33,7 @@ export interface HandlerSuccess { export interface HandlerError { status: number message?: string + error?: 'UnknownFeed' } export type HandlerOutput = HandlerError | HandlerSuccess diff --git a/src/lexicon/types/app/bsky/feed/getBookmarkedFeeds.ts b/src/lexicon/types/app/bsky/feed/getSavedFeeds.ts similarity index 100% rename from src/lexicon/types/app/bsky/feed/getBookmarkedFeeds.ts rename to src/lexicon/types/app/bsky/feed/getSavedFeeds.ts diff --git a/src/lexicon/types/app/bsky/feed/bookmarkFeed.ts b/src/lexicon/types/app/bsky/feed/saveFeed.ts similarity index 100% rename from src/lexicon/types/app/bsky/feed/bookmarkFeed.ts rename to src/lexicon/types/app/bsky/feed/saveFeed.ts diff --git a/src/lexicon/types/app/bsky/feed/unbookmarkFeed.ts b/src/lexicon/types/app/bsky/feed/unsaveFeed.ts similarity index 100% rename from src/lexicon/types/app/bsky/feed/unbookmarkFeed.ts rename to src/lexicon/types/app/bsky/feed/unsaveFeed.ts diff --git a/src/lexicon/types/com/atproto/admin/getModerationReports.ts b/src/lexicon/types/com/atproto/admin/getModerationReports.ts index f92f207..8fc5ac8 100644 --- a/src/lexicon/types/com/atproto/admin/getModerationReports.ts +++ b/src/lexicon/types/com/atproto/admin/getModerationReports.ts @@ -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 } diff --git a/src/lexicon/types/com/atproto/server/createAccount.ts b/src/lexicon/types/com/atproto/server/createAccount.ts index 7e5ff93..4e212bf 100644 --- a/src/lexicon/types/com/atproto/server/createAccount.ts +++ b/src/lexicon/types/com/atproto/server/createAccount.ts @@ -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 diff --git a/src/methods/describe-generator.ts b/src/methods/describe-generator.ts new file mode 100644 index 0000000..3155e4f --- /dev/null +++ b/src/methods/describe-generator.ts @@ -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, + }, + } + }) +} diff --git a/src/methods/feed-generation.ts b/src/methods/feed-generation.ts new file mode 100644 index 0000000..0096a97 --- /dev/null +++ b/src/methods/feed-generation.ts @@ -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, + } + }) +} diff --git a/src/server.ts b/src/server.ts index 07d23b3..b57dffe 100644 --- a/src/server.ts +++ b/src/server.ts @@ -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))