From ec38b70e2eca4b92a3326df000eeb0a9cd2acb06 Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 12 Jun 2023 17:38:25 -0500 Subject: [PATCH] delete api & event-stream pkgs --- .../app-view/api/app/bsky/actor/getProfile.ts | 44 -- .../api/app/bsky/actor/getProfiles.ts | 37 -- .../api/app/bsky/actor/getSuggestions.ts | 79 --- .../api/app/bsky/actor/searchActors.ts | 90 --- .../app/bsky/actor/searchActorsTypeahead.ts | 83 --- .../app/bsky/feed/describeFeedGenerator.ts | 21 - .../api/app/bsky/feed/getActorFeeds.ts | 66 -- .../api/app/bsky/feed/getAuthorFeed.ts | 95 --- .../src/app-view/api/app/bsky/feed/getFeed.ts | 207 ------ .../api/app/bsky/feed/getFeedGenerator.ts | 79 --- .../api/app/bsky/feed/getFeedGenerators.ts | 42 -- .../app-view/api/app/bsky/feed/getLikes.ts | 77 --- .../api/app/bsky/feed/getPostThread.ts | 251 -------- .../app-view/api/app/bsky/feed/getPosts.ts | 58 -- .../api/app/bsky/feed/getRepostedBy.ts | 74 --- .../app-view/api/app/bsky/feed/getTimeline.ts | 88 --- .../app-view/api/app/bsky/graph/getBlocks.ts | 70 -- .../api/app/bsky/graph/getFollowers.ts | 77 --- .../app-view/api/app/bsky/graph/getFollows.ts | 77 --- .../app-view/api/app/bsky/graph/getList.ts | 97 --- .../api/app/bsky/graph/getListMutes.ts | 69 -- .../app-view/api/app/bsky/graph/getLists.ts | 66 -- .../app-view/api/app/bsky/graph/getMutes.ts | 68 -- .../app-view/api/app/bsky/graph/muteActor.ts | 34 - .../api/app/bsky/graph/muteActorList.ts | 33 - .../api/app/bsky/graph/unmuteActor.ts | 31 - .../api/app/bsky/graph/unmuteActorList.ts | 24 - .../pds/src/app-view/api/app/bsky/index.ts | 67 -- .../app/bsky/notification/getUnreadCount.ts | 59 -- .../bsky/notification/listNotifications.ts | 162 ----- .../api/app/bsky/notification/updateSeen.ts | 41 -- .../src/app-view/api/app/bsky/unspecced.ts | 177 ----- .../src/app-view/api/app/bsky/util/feed.ts | 19 - packages/pds/src/app-view/api/index.ts | 8 - .../src/app-view/event-stream/consumers.ts | 38 -- packages/pds/src/app-view/proxied/index.ts | 605 ------------------ 36 files changed, 3213 deletions(-) delete mode 100644 packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/feed/describeFeedGenerator.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/graph/getList.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/graph/getLists.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/graph/muteActor.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/graph/unmuteActor.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/graph/unmuteActorList.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/index.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/notification/updateSeen.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/unspecced.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/util/feed.ts delete mode 100644 packages/pds/src/app-view/api/index.ts delete mode 100644 packages/pds/src/app-view/event-stream/consumers.ts delete mode 100644 packages/pds/src/app-view/proxied/index.ts diff --git a/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts b/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts deleted file mode 100644 index 850bd1015de..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../../lexicon' -import { softDeleted } from '../../../../../db/util' -import AppContext from '../../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.actor.getProfile({ - auth: ctx.accessVerifier, - handler: async ({ req, auth, params }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.actor.getProfile( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { actor } = params - const { db, services } = ctx - const actorService = services.appView.actor(db) - - const actorRes = await actorService.getActor(actor, true) - - if (!actorRes) { - throw new InvalidRequestError('Profile not found') - } - if (softDeleted(actorRes)) { - throw new InvalidRequestError( - 'Account has been taken down', - 'AccountTakedown', - ) - } - - return { - encoding: 'application/json', - body: await actorService.views.profileDetailed(actorRes, requester), - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts b/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts deleted file mode 100644 index bcc129b8de2..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.actor.getProfiles({ - auth: ctx.accessVerifier, - handler: async ({ req, auth, params }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.actor.getProfiles( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { actors } = params - const { db, services } = ctx - const actorService = services.appView.actor(db) - - const actorsRes = await actorService.getActors(actors) - - return { - encoding: 'application/json', - body: { - profiles: await actorService.views.profileDetailed( - actorsRes, - requester, - ), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts b/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts deleted file mode 100644 index 7b7f8d1289d..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts +++ /dev/null @@ -1,79 +0,0 @@ -import AppContext from '../../../../../context' -import { notSoftDeletedClause } from '../../../../../db/util' -import { Server } from '../../../../../lexicon' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.actor.getSuggestions({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.actor.getSuggestions( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { limit, cursor } = params - - const db = ctx.db.db - const { services } = ctx - const { ref } = db.dynamic - - const graphService = ctx.services.appView.graph(ctx.db) - - let suggestionsQb = db - .selectFrom('suggested_follow') - .innerJoin('did_handle', 'suggested_follow.did', 'did_handle.did') - .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') - .innerJoin('profile_agg', 'profile_agg.did', 'did_handle.did') - .where(notSoftDeletedClause(ref('repo_root'))) - .where('did_handle.did', '!=', requester) - .whereNotExists((qb) => - qb - .selectFrom('follow') - .selectAll() - .where('creator', '=', requester) - .whereRef('subjectDid', '=', ref('did_handle.did')), - ) - .whereNotExists( - graphService.blockQb(requester, [ref('did_handle.did')]), - ) - .selectAll('did_handle') - .select('profile_agg.postsCount as postsCount') - .limit(limit) - .orderBy('suggested_follow.order', 'asc') - - if (cursor) { - const cursorRow = await db - .selectFrom('suggested_follow') - .where('did', '=', cursor) - .selectAll() - .executeTakeFirst() - if (cursorRow) { - suggestionsQb = suggestionsQb.where( - 'suggested_follow.order', - '>', - cursorRow.order, - ) - } - } - - const suggestionsRes = await suggestionsQb.execute() - - return { - encoding: 'application/json', - body: { - cursor: suggestionsRes.at(-1)?.did, - actors: await services.appView - .actor(ctx.db) - .views.profile(suggestionsRes, requester), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts b/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts deleted file mode 100644 index f3f95460686..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { sql } from 'kysely' -import AppContext from '../../../../../context' -import Database from '../../../../../db' -import { DidHandle } from '../../../../../db/tables/did-handle' -import { Server } from '../../../../../lexicon' -import * as Method from '../../../../../lexicon/types/app/bsky/actor/searchActors' -import { - cleanTerm, - getUserSearchQueryPg, - getUserSearchQuerySqlite, - SearchKeyset, -} from '../../../../../services/util/search' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.actor.searchActors({ - auth: ctx.accessVerifier, - handler: async ({ req, auth, params }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.actor.searchActors( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { services, db } = ctx - let { term, limit } = params - const { cursor } = params - - term = cleanTerm(term || '') - limit = Math.min(limit ?? 25, 100) - - if (!term) { - return { - encoding: 'application/json', - body: { - actors: [], - }, - } - } - - const results = - db.dialect === 'pg' - ? await getResultsPg(db, { term, limit, cursor }) - : await getResultsSqlite(db, { term, limit, cursor }) - - const keyset = new SearchKeyset(sql``, sql``) - - const actors = await services.appView - .actor(db) - .views.profile(results, requester) - - const filtered = actors.filter( - (actor) => !actor.viewer?.blocking && !actor.viewer?.blockedBy, - ) - - return { - encoding: 'application/json', - body: { - cursor: keyset.packFromResult(results), - actors: filtered, - }, - } - }, - }) -} - -const getResultsPg: GetResultsFn = async (db, { term, limit, cursor }) => { - return await getUserSearchQueryPg(db, { term: term || '', limit, cursor }) - .select('distance') - .selectAll('did_handle') - .execute() -} - -const getResultsSqlite: GetResultsFn = async (db, { term, limit, cursor }) => { - return await getUserSearchQuerySqlite(db, { term: term || '', limit, cursor }) - .leftJoin('profile', 'profile.creator', 'did_handle.did') - .select(sql`0`.as('distance')) - .selectAll('did_handle') - .execute() -} - -type GetResultsFn = ( - db: Database, - opts: Method.QueryParams & { limit: number }, -) => Promise<(DidHandle & { distance: number })[]> diff --git a/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts b/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts deleted file mode 100644 index fc34adba5a0..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts +++ /dev/null @@ -1,83 +0,0 @@ -import AppContext from '../../../../../context' -import Database from '../../../../../db' -import { Server } from '../../../../../lexicon' -import * as Method from '../../../../../lexicon/types/app/bsky/actor/searchActorsTypeahead' -import { - cleanTerm, - getUserSearchQueryPg, - getUserSearchQuerySqlite, -} from '../../../../../services/util/search' -import { DidHandle } from '../../../../../db/tables/did-handle' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.actor.searchActorsTypeahead({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = - await ctx.appviewAgent.api.app.bsky.actor.searchActorsTypeahead( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { services, db } = ctx - let { term, limit } = params - - term = cleanTerm(term || '') - limit = Math.min(limit ?? 25, 100) - - if (!term) { - return { - encoding: 'application/json', - body: { - actors: [], - }, - } - } - - const results = - ctx.db.dialect === 'pg' - ? await getResultsPg(ctx.db, { term, limit }) - : await getResultsSqlite(ctx.db, { term, limit }) - - const actors = await services.appView - .actor(db) - .views.profileBasic(results, requester) - - const filtered = actors.filter( - (actor) => !actor.viewer?.blocking && !actor.viewer?.blockedBy, - ) - - return { - encoding: 'application/json', - body: { - actors: filtered, - }, - } - }, - }) -} - -const getResultsPg: GetResultsFn = async (db, { term, limit }) => { - return await getUserSearchQueryPg(db, { term: term || '', limit }) - .selectAll('did_handle') - .execute() -} - -const getResultsSqlite: GetResultsFn = async (db, { term, limit }) => { - return await getUserSearchQuerySqlite(db, { term: term || '', limit }) - .leftJoin('profile', 'profile.creator', 'did_handle.did') - .selectAll('did_handle') - .execute() -} - -type GetResultsFn = ( - db: Database, - opts: Method.QueryParams & { limit: number }, -) => Promise diff --git a/packages/pds/src/app-view/api/app/bsky/feed/describeFeedGenerator.ts b/packages/pds/src/app-view/api/app/bsky/feed/describeFeedGenerator.ts deleted file mode 100644 index 89a8a1f48dd..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/feed/describeFeedGenerator.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' -import { MethodNotImplementedError } from '@atproto/xrpc-server' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.describeFeedGenerator(async () => { - if (!ctx.cfg.feedGenDid) { - throw new MethodNotImplementedError() - } - - const feeds = Object.keys(ctx.algos).map((uri) => ({ uri })) - - return { - encoding: 'application/json', - body: { - did: ctx.cfg.feedGenDid, - feeds, - }, - } - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts b/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts deleted file mode 100644 index fe6ac23416f..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' -import { TimeCidKeyset, paginate } from '../../../../../db/pagination' -import { InvalidRequestError } from '@atproto/xrpc-server' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.getActorFeeds({ - auth: ctx.accessVerifier, - handler: async ({ req, auth, params }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getActorFeeds( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { actor, limit, cursor } = params - - const actorService = ctx.services.appView.actor(ctx.db) - const feedService = ctx.services.appView.feed(ctx.db) - - const creatorRes = await actorService.getActor(actor) - if (!creatorRes) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } - - const { ref } = ctx.db.db.dynamic - let feedsQb = feedService - .selectFeedGeneratorQb(requester) - .where('feed_generator.creator', '=', creatorRes.did) - - const keyset = new TimeCidKeyset( - ref('feed_generator.createdAt'), - ref('feed_generator.cid'), - ) - feedsQb = paginate(feedsQb, { - limit, - cursor, - keyset, - }) - - const [feedsRes, creatorProfile] = await Promise.all([ - feedsQb.execute(), - actorService.views.profile(creatorRes, requester), - ]) - const profiles = { [creatorProfile.did]: creatorProfile } - - const feeds = feedsRes.map((row) => - feedService.views.formatFeedGeneratorView(row, profiles), - ) - - return { - encoding: 'application/json', - body: { - cursor: keyset.packFromResult(feedsRes), - feeds, - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts deleted file mode 100644 index f30e5d2773a..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { Server } from '../../../../../lexicon' -import { FeedKeyset } from '../util/feed' -import { paginate } from '../../../../../db/pagination' -import AppContext from '../../../../../context' -import { FeedRow } from '../../../../services/feed' -import { InvalidRequestError } from '@atproto/xrpc-server' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.getAuthorFeed({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getAuthorFeed( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { actor, limit, cursor } = params - const db = ctx.db.db - const { ref } = db.dynamic - - // first verify there is not a block between requester & subject - const blocks = await ctx.services.appView - .graph(ctx.db) - .getBlocks(requester, actor) - if (blocks.blocking) { - throw new InvalidRequestError( - `Requester has blocked actor: ${actor}`, - 'BlockedActor', - ) - } else if (blocks.blockedBy) { - throw new InvalidRequestError( - `Requester is blocked by actor: $${actor}`, - 'BlockedByActor', - ) - } - - const accountService = ctx.services.account(ctx.db) - const feedService = ctx.services.appView.feed(ctx.db) - const graphService = ctx.services.appView.graph(ctx.db) - - const userLookupCol = actor.startsWith('did:') - ? 'did_handle.did' - : 'did_handle.handle' - const actorDidQb = db - .selectFrom('did_handle') - .select('did') - .where(userLookupCol, '=', actor) - .limit(1) - - let feedItemsQb = feedService - .selectFeedItemQb() - .where('originatorDid', '=', actorDidQb) - .where((qb) => - // Hide reposts of muted content - qb - .where('type', '=', 'post') - .orWhere((qb) => - accountService.whereNotMuted(qb, requester, [ - ref('post.creator'), - ]), - ), - ) - .whereNotExists(graphService.blockQb(requester, [ref('post.creator')])) - - const keyset = new FeedKeyset( - ref('feed_item.sortAt'), - ref('feed_item.cid'), - ) - - feedItemsQb = paginate(feedItemsQb, { - limit, - cursor, - keyset, - }) - - const feedItems: FeedRow[] = await feedItemsQb.execute() - const feed = await feedService.hydrateFeed(feedItems, requester) - - return { - encoding: 'application/json', - body: { - feed, - cursor: keyset.packFromResult(feedItems), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts deleted file mode 100644 index ca1ea03c495..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { - InvalidRequestError, - UpstreamFailureError, - createServiceAuthHeaders, - ServerTimer, - serverTimingHeader, -} from '@atproto/xrpc-server' -import { ResponseType, XRPCError } from '@atproto/xrpc' -import { - DidDocument, - PoorlyFormattedDidDocumentError, - getFeedGen, -} from '@atproto/identity' -import { AtpAgent, AppBskyFeedGetFeedSkeleton } from '@atproto/api' -import { SkeletonFeedPost } from '../../../../../lexicon/types/app/bsky/feed/defs' -import { QueryParams as GetFeedParams } from '../../../../../lexicon/types/app/bsky/feed/getFeed' -import { OutputSchema as SkeletonOutput } from '../../../../../lexicon/types/app/bsky/feed/getFeedSkeleton' -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' -import { FeedRow } from '../../../../services/feed' -import { AlgoResponse } from '../../../../../feed-gen/types' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.getFeed({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const { data: feed } = - await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( - { feed: params.feed }, - await ctx.serviceAuthHeaders(requester), - ) - const res = await ctx.appviewAgent.api.app.bsky.feed.getFeed( - params, - await ctx.serviceAuthHeaders(requester, feed.view.did), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { feed } = params - const feedService = ctx.services.appView.feed(ctx.db) - const localAlgo = ctx.algos[feed] - - const timerSkele = new ServerTimer('skele').start() - const { feedItems, ...rest } = - localAlgo !== undefined - ? await localAlgo(ctx, params, requester) - : await skeletonFromFeedGen(ctx, params, requester) - timerSkele.stop() - - const timerHydr = new ServerTimer('hydr').start() - const hydrated = await feedService.hydrateFeed(feedItems, requester) - timerHydr.stop() - - return { - encoding: 'application/json', - body: { - ...rest, - feed: hydrated, - }, - headers: { - 'server-timing': serverTimingHeader([timerSkele, timerHydr]), - }, - } - }, - }) -} - -async function skeletonFromFeedGen( - ctx: AppContext, - params: GetFeedParams, - requester: string, -): Promise { - const { feed } = params - // Resolve and fetch feed skeleton - const found = await ctx.db.db - .selectFrom('feed_generator') - .where('uri', '=', feed) - .select('feedDid') - .executeTakeFirst() - if (!found) { - throw new InvalidRequestError('could not find feed') - } - const feedDid = found.feedDid - - let resolved: DidDocument | null - try { - resolved = await ctx.idResolver.did.resolve(feedDid) - } catch (err) { - if (err instanceof PoorlyFormattedDidDocumentError) { - throw new InvalidRequestError(`invalid did document: ${feedDid}`) - } - throw err - } - if (!resolved) { - throw new InvalidRequestError(`could not resolve did document: ${feedDid}`) - } - - const fgEndpoint = getFeedGen(resolved) - if (!fgEndpoint) { - throw new InvalidRequestError( - `invalid feed generator service details in did document: ${feedDid}`, - ) - } - - const agent = new AtpAgent({ service: fgEndpoint }) - const headers = await createServiceAuthHeaders({ - iss: requester, - aud: feedDid, - keypair: ctx.repoSigningKey, - }) - - let skeleton: SkeletonOutput - try { - const result = await agent.api.app.bsky.feed.getFeedSkeleton( - params, - headers, - ) - skeleton = result.data - } catch (err) { - if (err instanceof AppBskyFeedGetFeedSkeleton.UnknownFeedError) { - throw new InvalidRequestError(err.message, 'UnknownFeed') - } - if (err instanceof XRPCError) { - if (err.status === ResponseType.Unknown) { - throw new UpstreamFailureError('feed unavailable') - } - if (err.status === ResponseType.InvalidResponse) { - throw new UpstreamFailureError( - 'feed provided an invalid response', - 'InvalidFeedResponse', - ) - } - } - throw err - } - - const { feed: skeletonFeed, ...rest } = skeleton - - // Hydrate feed skeleton - const { ref } = ctx.db.db.dynamic - const feedService = ctx.services.appView.feed(ctx.db) - const graphService = ctx.services.appView.graph(ctx.db) - const accountService = ctx.services.account(ctx.db) - const feedItemUris = skeletonFeed.map(getSkeleFeedItemUri) - - const feedItems = feedItemUris.length - ? await feedService - .selectFeedItemQb() - .where('feed_item.uri', 'in', feedItemUris) - .where((qb) => - // Hide posts and reposts of or by muted actors - accountService.whereNotMuted(qb, requester, [ - ref('post.creator'), - ref('originatorDid'), - ]), - ) - .whereNotExists( - graphService.blockQb(requester, [ - ref('post.creator'), - ref('originatorDid'), - ]), - ) - .execute() - : [] - - const orderedItems = getOrderedFeedItems(skeletonFeed, feedItems, params) - return { - ...rest, - feedItems: orderedItems, - } -} - -function getSkeleFeedItemUri(item: SkeletonFeedPost) { - if (typeof item.reason?.repost === 'string') { - return item.reason.repost - } - return item.post -} - -function getOrderedFeedItems( - skeletonItems: SkeletonFeedPost[], - feedItems: FeedRow[], - params: GetFeedParams, -) { - const SKIP = [] - const feedItemsByUri = feedItems.reduce((acc, item) => { - return Object.assign(acc, { [item.uri]: item }) - }, {} as Record) - // enforce limit param in the case that the feedgen does not - if (skeletonItems.length > params.limit) { - skeletonItems = skeletonItems.slice(0, params.limit) - } - return skeletonItems.flatMap((item) => { - const uri = getSkeleFeedItemUri(item) - const feedItem = feedItemsByUri[uri] - if (!feedItem || item.post !== feedItem.postUri) { - // Couldn't find the record, or skeleton repost referenced the wrong post - return SKIP - } - return feedItem - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts b/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts deleted file mode 100644 index 1e7ebff6aca..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { - DidDocument, - PoorlyFormattedDidDocumentError, - getFeedGen, -} from '@atproto/identity' -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.getFeedGenerator({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { feed } = params - - const feedService = ctx.services.appView.feed(ctx.db) - - const got = await feedService.getFeedGeneratorViews([feed], requester) - const feedInfo = got[feed] - if (!feedInfo) { - throw new InvalidRequestError('could not find feed') - } - - const feedDid = feedInfo.feedDid - let resolved: DidDocument | null - try { - resolved = await ctx.idResolver.did.resolve(feedDid) - } catch (err) { - if (err instanceof PoorlyFormattedDidDocumentError) { - throw new InvalidRequestError(`invalid did document: ${feedDid}`) - } - throw err - } - if (!resolved) { - throw new InvalidRequestError( - `could not resolve did document: ${feedDid}`, - ) - } - - const fgEndpoint = getFeedGen(resolved) - if (!fgEndpoint) { - throw new InvalidRequestError( - `invalid feed generator service details in did document: ${feedDid}`, - ) - } - - const profiles = await feedService.getActorViews( - [feedInfo.creator], - requester, - ) - const feedView = feedService.views.formatFeedGeneratorView( - feedInfo, - profiles, - ) - - return { - encoding: 'application/json', - body: { - view: feedView, - // @TODO temporarily hard-coding to true while external feedgens catch-up on describeFeedGenerator - isOnline: true, - isValid: true, - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts b/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts deleted file mode 100644 index 3052c642a46..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.getFeedGenerators({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerators( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { feeds } = params - - const feedService = ctx.services.appView.feed(ctx.db) - - const genViews = await feedService.getFeedGeneratorViews(feeds, requester) - const genList = Object.values(genViews) - - const creators = genList.map((gen) => gen.creator) - const profiles = await feedService.getActorViews(creators, requester) - - const feedViews = genList.map((gen) => - feedService.views.formatFeedGeneratorView(gen, profiles), - ) - - return { - encoding: 'application/json', - body: { - feeds: feedViews, - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts b/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts deleted file mode 100644 index a29f69a8be7..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Server } from '../../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../../db/pagination' -import AppContext from '../../../../../context' -import { notSoftDeletedClause } from '../../../../../db/util' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.getLikes({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getLikes( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { uri, limit, cursor, cid } = params - const { services, db } = ctx - const { ref } = db.db.dynamic - - const graphService = ctx.services.appView.graph(ctx.db) - - let builder = db.db - .selectFrom('like') - .where('like.subject', '=', uri) - .innerJoin('did_handle as creator', 'creator.did', 'like.creator') - .innerJoin( - 'repo_root as creator_repo', - 'creator_repo.did', - 'like.creator', - ) - .where(notSoftDeletedClause(ref('creator_repo'))) - .whereNotExists(graphService.blockQb(requester, [ref('like.creator')])) - .selectAll('creator') - .select([ - 'like.cid as cid', - 'like.createdAt as createdAt', - 'like.indexedAt as indexedAt', - ]) - - if (cid) { - builder = builder.where('like.subjectCid', '=', cid) - } - - const keyset = new TimeCidKeyset(ref('like.createdAt'), ref('like.cid')) - builder = paginate(builder, { - limit, - cursor, - keyset, - }) - - const likesRes = await builder.execute() - const actors = await services.appView - .actor(db) - .views.profile(likesRes, requester) - - return { - encoding: 'application/json', - body: { - uri, - cid, - cursor: keyset.packFromResult(likesRes), - likes: likesRes.map((row, i) => ({ - createdAt: row.createdAt, - indexedAt: row.indexedAt, - actor: actors[i], - })), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts b/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts deleted file mode 100644 index d2fac10d67f..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' -import { - ActorViewMap, - FeedEmbeds, - FeedRow, - FeedService, - PostInfoMap, -} from '../../../../services/feed' -import { Labels } from '../../../../services/label' -import { - BlockedPost, - NotFoundPost, - ThreadViewPost, - isNotFoundPost, -} from '../../../../../lexicon/types/app/bsky/feed/defs' - -export type PostThread = { - post: FeedRow - parent?: PostThread | ParentNotFoundError - replies?: PostThread[] -} - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.getPostThread({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getPostThread( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { uri, depth, parentHeight } = params - - const feedService = ctx.services.appView.feed(ctx.db) - const labelService = ctx.services.appView.label(ctx.db) - - const threadData = await getThreadData( - feedService, - uri, - depth, - parentHeight, - ) - if (!threadData) { - throw new InvalidRequestError(`Post not found: ${uri}`, 'NotFound') - } - const relevant = getRelevantIds(threadData) - const [actors, posts, embeds, labels] = await Promise.all([ - feedService.getActorViews(Array.from(relevant.dids), requester, { - skipLabels: true, - }), - feedService.getPostViews(Array.from(relevant.uris), requester), - feedService.embedsForPosts(Array.from(relevant.uris), requester), - labelService.getLabelsForSubjects([...relevant.uris, ...relevant.dids]), - ]) - - const thread = composeThread( - threadData, - feedService, - posts, - actors, - embeds, - labels, - ) - - if (isNotFoundPost(thread)) { - // @TODO technically this could be returned as a NotFoundPost based on lexicon - throw new InvalidRequestError(`Post not found: ${uri}`, 'NotFound') - } - - return { - encoding: 'application/json', - body: { thread }, - } - }, - }) -} - -const composeThread = ( - threadData: PostThread, - feedService: FeedService, - posts: PostInfoMap, - actors: ActorViewMap, - embeds: FeedEmbeds, - labels: Labels, -): ThreadViewPost | NotFoundPost | BlockedPost => { - const post = feedService.views.formatPostView( - threadData.post.postUri, - actors, - posts, - embeds, - labels, - ) - - if (!post) { - return { - $type: 'app.bsky.feed.defs#notFoundPost', - uri: threadData.post.postUri, - notFound: true, - } - } - - if (post.author.viewer?.blocking || post.author.viewer?.blockedBy) { - return { - $type: 'app.bsky.feed.defs#blockedPost', - uri: threadData.post.postUri, - blocked: true, - } - } - - let parent: ThreadViewPost | NotFoundPost | BlockedPost | undefined - if (threadData.parent) { - if (threadData.parent instanceof ParentNotFoundError) { - parent = { - $type: 'app.bsky.feed.defs#notFoundPost', - uri: threadData.parent.uri, - notFound: true, - } - } else { - parent = composeThread( - threadData.parent, - feedService, - posts, - actors, - embeds, - labels, - ) - } - } - - let replies: (ThreadViewPost | NotFoundPost | BlockedPost)[] | undefined - if (threadData.replies) { - replies = threadData.replies.map((reply) => - composeThread(reply, feedService, posts, actors, embeds, labels), - ) - } - - return { - $type: 'app.bsky.feed.defs#threadViewPost', - post, - parent, - replies, - } -} - -const getRelevantIds = ( - thread: PostThread, -): { dids: Set; uris: Set } => { - const dids = new Set() - const uris = new Set() - if (thread.parent && !(thread.parent instanceof ParentNotFoundError)) { - const fromParent = getRelevantIds(thread.parent) - fromParent.dids.forEach((did) => dids.add(did)) - fromParent.uris.forEach((uri) => uris.add(uri)) - } - if (thread.replies) { - for (const reply of thread.replies) { - const fromChild = getRelevantIds(reply) - fromChild.dids.forEach((did) => dids.add(did)) - fromChild.uris.forEach((uri) => uris.add(uri)) - } - } - dids.add(thread.post.postAuthorDid) - uris.add(thread.post.postUri) - return { dids, uris } -} - -const getThreadData = async ( - feedService: FeedService, - uri: string, - depth: number, - parentHeight: number, -): Promise => { - const [parents, children] = await Promise.all([ - feedService - .selectPostQb() - .innerJoin('post_hierarchy', 'post_hierarchy.ancestorUri', 'post.uri') - .where('post_hierarchy.uri', '=', uri) - .execute(), - feedService - .selectPostQb() - .innerJoin('post_hierarchy', 'post_hierarchy.uri', 'post.uri') - .where('post_hierarchy.uri', '!=', uri) - .where('post_hierarchy.ancestorUri', '=', uri) - .where('depth', '<=', depth) - .orderBy('post.createdAt', 'desc') - .execute(), - ]) - const parentsByUri = parents.reduce((acc, parent) => { - return Object.assign(acc, { [parent.postUri]: parent }) - }, {} as Record) - const childrenByParentUri = children.reduce((acc, child) => { - if (!child.replyParent) return acc - acc[child.replyParent] ??= [] - acc[child.replyParent].push(child) - return acc - }, {} as Record) - const post = parentsByUri[uri] - if (!post) return null - return { - post, - parent: post.replyParent - ? getParentData(parentsByUri, post.replyParent, parentHeight) - : undefined, - replies: getChildrenData(childrenByParentUri, uri, depth), - } -} - -const getParentData = ( - postsByUri: Record, - uri: string, - depth: number, -): PostThread | ParentNotFoundError | undefined => { - if (depth === 0) return undefined - const post = postsByUri[uri] - if (!post) return new ParentNotFoundError(uri) - return { - post, - parent: post.replyParent - ? getParentData(postsByUri, post.replyParent, depth - 1) - : undefined, - replies: [], - } -} - -const getChildrenData = ( - childrenByParentUri: Record, - uri: string, - depth: number, -): PostThread[] | undefined => { - if (depth === 0) return undefined - const children = childrenByParentUri[uri] ?? [] - return children.map((row) => ({ - post: row, - replies: getChildrenData(childrenByParentUri, row.postUri, depth - 1), - })) -} - -class ParentNotFoundError extends Error { - constructor(public uri: string) { - super(`Parent not found: ${uri}`) - } -} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts b/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts deleted file mode 100644 index 0f5df994e8e..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts +++ /dev/null @@ -1,58 +0,0 @@ -import * as common from '@atproto/common' -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' -import { AtUri } from '@atproto/uri' -import { PostView } from '@atproto/api/src/client/types/app/bsky/feed/defs' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.getPosts({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getPosts( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const feedService = ctx.services.appView.feed(ctx.db) - const labelService = ctx.services.appView.label(ctx.db) - - const uris = common.dedupeStrs(params.uris) - const dids = common.dedupeStrs( - params.uris.map((uri) => new AtUri(uri).hostname), - ) - - const [actors, postViews, embeds, labels] = await Promise.all([ - feedService.getActorViews(dids, requester, { skipLabels: true }), - feedService.getPostViews(uris, requester), - feedService.embedsForPosts(uris, requester), - labelService.getLabelsForSubjects([...uris, ...dids]), - ]) - - const posts: PostView[] = [] - for (const uri of uris) { - const post = feedService.views.formatPostView( - uri, - actors, - postViews, - embeds, - labels, - ) - if (post) { - posts.push(post) - } - } - - return { - encoding: 'application/json', - body: { posts }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts b/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts deleted file mode 100644 index 5d5948954b5..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Server } from '../../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../../db/pagination' -import AppContext from '../../../../../context' -import { notSoftDeletedClause } from '../../../../../db/util' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.getRepostedBy({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getRepostedBy( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { uri, limit, cursor, cid } = params - const { services, db } = ctx - const { ref } = db.db.dynamic - - const graphService = ctx.services.appView.graph(ctx.db) - - let builder = db.db - .selectFrom('repost') - .where('repost.subject', '=', uri) - .innerJoin('did_handle as creator', 'creator.did', 'repost.creator') - .innerJoin( - 'repo_root as creator_repo', - 'creator_repo.did', - 'repost.creator', - ) - .where(notSoftDeletedClause(ref('creator_repo'))) - .whereNotExists( - graphService.blockQb(requester, [ref('repost.creator')]), - ) - .selectAll('creator') - .select(['repost.cid as cid', 'repost.createdAt as createdAt']) - - if (cid) { - builder = builder.where('repost.subjectCid', '=', cid) - } - - const keyset = new TimeCidKeyset( - ref('repost.createdAt'), - ref('repost.cid'), - ) - builder = paginate(builder, { - limit, - cursor, - keyset, - }) - - const repostedByRes = await builder.execute() - const repostedBy = await services.appView - .actor(db) - .views.profile(repostedByRes, requester) - - return { - encoding: 'application/json', - body: { - uri, - cid, - repostedBy, - cursor: keyset.packFromResult(repostedByRes), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts b/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts deleted file mode 100644 index 8da47aab4af..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../../lexicon' -import { FeedAlgorithm, FeedKeyset, getFeedDateThreshold } from '../util/feed' -import { paginate } from '../../../../../db/pagination' -import AppContext from '../../../../../context' -import { FeedRow } from '../../../../services/feed' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.getTimeline({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getTimeline( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { algorithm, limit, cursor } = params - const db = ctx.db.db - const { ref } = db.dynamic - - if (algorithm && algorithm !== FeedAlgorithm.ReverseChronological) { - throw new InvalidRequestError(`Unsupported algorithm: ${algorithm}`) - } - - const accountService = ctx.services.account(ctx.db) - const feedService = ctx.services.appView.feed(ctx.db) - const graphService = ctx.services.appView.graph(ctx.db) - - const followingIdsSubquery = db - .selectFrom('follow') - .select('follow.subjectDid') - .where('follow.creator', '=', requester) - - const keyset = new FeedKeyset( - ref('feed_item.sortAt'), - ref('feed_item.cid'), - ) - const sortFrom = keyset.unpack(cursor)?.primary - - let feedItemsQb = feedService - .selectFeedItemQb() - .where((qb) => - qb - .where('originatorDid', '=', requester) - .orWhere('originatorDid', 'in', followingIdsSubquery), - ) - .where((qb) => - // Hide posts and reposts of or by muted actors - accountService.whereNotMuted(qb, requester, [ - ref('post.creator'), - ref('originatorDid'), - ]), - ) - .whereNotExists( - graphService.blockQb(requester, [ - ref('post.creator'), - ref('originatorDid'), - ]), - ) - .where('feed_item.sortAt', '>', getFeedDateThreshold(sortFrom)) - - feedItemsQb = paginate(feedItemsQb, { - limit, - cursor, - keyset, - tryIndex: true, - }) - - const feedItems: FeedRow[] = await feedItemsQb.execute() - const feed = await feedService.hydrateFeed(feedItems, requester) - - return { - encoding: 'application/json', - body: { - feed, - cursor: keyset.packFromResult(feedItems), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts b/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts deleted file mode 100644 index e14a12bd37a..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Server } from '../../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../../db/pagination' -import AppContext from '../../../../../context' -import { notSoftDeletedClause } from '../../../../../db/util' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.graph.getBlocks({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.graph.getBlocks( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { limit, cursor } = params - const { services, db } = ctx - const { ref } = db.db.dynamic - - let blocksReq = ctx.db.db - .selectFrom('actor_block') - .where('actor_block.creator', '=', requester) - .innerJoin( - 'did_handle as subject', - 'subject.did', - 'actor_block.subjectDid', - ) - .innerJoin( - 'repo_root as subject_repo', - 'subject_repo.did', - 'actor_block.subjectDid', - ) - .where(notSoftDeletedClause(ref('subject_repo'))) - .selectAll('subject') - .select([ - 'actor_block.cid as cid', - 'actor_block.createdAt as createdAt', - ]) - - const keyset = new TimeCidKeyset( - ref('actor_block.createdAt'), - ref('actor_block.cid'), - ) - blocksReq = paginate(blocksReq, { - limit, - cursor, - keyset, - }) - - const blocksRes = await blocksReq.execute() - - const actorService = services.appView.actor(db) - const blocks = await actorService.views.profile(blocksRes, requester) - - return { - encoding: 'application/json', - body: { - blocks, - cursor: keyset.packFromResult(blocksRes), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts b/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts deleted file mode 100644 index 3b3281edb41..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../../db/pagination' -import AppContext from '../../../../../context' -import { notSoftDeletedClause } from '../../../../../db/util' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.graph.getFollowers({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.graph.getFollowers( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { actor, limit, cursor } = params - const { services, db } = ctx - const { ref } = db.db.dynamic - - const actorService = services.appView.actor(db) - const graphService = services.appView.graph(db) - - const subjectRes = await actorService.getActor(actor) - if (!subjectRes) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } - - let followersReq = ctx.db.db - .selectFrom('follow') - .where('follow.subjectDid', '=', subjectRes.did) - .innerJoin('did_handle as creator', 'creator.did', 'follow.creator') - .innerJoin( - 'repo_root as creator_repo', - 'creator_repo.did', - 'follow.creator', - ) - .where(notSoftDeletedClause(ref('creator_repo'))) - .whereNotExists( - graphService.blockQb(requester, [ref('follow.subjectDid')]), - ) - .selectAll('creator') - .select(['follow.cid as cid', 'follow.createdAt as createdAt']) - - const keyset = new TimeCidKeyset( - ref('follow.createdAt'), - ref('follow.cid'), - ) - followersReq = paginate(followersReq, { - limit, - cursor, - keyset, - }) - - const followersRes = await followersReq.execute() - const [followers, subject] = await Promise.all([ - actorService.views.profile(followersRes, requester), - actorService.views.profile(subjectRes, requester), - ]) - - return { - encoding: 'application/json', - body: { - subject, - followers, - cursor: keyset.packFromResult(followersRes), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts b/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts deleted file mode 100644 index 4a8bf888fc6..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../../db/pagination' -import AppContext from '../../../../../context' -import { notSoftDeletedClause } from '../../../../../db/util' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.graph.getFollows({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.graph.getFollows( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { actor, limit, cursor } = params - const { services, db } = ctx - const { ref } = db.db.dynamic - - const actorService = services.appView.actor(db) - const graphService = services.appView.graph(db) - - const creatorRes = await actorService.getActor(actor) - if (!creatorRes) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } - - let followsReq = ctx.db.db - .selectFrom('follow') - .where('follow.creator', '=', creatorRes.did) - .innerJoin('did_handle as subject', 'subject.did', 'follow.subjectDid') - .innerJoin( - 'repo_root as subject_repo', - 'subject_repo.did', - 'follow.subjectDid', - ) - .where(notSoftDeletedClause(ref('subject_repo'))) - .whereNotExists( - graphService.blockQb(requester, [ref('follow.creator')]), - ) - .selectAll('subject') - .select(['follow.cid as cid', 'follow.createdAt as createdAt']) - - const keyset = new TimeCidKeyset( - ref('follow.createdAt'), - ref('follow.cid'), - ) - followsReq = paginate(followsReq, { - limit, - cursor, - keyset, - }) - - const followsRes = await followsReq.execute() - const [follows, subject] = await Promise.all([ - actorService.views.profile(followsRes, requester), - actorService.views.profile(creatorRes, requester), - ]) - - return { - encoding: 'application/json', - body: { - subject, - follows, - cursor: keyset.packFromResult(followsRes), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getList.ts b/packages/pds/src/app-view/api/app/bsky/graph/getList.ts deleted file mode 100644 index f9e393ba3f8..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/graph/getList.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../../db/pagination' -import AppContext from '../../../../../context' -import { ProfileView } from '../../../../../lexicon/types/app/bsky/actor/defs' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.graph.getList({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.graph.getList( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { list, limit, cursor } = params - const { services, db } = ctx - const { ref } = db.db.dynamic - - const graphService = ctx.services.appView.graph(ctx.db) - - const listRes = await graphService - .getListsQb(requester) - .where('list.uri', '=', list) - .executeTakeFirst() - if (!listRes) { - throw new InvalidRequestError(`List not found: ${list}`) - } - - let itemsReq = graphService - .getListItemsQb() - .where('list_item.listUri', '=', list) - .where('list_item.creator', '=', listRes.creator) - - const keyset = new TimeCidKeyset( - ref('list_item.createdAt'), - ref('list_item.cid'), - ) - itemsReq = paginate(itemsReq, { - limit, - cursor, - keyset, - }) - const itemsRes = await itemsReq.execute() - - const actorService = services.appView.actor(db) - const profiles = await actorService.views.profile(itemsRes, requester) - const profilesMap = profiles.reduce( - (acc, cur) => ({ - ...acc, - [cur.did]: cur, - }), - {} as Record, - ) - - const items = itemsRes.map((item) => ({ - subject: profilesMap[item.did], - })) - - const creator = await actorService.views.profile(listRes, requester) - - const subject = { - uri: listRes.uri, - creator, - name: listRes.name, - purpose: listRes.purpose, - description: listRes.description ?? undefined, - descriptionFacets: listRes.descriptionFacets - ? JSON.parse(listRes.descriptionFacets) - : undefined, - avatar: listRes.avatarCid - ? ctx.imgUriBuilder.getCommonSignedUri('avatar', listRes.avatarCid) - : undefined, - indexedAt: listRes.indexedAt, - viewer: { - muted: !!listRes.viewerMuted, - }, - } - - return { - encoding: 'application/json', - body: { - items, - list: subject, - cursor: keyset.packFromResult(itemsRes), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts b/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts deleted file mode 100644 index e3a486ccf0b..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Server } from '../../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../../db/pagination' -import AppContext from '../../../../../context' -import { ProfileView } from '../../../../../lexicon/types/app/bsky/actor/defs' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.graph.getListMutes({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.graph.getListMutes( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { limit, cursor } = params - const { db } = ctx - const { ref } = db.db.dynamic - - const graphService = ctx.services.appView.graph(ctx.db) - - let listsReq = graphService - .getListsQb(requester) - .whereExists( - ctx.db.db - .selectFrom('list_mute') - .where('list_mute.mutedByDid', '=', requester) - .whereRef('list_mute.listUri', '=', ref('list.uri')) - .selectAll(), - ) - - const keyset = new TimeCidKeyset(ref('list.createdAt'), ref('list.cid')) - listsReq = paginate(listsReq, { - limit, - cursor, - keyset, - }) - const listsRes = await listsReq.execute() - - const actorService = ctx.services.appView.actor(ctx.db) - const profiles = await actorService.views.profile(listsRes, requester) - const profilesMap = profiles.reduce( - (acc, cur) => ({ - ...acc, - [cur.did]: cur, - }), - {} as Record, - ) - - const lists = listsRes.map((row) => - graphService.formatListView(row, profilesMap), - ) - - return { - encoding: 'application/json', - body: { - lists, - cursor: keyset.packFromResult(listsRes), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts b/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts deleted file mode 100644 index cb77e2bf2e8..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../../db/pagination' -import AppContext from '../../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.graph.getLists({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.graph.getLists( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { actor, limit, cursor } = params - const { services, db } = ctx - const { ref } = db.db.dynamic - - const actorService = services.appView.actor(db) - const graphService = services.appView.graph(db) - - const creatorRes = await actorService.getActor(actor) - if (!creatorRes) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } - - let listsReq = graphService - .getListsQb(requester) - .where('list.creator', '=', creatorRes.did) - - const keyset = new TimeCidKeyset(ref('list.createdAt'), ref('list.cid')) - listsReq = paginate(listsReq, { - limit, - cursor, - keyset, - }) - - const [listsRes, creator] = await Promise.all([ - listsReq.execute(), - actorService.views.profile(creatorRes, requester), - ]) - const profileMap = { - [creator.did]: creator, - } - - const lists = listsRes.map((row) => - graphService.formatListView(row, profileMap), - ) - - return { - encoding: 'application/json', - body: { - lists, - cursor: keyset.packFromResult(listsRes), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts b/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts deleted file mode 100644 index 98218309800..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Server } from '../../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../../db/pagination' -import AppContext from '../../../../../context' -import { notSoftDeletedClause } from '../../../../../db/util' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.graph.getMutes({ - auth: ctx.accessVerifier, - handler: async ({ req, auth, params }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.graph.getMutes( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { limit, cursor } = params - const { services, db } = ctx - const { ref } = ctx.db.db.dynamic - - let mutesReq = ctx.db.db - .selectFrom('mute') - .innerJoin('did_handle as actor', 'actor.did', 'mute.did') - .innerJoin('repo_root', 'repo_root.did', 'mute.did') - .where(notSoftDeletedClause(ref('repo_root'))) - .where('mute.mutedByDid', '=', requester) - .selectAll('actor') - .select('mute.createdAt as createdAt') - - const keyset = new CreatedAtDidKeyset( - ref('mute.createdAt'), - ref('mute.did'), - ) - mutesReq = paginate(mutesReq, { - limit, - cursor, - keyset, - }) - - const mutesRes = await mutesReq.execute() - - // @NOTE calling into app-view, will eventually be replaced - const actorService = services.appView.actor(db) - - return { - encoding: 'application/json', - body: { - cursor: keyset.packFromResult(mutesRes), - mutes: await actorService.views.profile(mutesRes, requester), - }, - } - }, - }) -} - -export class CreatedAtDidKeyset extends TimeCidKeyset<{ - createdAt: string - did: string // dids are treated identically to cids in TimeCidKeyset -}> { - labelResult(result: { createdAt: string; did: string }) { - return { primary: result.createdAt, secondary: result.did } - } -} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/muteActor.ts b/packages/pds/src/app-view/api/app/bsky/graph/muteActor.ts deleted file mode 100644 index ce3dfcf4125..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/graph/muteActor.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.graph.muteActor({ - auth: ctx.accessVerifier, - handler: async ({ auth, input }) => { - const { actor } = input.body - const requester = auth.credentials.did - const { db, services } = ctx - - const subject = await services.account(db).getAccount(actor) - if (!subject) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } - if (subject.did === requester) { - throw new InvalidRequestError('Cannot mute oneself') - } - - await services.account(db).mute({ - did: subject.did, - mutedByDid: requester, - }) - - if (ctx.cfg.bskyAppViewEndpoint) { - await ctx.appviewAgent.api.app.bsky.graph.muteActor(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), - encoding: 'application/json', - }) - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts b/packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts deleted file mode 100644 index 982a652f7b8..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Server } from '../../../../../lexicon' -import * as lex from '../../../../../lexicon/lexicons' -import AppContext from '../../../../../context' -import { AtUri } from '@atproto/uri' -import { InvalidRequestError } from '@atproto/xrpc-server' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.graph.muteActorList({ - auth: ctx.accessVerifier, - handler: async ({ auth, input }) => { - const { list } = input.body - const requester = auth.credentials.did - - const listUri = new AtUri(list) - const collId = lex.ids.AppBskyGraphList - if (listUri.collection !== collId) { - throw new InvalidRequestError(`Invalid collection: expected: ${collId}`) - } - - await ctx.services.account(ctx.db).muteActorList({ - list, - mutedByDid: requester, - }) - - if (ctx.cfg.bskyAppViewEndpoint) { - await ctx.appviewAgent.api.app.bsky.graph.muteActorList(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), - encoding: 'application/json', - }) - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/unmuteActor.ts b/packages/pds/src/app-view/api/app/bsky/graph/unmuteActor.ts deleted file mode 100644 index ae9e8dac742..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/graph/unmuteActor.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Server } from '../../../../../lexicon' -import { InvalidRequestError } from '@atproto/xrpc-server' -import AppContext from '../../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.graph.unmuteActor({ - auth: ctx.accessVerifier, - handler: async ({ auth, input }) => { - const { actor } = input.body - const requester = auth.credentials.did - const { db, services } = ctx - - const subject = await services.account(db).getAccount(actor) - if (!subject) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } - - await services.account(db).unmute({ - did: subject.did, - mutedByDid: requester, - }) - - if (ctx.cfg.bskyAppViewEndpoint) { - await ctx.appviewAgent.api.app.bsky.graph.unmuteActor(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), - encoding: 'application/json', - }) - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/unmuteActorList.ts b/packages/pds/src/app-view/api/app/bsky/graph/unmuteActorList.ts deleted file mode 100644 index 69012072100..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/graph/unmuteActorList.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.graph.unmuteActorList({ - auth: ctx.accessVerifier, - handler: async ({ auth, input }) => { - const { list } = input.body - const requester = auth.credentials.did - - await ctx.services.account(ctx.db).unmuteActorList({ - list, - mutedByDid: requester, - }) - - if (ctx.cfg.bskyAppViewEndpoint) { - await ctx.appviewAgent.api.app.bsky.graph.unmuteActorList(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), - encoding: 'application/json', - }) - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/index.ts b/packages/pds/src/app-view/api/app/bsky/index.ts deleted file mode 100644 index 9d895ea6667..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/index.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' -import getTimeline from './feed/getTimeline' -import getActorFeeds from './feed/getActorFeeds' -import getAuthorFeed from './feed/getAuthorFeed' -import getFeedGenerator from './feed/getFeedGenerator' -import getFeedGenerators from './feed/getFeedGenerators' -import describeFeedGenerator from './feed/describeFeedGenerator' -import getFeed from './feed/getFeed' -import getLikes from './feed/getLikes' -import getPostThread from './feed/getPostThread' -import getPosts from './feed/getPosts' -import getProfile from './actor/getProfile' -import getProfiles from './actor/getProfiles' -import getRepostedBy from './feed/getRepostedBy' -import getBlocks from './graph/getBlocks' -import getFollowers from './graph/getFollowers' -import getFollows from './graph/getFollows' -import getList from './graph/getList' -import getListMutes from './graph/getListMutes' -import getLists from './graph/getLists' -import getMutes from './graph/getMutes' -import muteActor from './graph/muteActor' -import muteActorList from './graph/muteActorList' -import unmuteActor from './graph/unmuteActor' -import unmuteActorList from './graph/unmuteActorList' -import getUsersSearch from './actor/searchActors' -import getUsersTypeahead from './actor/searchActorsTypeahead' -import getSuggestions from './actor/getSuggestions' -import listNotifications from './notification/listNotifications' -import getUnreadCount from './notification/getUnreadCount' -import updateSeen from './notification/updateSeen' -import unspecced from './unspecced' - -export default function (server: Server, ctx: AppContext) { - getTimeline(server, ctx) - getActorFeeds(server, ctx) - getAuthorFeed(server, ctx) - getFeedGenerator(server, ctx) - getFeedGenerators(server, ctx) - describeFeedGenerator(server, ctx) - getFeed(server, ctx) - getLikes(server, ctx) - getPostThread(server, ctx) - getPosts(server, ctx) - getProfile(server, ctx) - getProfiles(server, ctx) - getRepostedBy(server, ctx) - getBlocks(server, ctx) - getFollowers(server, ctx) - getFollows(server, ctx) - getList(server, ctx) - getListMutes(server, ctx) - getLists(server, ctx) - getMutes(server, ctx) - muteActor(server, ctx) - muteActorList(server, ctx) - unmuteActor(server, ctx) - unmuteActorList(server, ctx) - getUsersSearch(server, ctx) - getUsersTypeahead(server, ctx) - getSuggestions(server, ctx) - listNotifications(server, ctx) - getUnreadCount(server, ctx) - updateSeen(server, ctx) - unspecced(server, ctx) -} diff --git a/packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts b/packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts deleted file mode 100644 index e16e2be46ce..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../../lexicon' -import { countAll, notSoftDeletedClause } from '../../../../../db/util' -import AppContext from '../../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.notification.getUnreadCount({ - auth: ctx.accessVerifier, - handler: async ({ req, auth, params }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = - await ctx.appviewAgent.api.app.bsky.notification.getUnreadCount( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { seenAt } = params - const { ref } = ctx.db.db.dynamic - if (seenAt) { - throw new InvalidRequestError('The seenAt parameter is unsupported') - } - - const accountService = ctx.services.account(ctx.db) - - const result = await ctx.db.db - .selectFrom('user_notification as notif') - .select(countAll.as('count')) - .innerJoin('user_account', 'user_account.did', 'notif.userDid') - .innerJoin('user_state', 'user_state.did', 'user_account.did') - .innerJoin( - 'repo_root as author_repo', - 'author_repo.did', - 'notif.author', - ) - .innerJoin('record', 'record.uri', 'notif.recordUri') - .where(notSoftDeletedClause(ref('author_repo'))) - .where(notSoftDeletedClause(ref('record'))) - .where('notif.userDid', '=', requester) - .where((qb) => - accountService.whereNotMuted(qb, requester, [ref('notif.author')]), - ) - .whereRef('notif.indexedAt', '>', 'user_state.lastSeenNotifs') - .executeTakeFirst() - - const count = result?.count ?? 0 - - return { - encoding: 'application/json', - body: { count }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts b/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts deleted file mode 100644 index c9ec4885e74..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { sql } from 'kysely' -import { InvalidRequestError } from '@atproto/xrpc-server' -import * as common from '@atproto/common' -import { Server } from '../../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../../db/pagination' -import AppContext from '../../../../../context' -import { notSoftDeletedClause, valuesList } from '../../../../../db/util' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.notification.listNotifications({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = - await ctx.appviewAgent.api.app.bsky.notification.listNotifications( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { limit, cursor, seenAt } = params - const { ref } = ctx.db.db.dynamic - if (seenAt) { - throw new InvalidRequestError('The seenAt parameter is unsupported') - } - - const accountService = ctx.services.account(ctx.db) - const graphService = ctx.services.appView.graph(ctx.db) - - let notifBuilder = ctx.db.db - .selectFrom('user_notification as notif') - .innerJoin('did_handle as author', 'author.did', 'notif.author') - .innerJoin( - 'repo_root as author_repo', - 'author_repo.did', - 'notif.author', - ) - .innerJoin('record', 'record.uri', 'notif.recordUri') - .where(notSoftDeletedClause(ref('author_repo'))) - .where(notSoftDeletedClause(ref('record'))) - .where('notif.userDid', '=', requester) - .where((qb) => - accountService.whereNotMuted(qb, requester, [ref('notif.author')]), - ) - .whereNotExists(graphService.blockQb(requester, [ref('notif.author')])) - .where((clause) => - clause - .where('reasonSubject', 'is', null) - .orWhereExists( - ctx.db.db - .selectFrom('record as subject') - .selectAll() - .whereRef('subject.uri', '=', ref('notif.reasonSubject')), - ), - ) - .select([ - 'notif.recordUri as uri', - 'notif.recordCid as cid', - 'author.did as authorDid', - 'author.handle as authorHandle', - 'notif.reason as reason', - 'notif.reasonSubject as reasonSubject', - 'notif.indexedAt as indexedAt', - ]) - - const keyset = new NotifsKeyset( - ref('notif.indexedAt'), - ref('notif.recordCid'), - ) - notifBuilder = paginate(notifBuilder, { - cursor, - limit, - keyset, - }) - - const userStateQuery = ctx.db.db - .selectFrom('user_state') - .selectAll() - .where('did', '=', requester) - .executeTakeFirst() - - const [userState, notifs] = await Promise.all([ - userStateQuery, - notifBuilder.execute(), - ]) - - if (!userState) { - throw new InvalidRequestError(`Could not find user: ${requester}`) - } - - const recordTuples = notifs.map((notif) => { - return sql`${notif.authorDid}, ${notif.cid}` - }) - - const emptyBlocksResult: { cid: string; bytes: Uint8Array }[] = [] - const blocksQb = recordTuples.length - ? ctx.db.db - .selectFrom('ipld_block') - .whereRef(sql`(creator, cid)`, 'in', valuesList(recordTuples)) - .select(['cid', 'content as bytes']) - : null - - const actorService = ctx.services.appView.actor(ctx.db) - - // @NOTE calling into app-view, will eventually be replaced - const labelService = ctx.services.appView.label(ctx.db) - const recordUris = notifs.map((notif) => notif.uri) - const [blocks, authors, labels] = await Promise.all([ - blocksQb ? blocksQb.execute() : emptyBlocksResult, - actorService.views.profile( - notifs.map((notif) => ({ - did: notif.authorDid, - handle: notif.authorHandle, - })), - requester, - ), - labelService.getLabelsForUris(recordUris), - ]) - - const bytesByCid = blocks.reduce((acc, block) => { - acc[block.cid] = block.bytes - return acc - }, {} as Record) - - const notifications = notifs.flatMap((notif, i) => { - const bytes = bytesByCid[notif.cid] - if (!bytes) return [] // Filter out - return { - uri: notif.uri, - cid: notif.cid, - author: authors[i], - reason: notif.reason, - reasonSubject: notif.reasonSubject || undefined, - record: common.cborBytesToRecord(bytes), - isRead: notif.indexedAt <= userState.lastSeenNotifs, - indexedAt: notif.indexedAt, - labels: labels[notif.uri] ?? [], - } - }) - - return { - encoding: 'application/json', - body: { - notifications, - cursor: keyset.packFromResult(notifs), - }, - } - }, - }) -} - -type NotifRow = { indexedAt: string; cid: string } -class NotifsKeyset extends TimeCidKeyset { - labelResult(result: NotifRow) { - return { primary: result.indexedAt, secondary: result.cid } - } -} diff --git a/packages/pds/src/app-view/api/app/bsky/notification/updateSeen.ts b/packages/pds/src/app-view/api/app/bsky/notification/updateSeen.ts deleted file mode 100644 index 717f358f77c..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/notification/updateSeen.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Server } from '../../../../../lexicon' -import { InvalidRequestError } from '@atproto/xrpc-server' -import AppContext from '../../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.notification.updateSeen({ - auth: ctx.accessVerifier, - handler: async ({ input, auth }) => { - const { seenAt } = input.body - const requester = auth.credentials.did - - let parsed: string - try { - parsed = new Date(seenAt).toISOString() - } catch (_err) { - throw new InvalidRequestError('Invalid date') - } - - const user = await ctx.services.account(ctx.db).getAccount(requester) - if (!user) { - throw new InvalidRequestError(`Could not find user: ${requester}`) - } - - await ctx.db.db - .updateTable('user_state') - .set({ lastSeenNotifs: parsed }) - .where('did', '=', user.did) - .executeTakeFirst() - - if (ctx.cfg.bskyAppViewEndpoint) { - await ctx.appviewAgent.api.app.bsky.notification.updateSeen( - input.body, - { - ...(await ctx.serviceAuthHeaders(requester)), - encoding: 'application/json', - }, - ) - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/unspecced.ts b/packages/pds/src/app-view/api/app/bsky/unspecced.ts deleted file mode 100644 index defa197b709..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/unspecced.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { Server } from '../../../../lexicon' -import { FeedKeyset } from './util/feed' -import { paginate } from '../../../../db/pagination' -import AppContext from '../../../../context' -import { FeedRow } from '../../../services/feed' -import { isPostView } from '../../../../lexicon/types/app/bsky/feed/defs' -import { NotEmptyArray } from '@atproto/common' -import { isViewRecord } from '../../../../lexicon/types/app/bsky/embed/record' -import { countAll, valuesList } from '../../../../db/util' - -const NO_WHATS_HOT_LABELS: NotEmptyArray = [ - '!no-promote', - 'corpse', - 'self-harm', -] - -const NSFW_LABELS = ['porn', 'sexual', 'nudity', 'underwear'] - -// @NOTE currently relies on the hot-classic feed being configured on the pds -// THIS IS A TEMPORARY UNSPECCED ROUTE -export default function (server: Server, ctx: AppContext) { - server.app.bsky.unspecced.getPopular({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const hotClassicUri = Object.keys(ctx.algos).find((uri) => - uri.endsWith('/hot-classic'), - ) - if (!hotClassicUri) { - return { - encoding: 'application/json', - body: { feed: [] }, - } - } - const { data: feed } = - await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( - { feed: hotClassicUri }, - await ctx.serviceAuthHeaders(requester), - ) - const res = await ctx.appviewAgent.api.app.bsky.feed.getFeed( - { ...params, feed: hotClassicUri }, - await ctx.serviceAuthHeaders(requester, feed.view.did), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { limit, cursor, includeNsfw } = params - const db = ctx.db.db - const { ref } = db.dynamic - - const accountService = ctx.services.account(ctx.db) - const feedService = ctx.services.appView.feed(ctx.db) - const graphService = ctx.services.appView.graph(ctx.db) - - const labelsToFilter = includeNsfw - ? NO_WHATS_HOT_LABELS - : [...NO_WHATS_HOT_LABELS, ...NSFW_LABELS] - - const postsQb = feedService - .selectPostQb() - .leftJoin('post_agg', 'post_agg.uri', 'post.uri') - .where('post_agg.likeCount', '>=', 12) - .where('post.replyParent', 'is', null) - .whereNotExists((qb) => - qb - .selectFrom('label') - .selectAll() - .whereRef('val', 'in', valuesList(labelsToFilter)) - .where('neg', '=', 0) - .where((clause) => - clause - .whereRef('label.uri', '=', ref('post.creator')) - .orWhereRef('label.uri', '=', ref('post.uri')), - ), - ) - .where((qb) => - accountService.whereNotMuted(qb, requester, [ref('post.creator')]), - ) - .whereNotExists(graphService.blockQb(requester, [ref('post.creator')])) - - const keyset = new FeedKeyset(ref('sortAt'), ref('cid')) - - let feedQb = ctx.db.db.selectFrom(postsQb.as('feed_items')).selectAll() - feedQb = paginate(feedQb, { limit, cursor, keyset }) - - const feedItems: FeedRow[] = await feedQb.execute() - const feed = await feedService.hydrateFeed(feedItems, requester) - - // filter out any quote post where the internal post has a filtered label - const noLabeledQuotePosts = feed.filter((post) => { - const quoteView = post.post.embed?.record - if (!quoteView || !isViewRecord(quoteView)) return true - for (const label of quoteView.labels || []) { - if (labelsToFilter.includes(label.val)) return false - } - return true - }) - - // remove record embeds in our response - const noRecordEmbeds = noLabeledQuotePosts.map((post) => { - delete post.post.record['embed'] - if (post.reply) { - if (isPostView(post.reply.parent)) { - delete post.reply.parent.record['embed'] - } - if (isPostView(post.reply.root)) { - delete post.reply.root.record['embed'] - } - } - return post - }) - - return { - encoding: 'application/json', - body: { - feed: noRecordEmbeds, - cursor: keyset.packFromResult(feedItems), - }, - } - }, - }) - - server.app.bsky.unspecced.getPopularFeedGenerators({ - auth: ctx.accessVerifier, - handler: async ({ auth }) => { - const requester = auth.credentials.did - const db = ctx.db.db - const { ref } = db.dynamic - const feedService = ctx.services.appView.feed(ctx.db) - - const mostPopularFeeds = await ctx.db.db - .selectFrom('feed_generator') - .select([ - 'uri', - ctx.db.db - .selectFrom('like') - .whereRef('like.subject', '=', ref('feed_generator.uri')) - .select(countAll.as('count')) - .as('likeCount'), - ]) - .orderBy('likeCount', 'desc') - .orderBy('cid', 'desc') - .limit(50) - .execute() - - const genViews = await feedService.getFeedGeneratorViews( - mostPopularFeeds.map((feed) => feed.uri), - requester, - ) - - const genList = Object.values(genViews) - const creators = genList.map((gen) => gen.creator) - const profiles = await feedService.getActorViews(creators, requester) - - const feedViews = genList.map((gen) => - feedService.views.formatFeedGeneratorView(gen, profiles), - ) - - return { - encoding: 'application/json', - body: { - feeds: feedViews.sort((feedA, feedB) => { - const likeA = feedA.likeCount ?? 0 - const likeB = feedB.likeCount ?? 0 - const likeDiff = likeB - likeA - if (likeDiff !== 0) return likeDiff - return feedB.cid.localeCompare(feedA.cid) - }), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/util/feed.ts b/packages/pds/src/app-view/api/app/bsky/util/feed.ts deleted file mode 100644 index da350cab4a3..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/util/feed.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { TimeCidKeyset } from '../../../../../db/pagination' -import { FeedRow } from '../../../../services/feed' - -export enum FeedAlgorithm { - ReverseChronological = 'reverse-chronological', -} - -export class FeedKeyset extends TimeCidKeyset { - labelResult(result: FeedRow) { - return { primary: result.sortAt, secondary: result.cid } - } -} - -// For users with sparse feeds, avoid scanning more than one week for a single page -export const getFeedDateThreshold = (from: string | undefined, days = 7) => { - const timelineDateThreshold = from ? new Date(from) : new Date() - timelineDateThreshold.setDate(timelineDateThreshold.getDate() - days) - return timelineDateThreshold.toISOString() -} diff --git a/packages/pds/src/app-view/api/index.ts b/packages/pds/src/app-view/api/index.ts deleted file mode 100644 index 1f963088637..00000000000 --- a/packages/pds/src/app-view/api/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Server } from '../../lexicon' -import appBsky from './app/bsky' -import AppContext from '../../context' - -export default function (server: Server, ctx: AppContext) { - appBsky(server, ctx) - return server -} diff --git a/packages/pds/src/app-view/event-stream/consumers.ts b/packages/pds/src/app-view/event-stream/consumers.ts deleted file mode 100644 index 48fc963fb09..00000000000 --- a/packages/pds/src/app-view/event-stream/consumers.ts +++ /dev/null @@ -1,38 +0,0 @@ -import AppContext from '../../context' -import Database from '../../db' -import { - DeleteRecord, - IndexRecord, - DeleteRepo, -} from '../../event-stream/messages' - -// Used w/ in-process PDS as alternative to the repo subscription -export const listen = (ctx: AppContext) => { - ctx.messageDispatcher.listen('index_record', { - async listener(input: { db: Database; message: IndexRecord }) { - const { db, message } = input - const indexingService = ctx.services.appView.indexing(db) - await indexingService.indexRecord( - message.uri, - message.cid, - message.obj, - message.action, - message.timestamp, - ) - }, - }) - ctx.messageDispatcher.listen('delete_record', { - async listener(input: { db: Database; message: DeleteRecord }) { - const { db, message } = input - const indexingService = ctx.services.appView.indexing(db) - await indexingService.deleteRecord(message.uri, message.cascading) - }, - }) - ctx.messageDispatcher.listen('delete_repo', { - async listener(input: { db: Database; message: DeleteRepo }) { - const { db, message } = input - const indexingService = ctx.services.appView.indexing(db) - await indexingService.deleteForUser(message.did) - }, - }) -} diff --git a/packages/pds/src/app-view/proxied/index.ts b/packages/pds/src/app-view/proxied/index.ts deleted file mode 100644 index abd343a5029..00000000000 --- a/packages/pds/src/app-view/proxied/index.ts +++ /dev/null @@ -1,605 +0,0 @@ -import { AtpAgent } from '@atproto/api' -import { dedupeStrs } from '@atproto/common' -import { - InvalidRequestError, - createServiceAuthHeaders, -} from '@atproto/xrpc-server' -import { Server } from '../../lexicon' -import AppContext from '../../context' -import { - FeedViewPost, - ThreadViewPost, - isPostView, - isReasonRepost, - isThreadViewPost, -} from '../../lexicon/types/app/bsky/feed/defs' - -export default function (server: Server, ctx: AppContext) { - const appviewEndpoint = ctx.cfg.bskyAppViewEndpoint - const appviewDid = ctx.cfg.bskyAppViewDid - if (!appviewEndpoint) { - throw new Error('Could not find bsky appview endpoint') - } - if (!appviewDid) { - throw new Error('Could not find bsky appview did') - } - - const agent = new AtpAgent({ service: appviewEndpoint }) - - const headers = async (did: string, aud?: string) => { - return createServiceAuthHeaders({ - iss: did, - aud: aud ?? appviewDid, - keypair: ctx.repoSigningKey, - }) - } - - server.app.bsky.actor.getProfile({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.actor.getProfile( - params, - await headers(requester), - ) - const profile = res.data - const muted = await ctx.services - .account(ctx.db) - .getMute(requester, profile.did) - profile.viewer ??= {} - profile.viewer.muted = muted - return { - encoding: 'application/json', - body: profile, - } - }, - }) - - server.app.bsky.actor.getProfiles({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.actor.getProfiles( - params, - await headers(requester), - ) - const profiles = res.data.profiles - - const dids = profiles.map((profile) => profile.did) - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - - for (const profile of profiles) { - profile.viewer ??= {} - profile.viewer.muted = mutes[profile.did] ?? false - } - - return { - encoding: 'application/json', - body: { - profiles, - }, - } - }, - }) - - server.app.bsky.actor.getSuggestions({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.actor.getSuggestions( - params, - await headers(requester), - ) - const { cursor, actors } = res.data - const dids = actors.map((actor) => actor.did) - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - for (const actor of actors) { - actor.viewer ??= {} - actor.viewer.muted = mutes[actor.did] ?? false - } - - return { - encoding: 'application/json', - body: { - cursor, - actors, - }, - } - }, - }) - - server.app.bsky.actor.searchActors({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.actor.searchActors( - params, - await headers(requester), - ) - - const { cursor, actors } = res.data - const dids = actors.map((actor) => actor.did) - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - for (const actor of actors) { - actor.viewer ??= {} - actor.viewer.muted = mutes[actor.did] ?? false - } - - return { - encoding: 'application/json', - body: { - cursor, - actors, - }, - } - }, - }) - - server.app.bsky.actor.searchActorsTypeahead({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.actor.searchActorsTypeahead( - params, - await headers(requester), - ) - - const { actors } = res.data - const dids = actors.map((actor) => actor.did) - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - for (const actor of actors) { - actor.viewer ??= {} - actor.viewer.muted = mutes[actor.did] ?? false - } - - return { - encoding: 'application/json', - body: { actors }, - } - }, - }) - - server.app.bsky.feed.getAuthorFeed({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.feed.getAuthorFeed( - params, - await headers(requester), - ) - const { cursor, feed } = res.data - const dids = didsForFeedViewPosts(feed) - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - const hydrated = processFeedViewPostMutes(feed, mutes, (post) => { - // eliminate posts by a muted account that have been reposted by account of feed - return ( - post.reason !== undefined && - isReasonRepost(post.reason) && - mutes[post.post.author.did] === true - ) - }) - - return { - encoding: 'application/json', - body: { - cursor, - feed: hydrated, - }, - } - }, - }) - - server.app.bsky.feed.getLikes({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.feed.getLikes( - params, - await headers(requester), - ) - - const { cursor, uri, cid, likes } = res.data - const dids = likes.map((like) => like.actor.did) - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - - for (const like of likes) { - like.actor.viewer ??= {} - like.actor.viewer.muted = mutes[like.actor.did] ?? false - } - - return { - encoding: 'application/json', - body: { - cursor, - uri, - cid, - likes, - }, - } - }, - }) - - server.app.bsky.feed.getPostThread({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.feed.getPostThread( - params, - await headers(requester), - ) - const { thread } = res.data - if (!isThreadViewPost(thread)) { - return { - encoding: 'application/json', - body: { thread }, - } - } - - const dids = isThreadViewPost(thread) ? didsForThread(thread) : [] - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - const hydrated = processThreadViewMutes(thread, mutes) - - return { - encoding: 'application/json', - body: { - thread: hydrated, - }, - } - }, - }) - - server.app.bsky.feed.getPosts({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.feed.getPosts( - params, - await headers(requester), - ) - const { posts } = res.data - - const dids = posts.map((p) => p.author.did) - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - for (const post of posts) { - post.author.viewer ??= {} - post.author.viewer.muted = mutes[post.author.did] ?? false - } - - return { - encoding: 'application/json', - body: { - posts, - }, - } - }, - }) - - server.app.bsky.feed.getRepostedBy({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.feed.getRepostedBy( - params, - await headers(requester), - ) - - const { cursor, uri, cid, repostedBy } = res.data - const dids = repostedBy.map((repost) => repost.did) - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - - for (const repost of repostedBy) { - repost.viewer ??= {} - repost.viewer.muted = mutes[repost.did] ?? false - } - - return { - encoding: 'application/json', - body: { - cursor, - uri, - cid, - repostedBy, - }, - } - }, - }) - - server.app.bsky.feed.getTimeline({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.feed.getTimeline( - params, - await headers(requester), - ) - const { cursor, feed } = res.data - const dids = didsForFeedViewPosts(feed) - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - const hydrated = processFeedViewPostMutes(feed, mutes, (post) => { - // remove posts & reposts from muted accounts - return ( - mutes[post.post.author.did] === true || - (post.reason !== undefined && - isReasonRepost(post.reason) && - mutes[post.reason.by.did]) === true - ) - }) - - return { - encoding: 'application/json', - body: { - cursor, - feed: hydrated, - }, - } - }, - }) - - server.app.bsky.graph.getFollowers({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.graph.getFollowers( - params, - await headers(requester), - ) - - const { cursor, subject, followers } = res.data - const dids = [subject.did, ...followers.map((follower) => follower.did)] - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - - for (const follower of followers) { - follower.viewer ??= {} - follower.viewer.muted = mutes[follower.did] ?? false - } - subject.viewer ??= {} - subject.viewer.muted = mutes[subject.did] ?? false - - return { - encoding: 'application/json', - body: { - cursor, - subject, - followers, - }, - } - }, - }) - - server.app.bsky.graph.getFollows({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.graph.getFollows( - params, - await headers(requester), - ) - const { cursor, subject, follows } = res.data - const dids = [subject.did, ...follows.map((follow) => follow.did)] - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - - for (const follow of follows) { - follow.viewer ??= {} - follow.viewer.muted = mutes[follow.did] ?? false - } - subject.viewer ??= {} - subject.viewer.muted = mutes[subject.did] ?? false - - return { - encoding: 'application/json', - body: { - cursor, - subject, - follows, - }, - } - }, - }) - - // @NOTE currently relies on the hot-classic feed being configured on the pds - server.app.bsky.unspecced.getPopular({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const hotClassicUri = Object.keys(ctx.algos).find((uri) => - uri.endsWith('/hot-classic'), - ) - if (!hotClassicUri) { - return { - encoding: 'application/json', - body: { feed: [] }, - } - } - const requester = auth.credentials.did - // @TODO cache the feedgen did lookup - const { data: feed } = await agent.api.app.bsky.feed.getFeedGenerator( - { feed: hotClassicUri }, - await headers(requester), - ) - const res = await agent.api.app.bsky.feed.getFeed( - { ...params, feed: hotClassicUri }, - await headers(requester, feed.view.did), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.unspecced.getPopularFeedGenerators({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.unspecced.getPopularFeedGenerators( - params, - await headers(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.notification.getUnreadCount({ - auth: ctx.accessVerifier, - handler: async ({ auth }) => { - const requester = auth.credentials.did - const seenAt = await ctx.services - .account(ctx.db) - .getLastSeenNotifs(requester) - const res = await agent.api.app.bsky.notification.getUnreadCount( - { seenAt }, - await headers(requester), - ) - const { count } = res.data - return { - encoding: 'application/json', - body: { - count, - }, - } - }, - }) - - server.app.bsky.notification.listNotifications({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const seenAt = await ctx.services - .account(ctx.db) - .getLastSeenNotifs(requester) - const res = await agent.api.app.bsky.notification.listNotifications( - { ...params, seenAt }, - await headers(requester), - ) - const { cursor, notifications } = res.data - const dids = notifications.map((notif) => notif.author.did) - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - const filtered = notifications.filter( - (notif) => mutes[notif.author.did] !== true, - ) - for (const notif of filtered) { - notif.isRead = seenAt !== undefined && seenAt >= notif.indexedAt - notif.author.viewer ??= {} - notif.author.viewer.muted = mutes[notif.author.did] ?? false - } - return { - encoding: 'application/json', - body: { - cursor, - notifications: filtered, - }, - } - }, - }) - - server.app.bsky.feed.getFeed({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - // @TODO cache the feedgen did lookup - const { data: feed } = await agent.api.app.bsky.feed.getFeedGenerator( - { feed: params.feed }, - await headers(requester), - ) - const res = await agent.api.app.bsky.feed.getFeed( - params, - await headers(requester, feed.view.did), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - return server -} - -const didsForFeedViewPosts = (feed: FeedViewPost[]): string[] => { - const dids: string[] = [] - for (const item of feed) { - dids.push(item.post.author.did) - if (item.reply) { - if (isPostView(item.reply.parent)) { - dids.push(item.reply.parent.author.did) - } - if (isPostView(item.reply.root)) { - dids.push(item.reply.root.author.did) - } - } - if (item.reason && isReasonRepost(item.reason)) { - dids.push(item.reason.by.did) - } - } - return dedupeStrs(dids) -} - -const processFeedViewPostMutes = ( - feed: FeedViewPost[], - mutes: Record, - shouldRemove: (post: FeedViewPost) => boolean, -): FeedViewPost[] => { - const hydrated: FeedViewPost[] = [] - for (const item of feed) { - if (shouldRemove(item)) continue - item.post.author.viewer ??= {} - item.post.author.viewer.muted = false - if (item.reply) { - if (isPostView(item.reply.parent)) { - item.reply.parent.author.viewer ??= {} - item.reply.parent.author.viewer.muted = - mutes[item.reply.parent.author.did] ?? false - } - if (isPostView(item.reply.root)) { - item.reply.root.author.viewer ??= {} - item.reply.root.author.viewer.muted = - mutes[item.reply.root.author.did] ?? false - } - } - if (item.reason && isReasonRepost(item.reason)) { - item.reason.by.viewer ??= {} - item.reason.by.viewer.muted = mutes[item.reason.by.did] ?? false - } - hydrated.push(item) - } - return hydrated -} - -const didsForThread = (thread: ThreadViewPost): string[] => { - return dedupeStrs(didsForThreadRecurse(thread)) -} - -const didsForThreadRecurse = (thread: ThreadViewPost): string[] => { - let forParent: string[] = [] - let forReplies: string[] = [] - if (isThreadViewPost(thread.parent)) { - forParent = didsForThread(thread.parent) - } - if (thread.replies) { - forReplies = thread.replies - .map((reply) => (isThreadViewPost(reply) ? didsForThread(reply) : [])) - .flat() - } - return [thread.post.author.did, ...forParent, ...forReplies] -} - -const processThreadViewMutes = ( - thread: ThreadViewPost, - mutes: Record, -): ThreadViewPost => { - thread.post.author.viewer ??= {} - thread.post.author.viewer.muted = mutes[thread.post.author.did] ?? false - if (isThreadViewPost(thread.parent)) { - processThreadViewMutes(thread.parent, mutes) - } - if (thread.replies) { - for (const reply of thread.replies) { - if (isThreadViewPost(reply)) { - processThreadViewMutes(reply, mutes) - } - } - } - return thread -}