From 578757cb44165457c4e6da06e895638773224e39 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 14 Sep 2023 11:19:04 -0500 Subject: [PATCH 01/46] List feeds (#1557) * lexicons for block lists * reorg blockset functionality into graph service, impl block/mute filtering * apply filterBlocksAndMutes() throughout appview except feeds * update local feeds to pass through cleanFeedSkeleton(), offload block/mute application * impl for grabbing block/mute details by did pair * refactor getActorInfos away, use actor service * experiment with moving getFeedGenerators over to a pipeline * move getPostThread over to a pipeline * move feeds over to pipelines * move suggestions and likes over to pipelines * move reposted-by, follows, followers over to pipelines, tidy author feed and post thread * remove old block/mute checks * unify post presentation logic * move profiles endpoints over to pipelines * tidy * tidy * misc fixes * unify some profile hydration/presentation in appview * profile detail, split hydration and presentation, misc fixes * unify feed hydration w/ profile hydration * unify hydration step for embeds, tidy application of labels * setup indexing of list-blocks in bsky appview * apply list-blocks, impl getListBlocks, tidy getList, tests * tidy * update pds proxy snaps * update pds proxy snaps * fix snap * make algos return feed items, save work in getFeed * misc changes, tidy * tidy * fix aturi import * lex * list purpose * lex gen * add route * add proxy route * seed client helpers * tests * mutes and blocks * proxy test * snapshot * hoist actors out of composeThread() * tidy * tidy * run ci on all prs * format * format * fix snap name * fix snapsh --------- Co-authored-by: Devin Ivy --- lexicons/app/bsky/feed/getListFeed.json | 42 + lexicons/app/bsky/graph/defs.json | 9 +- packages/api/src/client/index.ts | 14 + packages/api/src/client/lexicons.ts | 64 +- .../client/types/app/bsky/feed/getListFeed.ts | 46 ++ .../src/client/types/app/bsky/graph/defs.ts | 7 +- .../bsky/src/api/app/bsky/feed/getListFeed.ts | 129 +++ packages/bsky/src/api/index.ts | 2 + packages/bsky/src/lexicon/index.ts | 13 + packages/bsky/src/lexicon/lexicons.ts | 64 +- .../types/app/bsky/feed/getListFeed.ts | 50 ++ .../src/lexicon/types/app/bsky/graph/defs.ts | 7 +- packages/bsky/tests/seeds/client.ts | 54 ++ .../__snapshots__/list-feed.test.ts.snap | 721 ++++++++++++++++ packages/bsky/tests/views/blocks.test.ts | 23 + packages/bsky/tests/views/list-feed.test.ts | 194 +++++ packages/bsky/tests/views/mute-lists.test.ts | 14 + packages/bsky/tests/views/mutes.test.ts | 14 + .../app-view/api/app/bsky/feed/getListFeed.ts | 19 + .../api/app/bsky/feed/getSuggestedFeeds.ts | 2 +- .../pds/src/app-view/api/app/bsky/index.ts | 2 + packages/pds/src/lexicon/index.ts | 13 + packages/pds/src/lexicon/lexicons.ts | 64 +- .../types/app/bsky/feed/getListFeed.ts | 50 ++ .../src/lexicon/types/app/bsky/graph/defs.ts | 7 +- .../proxied/__snapshots__/views.test.ts.snap | 782 ++++++++++++++++++ packages/pds/tests/proxied/views.test.ts | 37 +- packages/pds/tests/seeds/client.ts | 53 ++ 28 files changed, 2487 insertions(+), 9 deletions(-) create mode 100644 lexicons/app/bsky/feed/getListFeed.json create mode 100644 packages/api/src/client/types/app/bsky/feed/getListFeed.ts create mode 100644 packages/bsky/src/api/app/bsky/feed/getListFeed.ts create mode 100644 packages/bsky/src/lexicon/types/app/bsky/feed/getListFeed.ts create mode 100644 packages/bsky/tests/views/__snapshots__/list-feed.test.ts.snap create mode 100644 packages/bsky/tests/views/list-feed.test.ts create mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getListFeed.ts create mode 100644 packages/pds/src/lexicon/types/app/bsky/feed/getListFeed.ts diff --git a/lexicons/app/bsky/feed/getListFeed.json b/lexicons/app/bsky/feed/getListFeed.json new file mode 100644 index 00000000000..f7b778bda28 --- /dev/null +++ b/lexicons/app/bsky/feed/getListFeed.json @@ -0,0 +1,42 @@ +{ + "lexicon": 1, + "id": "app.bsky.feed.getListFeed", + "defs": { + "main": { + "type": "query", + "description": "A view of a recent posts from actors in a list", + "parameters": { + "type": "params", + "required": ["list"], + "properties": { + "list": { "type": "string", "format": "at-uri" }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["feed"], + "properties": { + "cursor": { "type": "string" }, + "feed": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.feed.defs#feedViewPost" + } + } + } + } + }, + "errors": [{ "name": "UnknownList" }] + } + } +} diff --git a/lexicons/app/bsky/graph/defs.json b/lexicons/app/bsky/graph/defs.json index 44cf55875b4..894cfadc0ef 100644 --- a/lexicons/app/bsky/graph/defs.json +++ b/lexicons/app/bsky/graph/defs.json @@ -47,12 +47,19 @@ }, "listPurpose": { "type": "string", - "knownValues": ["app.bsky.graph.defs#modlist"] + "knownValues": [ + "app.bsky.graph.defs#modlist", + "app.bsky.graph.defs#curatelist" + ] }, "modlist": { "type": "token", "description": "A list of actors to apply an aggregate moderation action (mute/block) on" }, + "curatelist": { + "type": "token", + "description": "A list of actors used for curation purposes such as list feeds or interaction gating" + }, "listViewerState": { "type": "object", "properties": { diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index 761097aad7c..8a32bef870a 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -94,6 +94,7 @@ import * as AppBskyFeedGetFeedGenerator from './types/app/bsky/feed/getFeedGener import * as AppBskyFeedGetFeedGenerators from './types/app/bsky/feed/getFeedGenerators' import * as AppBskyFeedGetFeedSkeleton from './types/app/bsky/feed/getFeedSkeleton' import * as AppBskyFeedGetLikes from './types/app/bsky/feed/getLikes' +import * as AppBskyFeedGetListFeed from './types/app/bsky/feed/getListFeed' import * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread' import * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts' import * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy' @@ -218,6 +219,7 @@ export * as AppBskyFeedGetFeedGenerator from './types/app/bsky/feed/getFeedGener export * as AppBskyFeedGetFeedGenerators from './types/app/bsky/feed/getFeedGenerators' export * as AppBskyFeedGetFeedSkeleton from './types/app/bsky/feed/getFeedSkeleton' export * as AppBskyFeedGetLikes from './types/app/bsky/feed/getLikes' +export * as AppBskyFeedGetListFeed from './types/app/bsky/feed/getListFeed' export * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread' export * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts' export * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy' @@ -271,6 +273,7 @@ export const COM_ATPROTO_MODERATION = { } export const APP_BSKY_GRAPH = { DefsModlist: 'app.bsky.graph.defs#modlist', + DefsCuratelist: 'app.bsky.graph.defs#curatelist', } export class AtpBaseClient { @@ -1309,6 +1312,17 @@ export class FeedNS { }) } + getListFeed( + params?: AppBskyFeedGetListFeed.QueryParams, + opts?: AppBskyFeedGetListFeed.CallOptions, + ): Promise { + return this._service.xrpc + .call('app.bsky.feed.getListFeed', params, undefined, opts) + .catch((e) => { + throw AppBskyFeedGetListFeed.toKnownErr(e) + }) + } + getPostThread( params?: AppBskyFeedGetPostThread.QueryParams, opts?: AppBskyFeedGetPostThread.CallOptions, diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index f3c93c5e805..f1fd2519d74 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -5160,6 +5160,59 @@ export const schemaDict = { }, }, }, + AppBskyFeedGetListFeed: { + lexicon: 1, + id: 'app.bsky.feed.getListFeed', + defs: { + main: { + type: 'query', + description: 'A view of a recent posts from actors in a list', + parameters: { + type: 'params', + required: ['list'], + properties: { + list: { + type: 'string', + format: 'at-uri', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['feed'], + properties: { + cursor: { + type: 'string', + }, + feed: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#feedViewPost', + }, + }, + }, + }, + }, + errors: [ + { + name: 'UnknownList', + }, + ], + }, + }, + }, AppBskyFeedGetPostThread: { lexicon: 1, id: 'app.bsky.feed.getPostThread', @@ -5687,13 +5740,21 @@ export const schemaDict = { }, listPurpose: { type: 'string', - knownValues: ['app.bsky.graph.defs#modlist'], + knownValues: [ + 'app.bsky.graph.defs#modlist', + 'app.bsky.graph.defs#curatelist', + ], }, modlist: { type: 'token', description: 'A list of actors to apply an aggregate moderation action (mute/block) on', }, + curatelist: { + type: 'token', + description: + 'A list of actors used for curation purposes such as list feeds or interaction gating', + }, listViewerState: { type: 'object', properties: { @@ -6862,6 +6923,7 @@ export const ids = { AppBskyFeedGetFeedGenerators: 'app.bsky.feed.getFeedGenerators', AppBskyFeedGetFeedSkeleton: 'app.bsky.feed.getFeedSkeleton', AppBskyFeedGetLikes: 'app.bsky.feed.getLikes', + AppBskyFeedGetListFeed: 'app.bsky.feed.getListFeed', AppBskyFeedGetPostThread: 'app.bsky.feed.getPostThread', AppBskyFeedGetPosts: 'app.bsky.feed.getPosts', AppBskyFeedGetRepostedBy: 'app.bsky.feed.getRepostedBy', diff --git a/packages/api/src/client/types/app/bsky/feed/getListFeed.ts b/packages/api/src/client/types/app/bsky/feed/getListFeed.ts new file mode 100644 index 00000000000..511e9526c6d --- /dev/null +++ b/packages/api/src/client/types/app/bsky/feed/getListFeed.ts @@ -0,0 +1,46 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { Headers, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as AppBskyFeedDefs from './defs' + +export interface QueryParams { + list: string + limit?: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + feed: AppBskyFeedDefs.FeedViewPost[] + [k: string]: unknown +} + +export interface CallOptions { + headers?: Headers +} + +export interface Response { + success: boolean + headers: Headers + data: OutputSchema +} + +export class UnknownListError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message, src.headers) + } +} + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + if (e.error === 'UnknownList') return new UnknownListError(e) + } + return e +} diff --git a/packages/api/src/client/types/app/bsky/graph/defs.ts b/packages/api/src/client/types/app/bsky/graph/defs.ts index 566ea2446d8..fb40758534f 100644 --- a/packages/api/src/client/types/app/bsky/graph/defs.ts +++ b/packages/api/src/client/types/app/bsky/graph/defs.ts @@ -74,10 +74,15 @@ export function validateListItemView(v: unknown): ValidationResult { return lexicons.validate('app.bsky.graph.defs#listItemView', v) } -export type ListPurpose = 'app.bsky.graph.defs#modlist' | (string & {}) +export type ListPurpose = + | 'app.bsky.graph.defs#modlist' + | 'app.bsky.graph.defs#curatelist' + | (string & {}) /** A list of actors to apply an aggregate moderation action (mute/block) on */ export const MODLIST = 'app.bsky.graph.defs#modlist' +/** A list of actors used for curation purposes such as list feeds or interaction gating */ +export const CURATELIST = 'app.bsky.graph.defs#curatelist' export interface ListViewerState { muted?: boolean diff --git a/packages/bsky/src/api/app/bsky/feed/getListFeed.ts b/packages/bsky/src/api/app/bsky/feed/getListFeed.ts new file mode 100644 index 00000000000..f166c8abb99 --- /dev/null +++ b/packages/bsky/src/api/app/bsky/feed/getListFeed.ts @@ -0,0 +1,129 @@ +import { Server } from '../../../../lexicon' +import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getListFeed' +import { FeedKeyset, getFeedDateThreshold } from '../util/feed' +import { paginate } from '../../../../db/pagination' +import AppContext from '../../../../context' +import { setRepoRev } from '../../../util' +import { Database } from '../../../../db' +import { + FeedHydrationState, + FeedRow, + FeedService, +} from '../../../../services/feed' +import { ActorService } from '../../../../services/actor' +import { GraphService } from '../../../../services/graph' +import { createPipeline } from '../../../../pipeline' + +export default function (server: Server, ctx: AppContext) { + const getListFeed = createPipeline( + skeleton, + hydration, + noBlocksOrMutes, + presentation, + ) + server.app.bsky.feed.getListFeed({ + auth: ctx.authOptionalVerifier, + handler: async ({ params, auth, res }) => { + const viewer = auth.credentials.did + const db = ctx.db.getReplica() + const actorService = ctx.services.actor(db) + const feedService = ctx.services.feed(db) + const graphService = ctx.services.graph(db) + + const [result, repoRev] = await Promise.all([ + getListFeed( + { ...params, viewer }, + { db, actorService, feedService, graphService }, + ), + actorService.getRepoRev(viewer), + ]) + + setRepoRev(res, repoRev) + + return { + encoding: 'application/json', + body: result, + } + }, + }) +} + +export const skeleton = async ( + params: Params, + ctx: Context, +): Promise => { + const { list, cursor, limit } = params + const { db } = ctx + const { ref } = db.db.dynamic + + const keyset = new FeedKeyset(ref('post.sortAt'), ref('post.cid')) + const sortFrom = keyset.unpack(cursor)?.primary + + let builder = ctx.feedService + .selectPostQb() + .innerJoin('list_item', 'list_item.subjectDid', 'post.creator') + .where('list_item.listUri', '=', list) + .where('post.sortAt', '>', getFeedDateThreshold(sortFrom, 3)) + + builder = paginate(builder, { + limit, + cursor, + keyset, + tryIndex: true, + }) + const feedItems = await builder.execute() + + return { + params, + feedItems, + cursor: keyset.packFromResult(feedItems), + } +} + +const hydration = async (state: SkeletonState, ctx: Context) => { + const { feedService } = ctx + const { params, feedItems } = state + const refs = feedService.feedItemRefs(feedItems) + const hydrated = await feedService.feedHydration({ + ...refs, + viewer: params.viewer, + }) + return { ...state, ...hydrated } +} + +const noBlocksOrMutes = (state: HydrationState) => { + const { viewer } = state.params + if (!viewer) return state + state.feedItems = state.feedItems.filter( + (item) => + !state.bam.block([viewer, item.postAuthorDid]) && + !state.bam.mute([viewer, item.postAuthorDid]), + ) + return state +} + +const presentation = (state: HydrationState, ctx: Context) => { + const { feedService } = ctx + const { feedItems, cursor, params } = state + const feed = feedService.views.formatFeed(feedItems, state, { + viewer: params.viewer, + }) + return { feed, cursor } +} + +type Context = { + db: Database + actorService: ActorService + feedService: FeedService + graphService: GraphService +} + +type Params = QueryParams & { viewer: string | null } + +type SkeletonState = { + params: Params + feedItems: FeedRow[] + cursor?: string +} + +type HydrationState = SkeletonState & FeedHydrationState diff --git a/packages/bsky/src/api/index.ts b/packages/bsky/src/api/index.ts index 1928cda01d2..3768ed4da0b 100644 --- a/packages/bsky/src/api/index.ts +++ b/packages/bsky/src/api/index.ts @@ -10,6 +10,7 @@ import getFeedGenerator from './app/bsky/feed/getFeedGenerator' import getFeedGenerators from './app/bsky/feed/getFeedGenerators' import getFeedSkeleton from './app/bsky/feed/getFeedSkeleton' import getLikes from './app/bsky/feed/getLikes' +import getListFeed from './app/bsky/feed/getListFeed' import getPostThread from './app/bsky/feed/getPostThread' import getPosts from './app/bsky/feed/getPosts' import getActorLikes from './app/bsky/feed/getActorLikes' @@ -70,6 +71,7 @@ export default function (server: Server, ctx: AppContext) { getFeedGenerators(server, ctx) getFeedSkeleton(server, ctx) getLikes(server, ctx) + getListFeed(server, ctx) getPostThread(server, ctx) getPosts(server, ctx) getActorLikes(server, ctx) diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index 93435056503..52635132470 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -83,6 +83,7 @@ import * as AppBskyFeedGetFeedGenerator from './types/app/bsky/feed/getFeedGener import * as AppBskyFeedGetFeedGenerators from './types/app/bsky/feed/getFeedGenerators' import * as AppBskyFeedGetFeedSkeleton from './types/app/bsky/feed/getFeedSkeleton' import * as AppBskyFeedGetLikes from './types/app/bsky/feed/getLikes' +import * as AppBskyFeedGetListFeed from './types/app/bsky/feed/getListFeed' import * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread' import * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts' import * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy' @@ -126,6 +127,7 @@ export const COM_ATPROTO_MODERATION = { } export const APP_BSKY_GRAPH = { DefsModlist: 'app.bsky.graph.defs#modlist', + DefsCuratelist: 'app.bsky.graph.defs#curatelist', } export function createServer(options?: XrpcOptions): Server { @@ -1101,6 +1103,17 @@ export class FeedNS { return this._server.xrpc.method(nsid, cfg) } + getListFeed( + cfg: ConfigOf< + AV, + AppBskyFeedGetListFeed.Handler>, + AppBskyFeedGetListFeed.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.feed.getListFeed' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + getPostThread( cfg: ConfigOf< AV, diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index f3c93c5e805..f1fd2519d74 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -5160,6 +5160,59 @@ export const schemaDict = { }, }, }, + AppBskyFeedGetListFeed: { + lexicon: 1, + id: 'app.bsky.feed.getListFeed', + defs: { + main: { + type: 'query', + description: 'A view of a recent posts from actors in a list', + parameters: { + type: 'params', + required: ['list'], + properties: { + list: { + type: 'string', + format: 'at-uri', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['feed'], + properties: { + cursor: { + type: 'string', + }, + feed: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#feedViewPost', + }, + }, + }, + }, + }, + errors: [ + { + name: 'UnknownList', + }, + ], + }, + }, + }, AppBskyFeedGetPostThread: { lexicon: 1, id: 'app.bsky.feed.getPostThread', @@ -5687,13 +5740,21 @@ export const schemaDict = { }, listPurpose: { type: 'string', - knownValues: ['app.bsky.graph.defs#modlist'], + knownValues: [ + 'app.bsky.graph.defs#modlist', + 'app.bsky.graph.defs#curatelist', + ], }, modlist: { type: 'token', description: 'A list of actors to apply an aggregate moderation action (mute/block) on', }, + curatelist: { + type: 'token', + description: + 'A list of actors used for curation purposes such as list feeds or interaction gating', + }, listViewerState: { type: 'object', properties: { @@ -6862,6 +6923,7 @@ export const ids = { AppBskyFeedGetFeedGenerators: 'app.bsky.feed.getFeedGenerators', AppBskyFeedGetFeedSkeleton: 'app.bsky.feed.getFeedSkeleton', AppBskyFeedGetLikes: 'app.bsky.feed.getLikes', + AppBskyFeedGetListFeed: 'app.bsky.feed.getListFeed', AppBskyFeedGetPostThread: 'app.bsky.feed.getPostThread', AppBskyFeedGetPosts: 'app.bsky.feed.getPosts', AppBskyFeedGetRepostedBy: 'app.bsky.feed.getRepostedBy', diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getListFeed.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getListFeed.ts new file mode 100644 index 00000000000..e24c3f8ed22 --- /dev/null +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getListFeed.ts @@ -0,0 +1,50 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyFeedDefs from './defs' + +export interface QueryParams { + list: string + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + feed: AppBskyFeedDefs.FeedViewPost[] + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'UnknownList' +} + +export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/graph/defs.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/defs.ts index 63c05b5faa3..121d9db200a 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/defs.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/defs.ts @@ -74,10 +74,15 @@ export function validateListItemView(v: unknown): ValidationResult { return lexicons.validate('app.bsky.graph.defs#listItemView', v) } -export type ListPurpose = 'app.bsky.graph.defs#modlist' | (string & {}) +export type ListPurpose = + | 'app.bsky.graph.defs#modlist' + | 'app.bsky.graph.defs#curatelist' + | (string & {}) /** A list of actors to apply an aggregate moderation action (mute/block) on */ export const MODLIST = 'app.bsky.graph.defs#modlist' +/** A list of actors used for curation purposes such as list feeds or interaction gating */ +export const CURATELIST = 'app.bsky.graph.defs#curatelist' export interface ListViewerState { muted?: boolean diff --git a/packages/bsky/tests/seeds/client.ts b/packages/bsky/tests/seeds/client.ts index ddd1acb9192..b0a04db8c5e 100644 --- a/packages/bsky/tests/seeds/client.ts +++ b/packages/bsky/tests/seeds/client.ts @@ -9,6 +9,7 @@ import { InputSchema as CreateReportInput } from '@atproto/api/src/client/types/ import { Record as PostRecord } from '@atproto/api/src/client/types/app/bsky/feed/post' import { Record as LikeRecord } from '@atproto/api/src/client/types/app/bsky/feed/like' import { Record as FollowRecord } from '@atproto/api/src/client/types/app/bsky/graph/follow' +import { Record as ListRecord } from '@atproto/api/src/client/types/app/bsky/graph/list' // Makes it simple to create data via the XRPC client, // and keeps track of all created data in memory for convenience. @@ -74,6 +75,10 @@ export class SeedClient { likes: Record> replies: Record reposts: Record + lists: Record< + string, + Record }> + > dids: Record constructor(public agent: AtpAgent, public adminAuth?: string) { @@ -84,6 +89,7 @@ export class SeedClient { this.likes = {} this.replies = {} this.reposts = {} + this.lists = {} this.dids = {} } @@ -319,6 +325,54 @@ export class SeedClient { return repost } + async createList(by: string, name: string, purpose: 'mod' | 'curate') { + const res = await this.agent.api.app.bsky.graph.list.create( + { repo: by }, + { + name, + purpose: + purpose === 'mod' + ? 'app.bsky.graph.defs#modlist' + : 'app.bsky.graph.defs#curatelist', + createdAt: new Date().toISOString(), + }, + this.getHeaders(by), + ) + this.lists[by] ??= {} + const ref = new RecordRef(res.uri, res.cid) + this.lists[by][ref.uriStr] = { + ref: ref, + items: {}, + } + return ref + } + + async addToList(by: string, subject: string, list: RecordRef) { + const res = await this.agent.api.app.bsky.graph.listitem.create( + { repo: by }, + { subject, list: list.uriStr, createdAt: new Date().toISOString() }, + this.getHeaders(by), + ) + const ref = new RecordRef(res.uri, res.cid) + const found = (this.lists[by] ?? {})[list.uriStr] + if (found) { + found.items[subject] = ref + } + return ref + } + + async rmFromList(by: string, subject: string, list: RecordRef) { + const foundList = (this.lists[by] ?? {})[list.uriStr] ?? {} + if (!foundList) return + const foundItem = foundList.items[subject] + if (!foundItem) return + await this.agent.api.app.bsky.graph.listitem.delete( + { repo: by, rkey: foundItem.uri.rkey }, + this.getHeaders(by), + ) + delete foundList.items[subject] + } + async takeModerationAction(opts: { action: TakeActionInput['action'] subject: TakeActionInput['subject'] diff --git a/packages/bsky/tests/views/__snapshots__/list-feed.test.ts.snap b/packages/bsky/tests/views/__snapshots__/list-feed.test.ts.snap new file mode 100644 index 00000000000..34d5712d303 --- /dev/null +++ b/packages/bsky/tests/views/__snapshots__/list-feed.test.ts.snap @@ -0,0 +1,721 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`list feed views fetches list feed 1`] = ` +Array [ + Object { + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(2)", + "following": "record(1)", + "muted": false, + }, + }, + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(4)", + "uri": "record(5)", + }, + "root": Object { + "cid": "cids(3)", + "uri": "record(4)", + }, + }, + "text": "thanks bob", + }, + "replyCount": 0, + "repostCount": 1, + "uri": "record(0)", + "viewer": Object {}, + }, + "reply": Object { + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", + "did": "user(2)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(7)", + "muted": false, + }, + }, + "cid": "cids(4)", + "embed": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(5)@jpeg", + }, + ], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(5)", + "val": "test-label", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(5)", + "val": "test-label-2", + }, + ], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(5)", + }, + "size": 4114, + }, + }, + ], + }, + "reply": Object { + "parent": Object { + "cid": "cids(3)", + "uri": "record(4)", + }, + "root": Object { + "cid": "cids(3)", + "uri": "record(4)", + }, + }, + "text": "hear that label_me label_me_2", + }, + "replyCount": 1, + "repostCount": 0, + "uri": "record(5)", + "viewer": Object {}, + }, + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(2)", + "following": "record(1)", + "muted": false, + }, + }, + "cid": "cids(3)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(4)", + "viewer": Object { + "like": "record(6)", + }, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", + "did": "user(2)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(7)", + "muted": false, + }, + }, + "cid": "cids(4)", + "embed": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(5)@jpeg", + }, + ], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(5)", + "val": "test-label", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(5)", + "val": "test-label-2", + }, + ], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(5)", + }, + "size": 4114, + }, + }, + ], + }, + "reply": Object { + "parent": Object { + "cid": "cids(3)", + "uri": "record(4)", + }, + "root": Object { + "cid": "cids(3)", + "uri": "record(4)", + }, + }, + "text": "hear that label_me label_me_2", + }, + "replyCount": 1, + "repostCount": 0, + "uri": "record(5)", + "viewer": Object {}, + }, + "reply": Object { + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(2)", + "following": "record(1)", + "muted": false, + }, + }, + "cid": "cids(3)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(4)", + "viewer": Object { + "like": "record(6)", + }, + }, + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(2)", + "following": "record(1)", + "muted": false, + }, + }, + "cid": "cids(3)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(4)", + "viewer": Object { + "like": "record(6)", + }, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(2)", + "following": "record(1)", + "muted": false, + }, + }, + "cid": "cids(6)", + "embed": Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "did": "user(4)", + "handle": "dan.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(7)", + "embeds": Array [ + Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "did": "user(5)", + "handle": "carol.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(8)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "uri": "record(10)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.recordWithMedia", + "media": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(5)", + }, + "size": 4114, + }, + }, + Object { + "alt": "tests/image/fixtures/key-alt.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(9)", + }, + "size": 12736, + }, + }, + ], + }, + "record": Object { + "record": Object { + "cid": "cids(10)", + "uri": "record(11)", + }, + }, + }, + "text": "hi im carol", + }, + }, + }, + ], + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "uri": "record(9)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(8)", + "uri": "record(10)", + }, + }, + "facets": Array [ + Object { + "features": Array [ + Object { + "$type": "app.bsky.richtext.facet#mention", + "did": "user(0)", + }, + ], + "index": Object { + "byteEnd": 18, + "byteStart": 0, + }, + }, + ], + "text": "@alice.bluesky.xyz is the best", + }, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(6)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(8)", + "val": "test-label", + }, + ], + "likeCount": 2, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(7)", + "uri": "record(9)", + }, + }, + "text": "yoohoo label_me", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(8)", + "viewer": Object { + "like": "record(12)", + }, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", + "did": "user(2)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(7)", + "muted": false, + }, + }, + "cid": "cids(11)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000+00:00", + "text": "bobby boy here", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(13)", + "viewer": Object {}, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(2)", + "following": "record(1)", + "muted": false, + }, + }, + "cid": "cids(3)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(4)", + "viewer": Object { + "like": "record(6)", + }, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", + "did": "user(2)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(7)", + "muted": false, + }, + }, + "cid": "cids(10)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "langs": Array [ + "en-US", + "i-klingon", + ], + "text": "bob back at it again!", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(11)", + "viewer": Object {}, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(2)", + "following": "record(1)", + "muted": false, + }, + }, + "cid": "cids(12)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(12)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(14)", + "val": "self-label", + }, + ], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, + "text": "hey there", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(14)", + "viewer": Object {}, + }, + }, +] +`; diff --git a/packages/bsky/tests/views/blocks.test.ts b/packages/bsky/tests/views/blocks.test.ts index 127aac4fc6b..30b46e78ab5 100644 --- a/packages/bsky/tests/views/blocks.test.ts +++ b/packages/bsky/tests/views/blocks.test.ts @@ -163,6 +163,29 @@ describe('pds views with blocking', () => { ).toBeFalsy() }) + it('strips blocked users out of getListFeed', async () => { + const listRef = await sc.createList(alice, 'test list', 'curate') + await sc.addToList(alice, alice, listRef) + await sc.addToList(alice, carol, listRef) + await sc.addToList(alice, dan, listRef) + + const resCarol = await agent.api.app.bsky.feed.getListFeed( + { list: listRef.uriStr, limit: 100 }, + { headers: await network.serviceHeaders(carol) }, + ) + expect( + resCarol.data.feed.some((post) => post.post.author.did === dan), + ).toBeFalsy() + + const resDan = await agent.api.app.bsky.feed.getListFeed( + { list: listRef.uriStr, limit: 100 }, + { headers: await network.serviceHeaders(dan) }, + ) + expect( + resDan.data.feed.some((post) => post.post.author.did === carol), + ).toBeFalsy() + }) + it('returns block status on getProfile', async () => { const resCarol = await agent.api.app.bsky.actor.getProfile( { actor: dan }, diff --git a/packages/bsky/tests/views/list-feed.test.ts b/packages/bsky/tests/views/list-feed.test.ts new file mode 100644 index 00000000000..c9d94f1a9d4 --- /dev/null +++ b/packages/bsky/tests/views/list-feed.test.ts @@ -0,0 +1,194 @@ +import AtpAgent from '@atproto/api' +import { TestNetwork } from '@atproto/dev-env' +import { forSnapshot, paginateAll, stripViewerFromPost } from '../_util' +import { RecordRef, SeedClient } from '../seeds/client' +import basicSeed from '../seeds/basic' +import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' + +describe('list feed views', () => { + let network: TestNetwork + let agent: AtpAgent + let sc: SeedClient + + // account dids, for convenience + let alice: string + let bob: string + let carol: string + + let listRef: RecordRef + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'bsky_views_list_feed', + }) + agent = network.bsky.getClient() + const pdsAgent = network.pds.getClient() + sc = new SeedClient(pdsAgent) + await basicSeed(sc) + alice = sc.dids.alice + bob = sc.dids.bob + carol = sc.dids.carol + listRef = await sc.createList(alice, 'test list', 'curate') + await sc.addToList(alice, alice, listRef) + await sc.addToList(alice, bob, listRef) + await network.processAll() + }) + + afterAll(async () => { + await network.close() + }) + + it('fetches list feed', async () => { + const res = await agent.api.app.bsky.feed.getListFeed( + { list: listRef.uriStr }, + { headers: await network.serviceHeaders(carol) }, + ) + expect(forSnapshot(res.data.feed)).toMatchSnapshot() + + // all posts are from alice or bob + expect( + res.data.feed.every((row) => [alice, bob].includes(row.post.author.did)), + ).toBeTruthy() + }) + + it('paginates', async () => { + const results = (results) => results.flatMap((res) => res.feed) + const paginator = async (cursor?: string) => { + const res = await agent.api.app.bsky.feed.getListFeed( + { + list: listRef.uriStr, + cursor, + limit: 2, + }, + { headers: await network.serviceHeaders(carol) }, + ) + return res.data + } + + const paginatedAll = await paginateAll(paginator) + paginatedAll.forEach((res) => + expect(res.feed.length).toBeLessThanOrEqual(2), + ) + + const full = await agent.api.app.bsky.feed.getListFeed( + { list: listRef.uriStr }, + { headers: await network.serviceHeaders(carol) }, + ) + + expect(full.data.feed.length).toEqual(7) + expect(results(paginatedAll)).toEqual(results([full.data])) + }) + + it('fetches results unauthed', async () => { + const { data: authed } = await agent.api.app.bsky.feed.getListFeed( + { list: listRef.uriStr }, + { headers: await network.serviceHeaders(alice) }, + ) + const { data: unauthed } = await agent.api.app.bsky.feed.getListFeed({ + list: listRef.uriStr, + }) + expect(unauthed.feed.length).toBeGreaterThan(0) + expect(unauthed.feed).toEqual( + authed.feed.map((item) => { + const result = { + ...item, + post: stripViewerFromPost(item.post), + } + if (item.reply) { + result.reply = { + parent: stripViewerFromPost(item.reply.parent), + root: stripViewerFromPost(item.reply.root), + } + } + return result + }), + ) + }) + + it('works for empty lists', async () => { + const emptyList = await sc.createList(alice, 'empty list', 'curate') + const res = await agent.api.app.bsky.feed.getListFeed({ + list: emptyList.uriStr, + }) + + expect(res.data.feed.length).toEqual(0) + }) + + it('blocks posts by actor takedown', async () => { + const actionRes = await agent.api.com.atproto.admin.takeModerationAction( + { + action: TAKEDOWN, + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: bob, + }, + createdBy: 'did:example:admin', + reason: 'Y', + }, + { + encoding: 'application/json', + headers: network.pds.adminAuthHeaders(), + }, + ) + + const res = await agent.api.app.bsky.feed.getListFeed({ + list: listRef.uriStr, + }) + const hasBob = res.data.feed.some((item) => item.post.author.did === bob) + expect(hasBob).toBe(false) + + // Cleanup + await agent.api.com.atproto.admin.reverseModerationAction( + { + id: actionRes.data.id, + createdBy: 'did:example:admin', + reason: 'Y', + }, + { + encoding: 'application/json', + headers: network.pds.adminAuthHeaders(), + }, + ) + }) + + it('blocks posts by record takedown.', async () => { + const postRef = sc.replies[bob][0].ref // Post and reply parent + const actionRes = await agent.api.com.atproto.admin.takeModerationAction( + { + action: TAKEDOWN, + subject: { + $type: 'com.atproto.repo.strongRef', + uri: postRef.uriStr, + cid: postRef.cidStr, + }, + createdBy: 'did:example:admin', + reason: 'Y', + }, + { + encoding: 'application/json', + headers: network.pds.adminAuthHeaders(), + }, + ) + + const res = await agent.api.app.bsky.feed.getListFeed({ + list: listRef.uriStr, + }) + const hasPost = res.data.feed.some( + (item) => item.post.uri === postRef.uriStr, + ) + expect(hasPost).toBe(false) + + // Cleanup + await agent.api.com.atproto.admin.reverseModerationAction( + { + id: actionRes.data.id, + createdBy: 'did:example:admin', + reason: 'Y', + }, + { + encoding: 'application/json', + headers: network.pds.adminAuthHeaders(), + }, + ) + }) +}) diff --git a/packages/bsky/tests/views/mute-lists.test.ts b/packages/bsky/tests/views/mute-lists.test.ts index fd2d02ed81f..a1800ad1143 100644 --- a/packages/bsky/tests/views/mute-lists.test.ts +++ b/packages/bsky/tests/views/mute-lists.test.ts @@ -127,6 +127,20 @@ describe('bsky views with mutes from mute lists', () => { ).toBe(false) }) + it('removes content from muted users on getListFeed', async () => { + const listRef = await sc.createList(bob, 'test list', 'curate') + await sc.addToList(alice, bob, listRef) + await sc.addToList(alice, carol, listRef) + await sc.addToList(alice, dan, listRef) + const res = await agent.api.app.bsky.feed.getListFeed( + { list: listRef.uriStr }, + { headers: await network.serviceHeaders(alice) }, + ) + expect( + res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)), + ).toBe(false) + }) + it('returns mute status on getProfile', async () => { const res = await agent.api.app.bsky.actor.getProfile( { actor: carol }, diff --git a/packages/bsky/tests/views/mutes.test.ts b/packages/bsky/tests/views/mutes.test.ts index de3e13313f3..15be18a7b27 100644 --- a/packages/bsky/tests/views/mutes.test.ts +++ b/packages/bsky/tests/views/mutes.test.ts @@ -88,6 +88,20 @@ describe('mute views', () => { ).toBe(false) }) + it('removes content from muted users on getListFeed', async () => { + const listRef = await sc.createList(bob, 'test list', 'curate') + await sc.addToList(alice, bob, listRef) + await sc.addToList(alice, carol, listRef) + await sc.addToList(alice, dan, listRef) + const res = await agent.api.app.bsky.feed.getListFeed( + { list: listRef.uriStr }, + { headers: await network.serviceHeaders(alice) }, + ) + expect( + res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)), + ).toBe(false) + }) + it('returns mute status on getProfile', async () => { const res = await agent.api.app.bsky.actor.getProfile( { actor: bob }, diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getListFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getListFeed.ts new file mode 100644 index 00000000000..1ac1e983861 --- /dev/null +++ b/packages/pds/src/app-view/api/app/bsky/feed/getListFeed.ts @@ -0,0 +1,19 @@ +import { Server } from '../../../../../lexicon' +import AppContext from '../../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.feed.getListFeed({ + auth: ctx.accessVerifier, + handler: async ({ auth, params }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.feed.getListFeed( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getSuggestedFeeds.ts b/packages/pds/src/app-view/api/app/bsky/feed/getSuggestedFeeds.ts index ba5fb19767b..37f06390fb9 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getSuggestedFeeds.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getSuggestedFeeds.ts @@ -4,7 +4,7 @@ import AppContext from '../../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getSuggestedFeeds({ auth: ctx.accessVerifier, - handler: async ({ req, auth, params }) => { + handler: async ({ auth, params }) => { const requester = auth.credentials.did const res = await ctx.appviewAgent.api.app.bsky.feed.getSuggestedFeeds( params, diff --git a/packages/pds/src/app-view/api/app/bsky/index.ts b/packages/pds/src/app-view/api/app/bsky/index.ts index 5cffb90653d..96b0b5de06c 100644 --- a/packages/pds/src/app-view/api/app/bsky/index.ts +++ b/packages/pds/src/app-view/api/app/bsky/index.ts @@ -9,6 +9,7 @@ import getFeedGenerators from './feed/getFeedGenerators' import describeFeedGenerator from './feed/describeFeedGenerator' import getFeed from './feed/getFeed' import getLikes from './feed/getLikes' +import getListFeed from './feed/getListFeed' import getPostThread from './feed/getPostThread' import getPosts from './feed/getPosts' import getActorLikes from './feed/getActorLikes' @@ -47,6 +48,7 @@ export default function (server: Server, ctx: AppContext) { describeFeedGenerator(server, ctx) getFeed(server, ctx) getLikes(server, ctx) + getListFeed(server, ctx) getPostThread(server, ctx) getPosts(server, ctx) getActorLikes(server, ctx) diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 93435056503..52635132470 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -83,6 +83,7 @@ import * as AppBskyFeedGetFeedGenerator from './types/app/bsky/feed/getFeedGener import * as AppBskyFeedGetFeedGenerators from './types/app/bsky/feed/getFeedGenerators' import * as AppBskyFeedGetFeedSkeleton from './types/app/bsky/feed/getFeedSkeleton' import * as AppBskyFeedGetLikes from './types/app/bsky/feed/getLikes' +import * as AppBskyFeedGetListFeed from './types/app/bsky/feed/getListFeed' import * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread' import * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts' import * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy' @@ -126,6 +127,7 @@ export const COM_ATPROTO_MODERATION = { } export const APP_BSKY_GRAPH = { DefsModlist: 'app.bsky.graph.defs#modlist', + DefsCuratelist: 'app.bsky.graph.defs#curatelist', } export function createServer(options?: XrpcOptions): Server { @@ -1101,6 +1103,17 @@ export class FeedNS { return this._server.xrpc.method(nsid, cfg) } + getListFeed( + cfg: ConfigOf< + AV, + AppBskyFeedGetListFeed.Handler>, + AppBskyFeedGetListFeed.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.feed.getListFeed' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + getPostThread( cfg: ConfigOf< AV, diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index f3c93c5e805..f1fd2519d74 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -5160,6 +5160,59 @@ export const schemaDict = { }, }, }, + AppBskyFeedGetListFeed: { + lexicon: 1, + id: 'app.bsky.feed.getListFeed', + defs: { + main: { + type: 'query', + description: 'A view of a recent posts from actors in a list', + parameters: { + type: 'params', + required: ['list'], + properties: { + list: { + type: 'string', + format: 'at-uri', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['feed'], + properties: { + cursor: { + type: 'string', + }, + feed: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#feedViewPost', + }, + }, + }, + }, + }, + errors: [ + { + name: 'UnknownList', + }, + ], + }, + }, + }, AppBskyFeedGetPostThread: { lexicon: 1, id: 'app.bsky.feed.getPostThread', @@ -5687,13 +5740,21 @@ export const schemaDict = { }, listPurpose: { type: 'string', - knownValues: ['app.bsky.graph.defs#modlist'], + knownValues: [ + 'app.bsky.graph.defs#modlist', + 'app.bsky.graph.defs#curatelist', + ], }, modlist: { type: 'token', description: 'A list of actors to apply an aggregate moderation action (mute/block) on', }, + curatelist: { + type: 'token', + description: + 'A list of actors used for curation purposes such as list feeds or interaction gating', + }, listViewerState: { type: 'object', properties: { @@ -6862,6 +6923,7 @@ export const ids = { AppBskyFeedGetFeedGenerators: 'app.bsky.feed.getFeedGenerators', AppBskyFeedGetFeedSkeleton: 'app.bsky.feed.getFeedSkeleton', AppBskyFeedGetLikes: 'app.bsky.feed.getLikes', + AppBskyFeedGetListFeed: 'app.bsky.feed.getListFeed', AppBskyFeedGetPostThread: 'app.bsky.feed.getPostThread', AppBskyFeedGetPosts: 'app.bsky.feed.getPosts', AppBskyFeedGetRepostedBy: 'app.bsky.feed.getRepostedBy', diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/getListFeed.ts b/packages/pds/src/lexicon/types/app/bsky/feed/getListFeed.ts new file mode 100644 index 00000000000..e24c3f8ed22 --- /dev/null +++ b/packages/pds/src/lexicon/types/app/bsky/feed/getListFeed.ts @@ -0,0 +1,50 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyFeedDefs from './defs' + +export interface QueryParams { + list: string + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + feed: AppBskyFeedDefs.FeedViewPost[] + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'UnknownList' +} + +export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/graph/defs.ts b/packages/pds/src/lexicon/types/app/bsky/graph/defs.ts index 63c05b5faa3..121d9db200a 100644 --- a/packages/pds/src/lexicon/types/app/bsky/graph/defs.ts +++ b/packages/pds/src/lexicon/types/app/bsky/graph/defs.ts @@ -74,10 +74,15 @@ export function validateListItemView(v: unknown): ValidationResult { return lexicons.validate('app.bsky.graph.defs#listItemView', v) } -export type ListPurpose = 'app.bsky.graph.defs#modlist' | (string & {}) +export type ListPurpose = + | 'app.bsky.graph.defs#modlist' + | 'app.bsky.graph.defs#curatelist' + | (string & {}) /** A list of actors to apply an aggregate moderation action (mute/block) on */ export const MODLIST = 'app.bsky.graph.defs#modlist' +/** A list of actors used for curation purposes such as list feeds or interaction gating */ +export const CURATELIST = 'app.bsky.graph.defs#curatelist' export interface ListViewerState { muted?: boolean diff --git a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap index 1e7b75c9cc8..d6707292de4 100644 --- a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap @@ -774,6 +774,788 @@ Object { } `; +exports[`proxies view requests feed.getListFeed 1`] = ` +Object { + "cursor": "0000000000000::bafycid", + "feed": Array [ + Object { + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(4)", + "uri": "record(3)", + }, + "root": Object { + "cid": "cids(3)", + "uri": "record(2)", + }, + }, + "text": "thanks bob", + }, + "replyCount": 0, + "repostCount": 1, + "uri": "record(0)", + "viewer": Object {}, + }, + "reply": Object { + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", + "did": "user(2)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(5)", + "following": "record(4)", + "muted": false, + }, + }, + "cid": "cids(4)", + "embed": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(6)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(6)@jpeg", + }, + ], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(3)", + "val": "test-label", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(3)", + "val": "test-label-2", + }, + ], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(6)", + }, + "size": 4114, + }, + }, + ], + }, + "reply": Object { + "parent": Object { + "cid": "cids(3)", + "uri": "record(2)", + }, + "root": Object { + "cid": "cids(3)", + "uri": "record(2)", + }, + }, + "text": "hear that label_me label_me_2", + }, + "replyCount": 1, + "repostCount": 0, + "uri": "record(3)", + "viewer": Object {}, + }, + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(3)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(2)", + "viewer": Object {}, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", + "did": "user(2)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(5)", + "following": "record(4)", + "muted": false, + }, + }, + "cid": "cids(4)", + "embed": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(6)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(6)@jpeg", + }, + ], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(3)", + "val": "test-label", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(3)", + "val": "test-label-2", + }, + ], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(6)", + }, + "size": 4114, + }, + }, + ], + }, + "reply": Object { + "parent": Object { + "cid": "cids(3)", + "uri": "record(2)", + }, + "root": Object { + "cid": "cids(3)", + "uri": "record(2)", + }, + }, + "text": "hear that label_me label_me_2", + }, + "replyCount": 1, + "repostCount": 0, + "uri": "record(3)", + "viewer": Object {}, + }, + "reply": Object { + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(3)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(2)", + "viewer": Object {}, + }, + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(3)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(2)", + "viewer": Object {}, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(7)", + "embed": Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "did": "user(4)", + "handle": "dan.test", + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(4)", + "val": "repo-action-label", + }, + ], + "viewer": Object { + "blockedBy": false, + "following": "record(9)", + "muted": false, + }, + }, + "cid": "cids(8)", + "embeds": Array [ + Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "did": "user(5)", + "handle": "carol.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(12)", + "following": "record(11)", + "muted": false, + }, + }, + "cid": "cids(9)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "uri": "record(10)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.recordWithMedia", + "media": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(6)", + }, + "size": 4114, + }, + }, + Object { + "alt": "tests/image/fixtures/key-alt.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(10)", + }, + "size": 12736, + }, + }, + ], + }, + "record": Object { + "record": Object { + "cid": "cids(11)", + "uri": "record(13)", + }, + }, + }, + "text": "hi im carol", + }, + }, + }, + ], + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "uri": "record(8)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(9)", + "uri": "record(10)", + }, + }, + "facets": Array [ + Object { + "features": Array [ + Object { + "$type": "app.bsky.richtext.facet#mention", + "did": "user(0)", + }, + ], + "index": Object { + "byteEnd": 18, + "byteStart": 0, + }, + }, + ], + "text": "@alice.bluesky.xyz is the best", + }, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(7)", + "val": "test-label", + }, + ], + "likeCount": 2, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(8)", + "uri": "record(8)", + }, + }, + "text": "yoohoo label_me", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(7)", + "viewer": Object {}, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", + "did": "user(2)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(5)", + "following": "record(4)", + "muted": false, + }, + }, + "cid": "cids(12)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000+00:00", + "text": "bobby boy here", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(14)", + "viewer": Object {}, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(3)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(2)", + "viewer": Object {}, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", + "did": "user(2)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(5)", + "following": "record(4)", + "muted": false, + }, + }, + "cid": "cids(11)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "langs": Array [ + "en-US", + "i-klingon", + ], + "text": "bob back at it again!", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(13)", + "viewer": Object {}, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(13)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(13)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(15)", + "val": "self-label", + }, + ], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, + "text": "hey there", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(15)", + "viewer": Object {}, + }, + }, + ], +} +`; + exports[`proxies view requests feed.getPosts 1`] = ` Object { "posts": Array [ diff --git a/packages/pds/tests/proxied/views.test.ts b/packages/pds/tests/proxied/views.test.ts index 84079af1c44..7b81ee8bec3 100644 --- a/packages/pds/tests/proxied/views.test.ts +++ b/packages/pds/tests/proxied/views.test.ts @@ -21,11 +21,14 @@ describe('proxies view requests', () => { agent = network.pds.getClient() sc = new SeedClient(agent) await basicSeed(sc) - await network.processAll() alice = sc.dids.alice bob = sc.dids.bob carol = sc.dids.carol dan = sc.dids.dan + const listRef = await sc.createList(alice, 'test list', 'curate') + await sc.addToList(alice, alice, listRef) + await sc.addToList(alice, bob, listRef) + await network.processAll() }) afterAll(async () => { @@ -179,6 +182,38 @@ describe('proxies view requests', () => { expect([...pt1.data.feed, ...pt2.data.feed]).toEqual(res.data.feed) }) + it('feed.getListFeed', async () => { + const list = Object.values(sc.lists[alice])[0].ref.uriStr + const res = await agent.api.app.bsky.feed.getListFeed( + { + list, + }, + { + headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' }, + }, + ) + expect(forSnapshot(res.data)).toMatchSnapshot() + const pt1 = await agent.api.app.bsky.feed.getListFeed( + { + list, + limit: 1, + }, + { + headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' }, + }, + ) + const pt2 = await agent.api.app.bsky.feed.getListFeed( + { + list, + cursor: pt1.data.cursor, + }, + { + headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' }, + }, + ) + expect([...pt1.data.feed, ...pt2.data.feed]).toEqual(res.data.feed) + }) + it('feed.getLikes', async () => { const postUri = sc.posts[carol][0].ref.uriStr const res = await agent.api.app.bsky.feed.getLikes( diff --git a/packages/pds/tests/seeds/client.ts b/packages/pds/tests/seeds/client.ts index 2918de9c7dd..c52f499f6ab 100644 --- a/packages/pds/tests/seeds/client.ts +++ b/packages/pds/tests/seeds/client.ts @@ -77,6 +77,10 @@ export class SeedClient { likes: Record> replies: Record reposts: Record + lists: Record< + string, + Record }> + > dids: Record constructor(public agent: AtpAgent) { @@ -88,6 +92,7 @@ export class SeedClient { this.likes = {} this.replies = {} this.reposts = {} + this.lists = {} this.dids = {} } @@ -369,6 +374,54 @@ export class SeedClient { return repost } + async createList(by: string, name: string, purpose: 'mod' | 'curate') { + const res = await this.agent.api.app.bsky.graph.list.create( + { repo: by }, + { + name, + purpose: + purpose === 'mod' + ? 'app.bsky.graph.defs#modlist' + : 'app.bsky.graph.defs#curatelist', + createdAt: new Date().toISOString(), + }, + this.getHeaders(by), + ) + this.lists[by] ??= {} + const ref = new RecordRef(res.uri, res.cid) + this.lists[by][ref.uriStr] = { + ref: ref, + items: {}, + } + return ref + } + + async addToList(by: string, subject: string, list: RecordRef) { + const res = await this.agent.api.app.bsky.graph.listitem.create( + { repo: by }, + { subject, list: list.uriStr, createdAt: new Date().toISOString() }, + this.getHeaders(by), + ) + const ref = new RecordRef(res.uri, res.cid) + const found = (this.lists[by] ?? {})[list.uriStr] + if (found) { + found.items[subject] = ref + } + return ref + } + + async rmFromList(by: string, subject: string, list: RecordRef) { + const foundList = (this.lists[by] ?? {})[list.uriStr] ?? {} + if (!foundList) return + const foundItem = foundList.items[subject] + if (!foundItem) return + await this.agent.api.app.bsky.graph.listitem.delete( + { repo: by, rkey: foundItem.uri.rkey }, + this.getHeaders(by), + ) + delete foundList.items[subject] + } + async takeModerationAction(opts: { action: TakeActionInput['action'] subject: TakeActionInput['subject'] From 1c42157c593f8915dfb4bebfe37e3dddb956de31 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 14 Sep 2023 13:54:47 -0500 Subject: [PATCH 02/46] Improve xrpc server error handling (#1597) improve xrpc server error handling --- packages/xrpc-server/src/rate-limiter.ts | 21 ++++++++++++++------- packages/xrpc-server/src/server.ts | 23 ++++++++++++++++++----- packages/xrpc-server/src/types.ts | 4 ++-- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/packages/xrpc-server/src/rate-limiter.ts b/packages/xrpc-server/src/rate-limiter.ts index 82719101674..ab6d79d07af 100644 --- a/packages/xrpc-server/src/rate-limiter.ts +++ b/packages/xrpc-server/src/rate-limiter.ts @@ -61,7 +61,7 @@ export class RateLimiter implements RateLimiterI { async consume( ctx: XRPCReqContext, opts?: { calcKey?: CalcKeyFn; calcPoints?: CalcPointsFn }, - ): Promise { + ): Promise { if ( this.byPassSecret && ctx.req.header('x-ratelimit-bypass') === this.byPassSecret @@ -82,7 +82,7 @@ export class RateLimiter implements RateLimiterI { // yes this library rejects with a res not an error if (err instanceof RateLimiterRes) { const status = formatLimiterStatus(this.limiter, err) - throw new RateLimitExceededError(status) + return new RateLimitExceededError(status) } else { if (this.failClosed) { throw err @@ -119,12 +119,18 @@ export const formatLimiterStatus = ( export const consumeMany = async ( ctx: XRPCReqContext, fns: RateLimiterConsume[], -): Promise => { - if (fns.length === 0) return +): Promise => { + if (fns.length === 0) return null const results = await Promise.all(fns.map((fn) => fn(ctx))) const tightestLimit = getTightestLimit(results) - if (tightestLimit !== null) { + if (tightestLimit === null) { + return null + } else if (tightestLimit instanceof RateLimitExceededError) { + setResHeaders(ctx, tightestLimit.status) + return tightestLimit + } else { setResHeaders(ctx, tightestLimit) + return tightestLimit } } @@ -142,11 +148,12 @@ export const setResHeaders = ( } export const getTightestLimit = ( - resps: (RateLimiterStatus | null)[], -): RateLimiterStatus | null => { + resps: (RateLimiterStatus | RateLimitExceededError | null)[], +): RateLimiterStatus | RateLimitExceededError | null => { let lowest: RateLimiterStatus | null = null for (const resp of resps) { if (resp === null) continue + if (resp instanceof RateLimitExceededError) return resp if (lowest === null || resp.remainingPoints < lowest.remainingPoints) { lowest = resp } diff --git a/packages/xrpc-server/src/server.ts b/packages/xrpc-server/src/server.ts index 40f4baac771..5d27e3e45a8 100644 --- a/packages/xrpc-server/src/server.ts +++ b/packages/xrpc-server/src/server.ts @@ -34,6 +34,7 @@ import { RateLimiterI, RateLimiterConsume, isShared, + RateLimitExceededError, } from './types' import { decodeQueryParams, @@ -247,7 +248,10 @@ export class Server { // handle rate limits if (consumeRateLimit) { - await consumeRateLimit(reqCtx) + const result = await consumeRateLimit(reqCtx) + if (result instanceof RateLimitExceededError) { + return next(result) + } } // run the handler @@ -475,14 +479,23 @@ function createAuthMiddleware(verifier: AuthVerifier): RequestHandler { const errorMiddleware: ErrorRequestHandler = function (err, req, res, next) { const locals: RequestLocals | undefined = req[kRequestLocals] const methodSuffix = locals ? ` method ${locals.nsid}` : '' - if (err instanceof XRPCError) { - log.error(err, `error in xrpc${methodSuffix}`) - } else { + const xrpcError = XRPCError.fromError(err) + if (xrpcError instanceof InternalServerError) { + // log trace for unhandled exceptions log.error(err, `unhandled exception in xrpc${methodSuffix}`) + } else { + // do not log trace for known xrpc errors + log.error( + { + status: xrpcError.type, + message: xrpcError.message, + name: xrpcError.customErrorName, + }, + `error in xrpc${methodSuffix}`, + ) } if (res.headersSent) { return next(err) } - const xrpcError = XRPCError.fromError(err) return res.status(xrpcError.type).json(xrpcError.payload) } diff --git a/packages/xrpc-server/src/types.ts b/packages/xrpc-server/src/types.ts index 801c8baa6f2..829f8a270d2 100644 --- a/packages/xrpc-server/src/types.ts +++ b/packages/xrpc-server/src/types.ts @@ -95,7 +95,7 @@ export interface RateLimiterI { export type RateLimiterConsume = ( ctx: XRPCReqContext, opts?: { calcKey?: CalcKeyFn; calcPoints?: CalcPointsFn }, -) => Promise +) => Promise export type RateLimiterCreator = (opts: { keyPrefix: string @@ -224,7 +224,7 @@ export class ForbiddenError extends XRPCError { export class RateLimitExceededError extends XRPCError { constructor( - status: RateLimiterStatus, + public status: RateLimiterStatus, errorMessage?: string, customErrorName?: string, ) { From 36ba2953333d13a6bbfee21612e48575fb40e8ea Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 14 Sep 2023 13:55:12 -0500 Subject: [PATCH 03/46] Remove appview proxy runtime flags (#1590) * remove appview proxy runtime flags * clean up proxy tests --- packages/dev-env/src/bin-network.ts | 37 ----------------- packages/pds/package.json | 5 +-- .../api/com/atproto/identity/resolveHandle.ts | 4 +- .../pds/src/api/com/atproto/repo/getRecord.ts | 4 +- .../app-view/api/app/bsky/actor/getProfile.ts | 2 +- .../api/app/bsky/actor/getProfiles.ts | 4 +- .../api/app/bsky/actor/getSuggestions.ts | 4 +- .../api/app/bsky/actor/searchActors.ts | 4 +- .../app/bsky/actor/searchActorsTypeahead.ts | 4 +- .../api/app/bsky/feed/getActorFeeds.ts | 4 +- .../api/app/bsky/feed/getActorLikes.ts | 2 +- .../api/app/bsky/feed/getAuthorFeed.ts | 2 +- .../src/app-view/api/app/bsky/feed/getFeed.ts | 4 +- .../api/app/bsky/feed/getFeedGenerator.ts | 4 +- .../api/app/bsky/feed/getFeedGenerators.ts | 4 +- .../app-view/api/app/bsky/feed/getLikes.ts | 4 +- .../api/app/bsky/feed/getPostThread.ts | 4 +- .../app-view/api/app/bsky/feed/getPosts.ts | 4 +- .../api/app/bsky/feed/getRepostedBy.ts | 4 +- .../app-view/api/app/bsky/feed/getTimeline.ts | 4 +- .../app-view/api/app/bsky/graph/getBlocks.ts | 4 +- .../api/app/bsky/graph/getFollowers.ts | 2 +- .../app-view/api/app/bsky/graph/getFollows.ts | 2 +- .../app-view/api/app/bsky/graph/getList.ts | 4 +- .../api/app/bsky/graph/getListMutes.ts | 4 +- .../app-view/api/app/bsky/graph/getLists.ts | 4 +- .../app-view/api/app/bsky/graph/getMutes.ts | 4 +- .../app/bsky/notification/getUnreadCount.ts | 4 +- .../bsky/notification/listNotifications.ts | 4 +- .../src/app-view/api/app/bsky/unspecced.ts | 4 +- packages/pds/src/context.ts | 17 +------- packages/pds/src/runtime-flags.ts | 41 +------------------ packages/pds/tests/proxied/admin.test.ts | 26 ------------ pnpm-lock.yaml | 9 +--- 34 files changed, 57 insertions(+), 180 deletions(-) diff --git a/packages/dev-env/src/bin-network.ts b/packages/dev-env/src/bin-network.ts index c0fe110fabd..193c7ea968a 100644 --- a/packages/dev-env/src/bin-network.ts +++ b/packages/dev-env/src/bin-network.ts @@ -24,7 +24,6 @@ const run = async () => { }, plc: { port: 2582 }, }) - await enableProxy(network) await generateMockSetup(network) console.log( @@ -40,39 +39,3 @@ const run = async () => { } run() - -// @TODO remove once we remove proxy runtime flags -const enableProxy = async (network: TestNetwork) => { - const flags = [ - 'appview-proxy:app.bsky.feed.getAuthorFeed', - 'appview-proxy:app.bsky.graph.getFollowers', - 'appview-proxy:app.bsky.feed.getPosts', - 'appview-proxy:app.bsky.graph.getFollows', - 'appview-proxy:app.bsky.feed.getLikes', - 'appview-proxy:app.bsky.feed.getRepostedBy', - 'appview-proxy:app.bsky.feed.getPostThread', - 'appview-proxy:app.bsky.actor.getProfile', - 'appview-proxy:app.bsky.actor.getProfiles', - 'appview-proxy:app.bsky.feed.getTimeline', - 'appview-proxy:app.bsky.feed.getSuggestions', - 'appview-proxy:app.bsky.feed.getFeed', - 'appview-proxy:app.bsky.feed.getActorFeeds', - 'appview-proxy:app.bsky.feed.getActorLikes', - 'appview-proxy:app.bsky.feed.getFeedGenerator', - 'appview-proxy:app.bsky.feed.getFeedGenerators', - 'appview-proxy:app.bsky.feed.getBlocks', - 'appview-proxy:app.bsky.feed.getList', - 'appview-proxy:app.bsky.notification.listNotifications', - 'appview-proxy:app.bsky.feed.getLists', - 'appview-proxy:app.bsky.feed.getListMutes', - 'appview-proxy:com.atproto.repo.getRecord', - 'appview-proxy:com.atproto.identity.resolveHandle', - 'appview-proxy:app.bsky.notification.getUnreadCount', - 'appview-proxy:app.bsky.actor.searchActorsTypeahead', - 'appview-proxy:app.bsky.actor.searchActors', - ] - await network.pds.ctx.db.db - .insertInto('runtime_flag') - .values(flags.map((name) => ({ name, value: '10' }))) - .execute() -} diff --git a/packages/pds/package.json b/packages/pds/package.json index 488088942d6..8803b6409ee 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -30,10 +30,10 @@ "@atproto/api": "workspace:^", "@atproto/common": "workspace:^", "@atproto/crypto": "workspace:^", - "@atproto/syntax": "workspace:^", "@atproto/identity": "workspace:^", "@atproto/lexicon": "workspace:^", "@atproto/repo": "workspace:^", + "@atproto/syntax": "workspace:^", "@atproto/xrpc": "workspace:^", "@atproto/xrpc-server": "workspace:^", "@did-plc/lib": "^0.0.1", @@ -53,7 +53,6 @@ "iso-datestring-validator": "^2.2.2", "jsonwebtoken": "^8.5.1", "kysely": "^0.22.0", - "lru-cache": "^10.0.1", "multiformats": "^9.9.0", "nodemailer": "^6.8.0", "nodemailer-html-to-text": "^3.2.0", @@ -67,9 +66,9 @@ "zod": "^3.21.4" }, "devDependencies": { + "@atproto/api": "workspace:^", "@atproto/bsky": "workspace:^", "@atproto/dev-env": "workspace:^", - "@atproto/api": "workspace:^", "@atproto/lex-cli": "workspace:^", "@did-plc/server": "^0.0.1", "@types/cors": "^2.8.12", diff --git a/packages/pds/src/api/com/atproto/identity/resolveHandle.ts b/packages/pds/src/api/com/atproto/identity/resolveHandle.ts index a4eead11e22..494287dc096 100644 --- a/packages/pds/src/api/com/atproto/identity/resolveHandle.ts +++ b/packages/pds/src/api/com/atproto/identity/resolveHandle.ts @@ -5,7 +5,7 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { - server.com.atproto.identity.resolveHandle(async ({ params, req }) => { + server.com.atproto.identity.resolveHandle(async ({ params }) => { let handle: string try { handle = ident.normalizeAndEnsureValidHandle(params.handle) @@ -34,7 +34,7 @@ export default function (server: Server, ctx: AppContext) { // this is not someone on our server, but we help with resolving anyway - if (!did && (await ctx.canProxyRead(req))) { + if (!did && ctx.canProxyRead()) { did = await tryResolveFromAppview(ctx.appviewAgent, handle) } diff --git a/packages/pds/src/api/com/atproto/repo/getRecord.ts b/packages/pds/src/api/com/atproto/repo/getRecord.ts index 2ce83916b32..2d73244b0d9 100644 --- a/packages/pds/src/api/com/atproto/repo/getRecord.ts +++ b/packages/pds/src/api/com/atproto/repo/getRecord.ts @@ -4,7 +4,7 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { - server.com.atproto.repo.getRecord(async ({ req, params }) => { + server.com.atproto.repo.getRecord(async ({ params }) => { const { repo, collection, rkey, cid } = params const did = await ctx.services.account(ctx.db).getDidForActor(repo) @@ -26,7 +26,7 @@ export default function (server: Server, ctx: AppContext) { } } - if (await ctx.canProxyRead(req)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.com.atproto.repo.getRecord(params) return { encoding: 'application/json', 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 index bb4678c36ad..540a8cc3779 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts @@ -13,7 +13,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ req, auth, params }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.actor.getProfile( params, 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 index 966f0b9d10b..0750a6109d7 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts @@ -7,9 +7,9 @@ import { handleReadAfterWrite } from '../util/read-after-write' export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.getProfiles({ auth: ctx.accessVerifier, - handler: async ({ req, auth, params }) => { + handler: async ({ auth, params }) => { const requester = auth.credentials.did - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.actor.getProfiles( params, await ctx.serviceAuthHeaders(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 index 4d58bf4986c..f80110dacb0 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts @@ -5,9 +5,9 @@ import { Server } from '../../../../../lexicon' export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.getSuggestions({ auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { + handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.actor.getSuggestions( params, await ctx.serviceAuthHeaders(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 index 7e22848498f..0c1e07c014a 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts @@ -14,9 +14,9 @@ import { export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.searchActors({ auth: ctx.accessVerifier, - handler: async ({ req, auth, params }) => { + handler: async ({ auth, params }) => { const requester = auth.credentials.did - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.actor.searchActors( params, await ctx.serviceAuthHeaders(requester), 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 index cc35c5d2a7d..bcc80d6acc3 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts @@ -12,9 +12,9 @@ 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 }) => { + handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.actor.searchActorsTypeahead( params, 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 index 253b31886da..f6ded6d2c0e 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts @@ -6,9 +6,9 @@ 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 }) => { + handler: async ({ auth, params }) => { const requester = auth.credentials.did - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.feed.getActorFeeds( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts b/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts index 74f1581db9a..5e67e9c6dcf 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts @@ -16,7 +16,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.feed.getActorLikes( params, requester 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 index 24f511bf99c..7850092dc28 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts @@ -16,7 +16,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ req, params, auth }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.feed.getAuthorFeed( params, requester 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 index 3ef12fa48c6..5f9eb9d975c 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts @@ -24,10 +24,10 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getFeed({ auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { + handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const { data: feed } = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( { feed: params.feed }, 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 index b87a2d32e65..66e0cc8fdc5 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts @@ -10,9 +10,9 @@ import AppContext from '../../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getFeedGenerator({ auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { + handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( params, await ctx.serviceAuthHeaders(requester), 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 index ed3be795b28..3cf7f8eef11 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts @@ -4,9 +4,9 @@ import AppContext from '../../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getFeedGenerators({ auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { + handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerators( params, await ctx.serviceAuthHeaders(requester), 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 index ed2a950d866..e6658863bed 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts @@ -7,9 +7,9 @@ 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 }) => { + handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.feed.getLikes( params, await ctx.serviceAuthHeaders(requester), 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 index 5f570827635..fb5276f2452 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts @@ -49,9 +49,9 @@ export type PostThread = { export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getPostThread({ auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { + handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { try { const res = await ctx.appviewAgent.api.app.bsky.feed.getPostThread( params, 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 index c4265d9c1cd..25bc3d652e4 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts @@ -6,9 +6,9 @@ import { PostView } from '../../../../../lexicon/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 }) => { + handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.feed.getPosts( params, await ctx.serviceAuthHeaders(requester), 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 index 7b713e8a8a9..f00f9a1a2b5 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts @@ -6,9 +6,9 @@ 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 }) => { + handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.feed.getRepostedBy( params, await ctx.serviceAuthHeaders(requester), 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 index 26778c4b4a4..5b1e29168a1 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts @@ -12,14 +12,14 @@ import { LocalRecords } from '../../../../../services/local' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getTimeline({ auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { + handler: async ({ params, auth }) => { const requester = auth.credentials.did const { algorithm, limit, cursor } = params if (algorithm && algorithm !== FeedAlgorithm.ReverseChronological) { throw new InvalidRequestError(`Unsupported algorithm: ${algorithm}`) } - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.feed.getTimeline( params, await ctx.serviceAuthHeaders(requester), 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 index 100331d0226..11acf6352a5 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts @@ -6,9 +6,9 @@ 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 }) => { + handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.graph.getBlocks( params, await ctx.serviceAuthHeaders(requester), 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 index b14d11c3ac6..73a38e83df2 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts @@ -11,7 +11,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ req, params, auth }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.graph.getFollowers( params, requester 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 index 82e9acee29f..3ebd47f6210 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts @@ -11,7 +11,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ req, params, auth }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.graph.getFollows( params, requester 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 index e3a925a2101..a7a1556ec18 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getList.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getList.ts @@ -6,9 +6,9 @@ import AppContext from '../../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getList({ auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { + handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.graph.getList( params, await ctx.serviceAuthHeaders(requester), 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 index b3c7905f32a..673b2c61b38 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts @@ -5,9 +5,9 @@ import AppContext from '../../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getListMutes({ auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { + handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.graph.getListMutes( params, await ctx.serviceAuthHeaders(requester), 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 index ac27522ead7..c6653726459 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts @@ -6,9 +6,9 @@ import AppContext from '../../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getLists({ auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { + handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.graph.getLists( params, await ctx.serviceAuthHeaders(requester), 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 index d8578916811..a082666a968 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts @@ -6,9 +6,9 @@ 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 }) => { + handler: async ({ auth, params }) => { const requester = auth.credentials.did - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.graph.getMutes( params, await ctx.serviceAuthHeaders(requester), 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 index a199f42a299..262cfa06fe0 100644 --- a/packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts +++ b/packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts @@ -6,9 +6,9 @@ import AppContext from '../../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.notification.getUnreadCount({ auth: ctx.accessVerifier, - handler: async ({ req, auth, params }) => { + handler: async ({ auth, params }) => { const requester = auth.credentials.did - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.notification.getUnreadCount( params, 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 index bf688dc25ed..d3e44a90aa2 100644 --- a/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts +++ b/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts @@ -10,9 +10,9 @@ import { getSelfLabels } from '../../../../services/label' export default function (server: Server, ctx: AppContext) { server.app.bsky.notification.listNotifications({ auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { + handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const res = await ctx.appviewAgent.api.app.bsky.notification.listNotifications( params, diff --git a/packages/pds/src/app-view/api/app/bsky/unspecced.ts b/packages/pds/src/app-view/api/app/bsky/unspecced.ts index e50758dbb7a..c500f2cb6b9 100644 --- a/packages/pds/src/app-view/api/app/bsky/unspecced.ts +++ b/packages/pds/src/app-view/api/app/bsky/unspecced.ts @@ -25,9 +25,9 @@ const NSFW_LABELS = ['porn', 'sexual', 'nudity', 'underwear'] export default function (server: Server, ctx: AppContext) { server.app.bsky.unspecced.getPopular({ auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { + handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (await ctx.canProxyRead(req, requester)) { + if (ctx.canProxyRead()) { const hotClassicUri = Object.keys(ctx.algos).find((uri) => uri.endsWith('/hot-classic'), ) diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index ca57730504b..1f414845ea1 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -191,24 +191,11 @@ export class AppContext { return this.opts.appviewAgent } - async canProxyRead( - req: express.Request, - did?: string | null, - ): Promise { + canProxyRead(): boolean { if (!this.cfg.bskyAppViewProxy || !this.cfg.bskyAppViewEndpoint) { return false } - if (req.get('x-appview-proxy') !== undefined) { - return true - } - // e.g. /xrpc/a.b.c.d/ -> a.b.c.d/ -> a.b.c.d - const endpoint = req.path.replace('/xrpc/', '').replaceAll('/', '') - if (!did) { - // when no did assigned, only proxy reads if threshold is at max of 10 - const threshold = this.runtimeFlags.appviewProxy.getThreshold(endpoint) - return threshold === 10 - } - return await this.runtimeFlags.appviewProxy.shouldProxy(endpoint, did) + return true } canProxyFeedConstruction(req: express.Request): boolean { diff --git a/packages/pds/src/runtime-flags.ts b/packages/pds/src/runtime-flags.ts index 052ed4973f6..b4c3437d1f9 100644 --- a/packages/pds/src/runtime-flags.ts +++ b/packages/pds/src/runtime-flags.ts @@ -1,18 +1,13 @@ import { BailableWait, bailableWait } from '@atproto/common' -import { randomIntFromSeed } from '@atproto/crypto' -import { LRUCache } from 'lru-cache' import Database from './db' import { dbLogger as log } from './logger' -type AppviewProxyFlagName = `appview-proxy:${string}` - -export type FlagName = AppviewProxyFlagName +export type FlagName = '' export class RuntimeFlags { destroyed = false private flags = new Map() private pollWait: BailableWait | undefined = undefined - public appviewProxy = new AppviewProxyFlags(this) constructor(public db: Database) {} @@ -54,37 +49,3 @@ export class RuntimeFlags { this.poll() } } - -class AppviewProxyFlags { - private partitionCache = new LRUCache({ - max: 50000, - fetchMethod(did: string) { - return randomIntFromSeed(did, 10) - }, - }) - - constructor(private runtimeFlags: RuntimeFlags) {} - - getThreshold(endpoint: string) { - const val = this.runtimeFlags.get(`appview-proxy:${endpoint}`) || '0' - const threshold = parseInt(val, 10) - return appviewFlagIsValid(threshold) ? threshold : 0 - } - - async shouldProxy(endpoint: string, did: string) { - const threshold = this.getThreshold(endpoint) - if (threshold === 0) { - return false - } - if (threshold === 10) { - return true - } - // threshold is 0 to 10 inclusive, partitions are 0 to 9 inclusive. - const partition = await this.partitionCache.fetch(did) - return partition !== undefined && partition < threshold - } -} - -const appviewFlagIsValid = (val: number) => { - return 0 <= val && val <= 10 -} diff --git a/packages/pds/tests/proxied/admin.test.ts b/packages/pds/tests/proxied/admin.test.ts index d02e531203f..127717225a6 100644 --- a/packages/pds/tests/proxied/admin.test.ts +++ b/packages/pds/tests/proxied/admin.test.ts @@ -267,19 +267,12 @@ describe('proxies admin requests', () => { }, ) // check profile and labels - const tryGetProfilePds = agent.api.app.bsky.actor.getProfile( - { actor: sc.dids.alice }, - { headers: sc.getHeaders(sc.dids.carol) }, - ) const tryGetProfileAppview = agent.api.app.bsky.actor.getProfile( { actor: sc.dids.alice }, { headers: { ...sc.getHeaders(sc.dids.carol), 'x-appview-proxy': 'true' }, }, ) - await expect(tryGetProfilePds).rejects.toThrow( - 'Account has been taken down', - ) await expect(tryGetProfileAppview).rejects.toThrow( 'Account has been taken down', ) @@ -296,19 +289,12 @@ describe('proxies admin requests', () => { }, ) // check profile and labels - const { data: profilePds } = await agent.api.app.bsky.actor.getProfile( - { actor: sc.dids.alice }, - { headers: sc.getHeaders(sc.dids.carol) }, - ) const { data: profileAppview } = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.alice }, { headers: { ...sc.getHeaders(sc.dids.carol), 'x-appview-proxy': 'true' }, }, ) - expect(profilePds).toEqual( - expect.objectContaining({ did: sc.dids.alice, handle: 'alice.test' }), - ) expect(profileAppview).toEqual( expect.objectContaining({ did: sc.dids.alice, handle: 'alice.test' }), ) @@ -342,17 +328,12 @@ describe('proxies admin requests', () => { }, ) // check thread and labels - const tryGetPostPds = agent.api.app.bsky.feed.getPostThread( - { uri: post.ref.uriStr, depth: 0 }, - { headers: sc.getHeaders(sc.dids.carol) }, - ) const tryGetPostAppview = agent.api.app.bsky.feed.getPostThread( { uri: post.ref.uriStr, depth: 0 }, { headers: { ...sc.getHeaders(sc.dids.carol), 'x-appview-proxy': 'true' }, }, ) - await expect(tryGetPostPds).rejects.toThrow(NotFoundError) await expect(tryGetPostAppview).rejects.toThrow(NotFoundError) const labelsA = await services.appView .label(db) @@ -367,19 +348,12 @@ describe('proxies admin requests', () => { }, ) // check thread and labels - const { data: threadPds } = await agent.api.app.bsky.feed.getPostThread( - { uri: post.ref.uriStr, depth: 0 }, - { headers: sc.getHeaders(sc.dids.carol) }, - ) const { data: threadAppview } = await agent.api.app.bsky.feed.getPostThread( { uri: post.ref.uriStr, depth: 0 }, { headers: { ...sc.getHeaders(sc.dids.carol), 'x-appview-proxy': 'true' }, }, ) - expect(threadPds.thread.post).toEqual( - expect.objectContaining({ uri: post.ref.uriStr, cid: post.ref.cidStr }), - ) expect(threadAppview.thread.post).toEqual( expect.objectContaining({ uri: post.ref.uriStr, cid: post.ref.cidStr }), ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1898a37f098..fb80b8c6a35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -552,9 +552,6 @@ importers: kysely: specifier: ^0.22.0 version: 0.22.0 - lru-cache: - specifier: ^10.0.1 - version: 10.0.1 multiformats: specifier: ^9.9.0 version: 9.9.0 @@ -8998,11 +8995,6 @@ packages: resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} dev: false - /lru-cache@10.0.1: - resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==} - engines: {node: 14 || >=16.14} - dev: false - /lru-cache@4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} dependencies: @@ -10831,6 +10823,7 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + requiresBuild: true dev: false /tsutils@3.21.0(typescript@4.8.4): From 77749ab40cdcde31e7135a258274936d52b06007 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 14 Sep 2023 14:42:09 -0500 Subject: [PATCH 04/46] getPopular hotfix (#1599) dont pass all params --- packages/pds/src/app-view/api/app/bsky/unspecced.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/src/app-view/api/app/bsky/unspecced.ts b/packages/pds/src/app-view/api/app/bsky/unspecced.ts index c500f2cb6b9..9dc3f77aba4 100644 --- a/packages/pds/src/app-view/api/app/bsky/unspecced.ts +++ b/packages/pds/src/app-view/api/app/bsky/unspecced.ts @@ -43,7 +43,7 @@ export default function (server: Server, ctx: AppContext) { await ctx.serviceAuthHeaders(requester), ) const res = await ctx.appviewAgent.api.app.bsky.feed.getFeed( - { ...params, feed: hotClassicUri }, + { feed: hotClassicUri, limit: params.limit, cursor: params.cursor }, await ctx.serviceAuthHeaders(requester, feed.view.did), ) return { From 9879ca97b7aec011e1a403ea384f4ef971482ea3 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 14 Sep 2023 15:24:48 -0500 Subject: [PATCH 05/46] Interaction Gating (#1561) * lexicons for block lists * reorg blockset functionality into graph service, impl block/mute filtering * apply filterBlocksAndMutes() throughout appview except feeds * update local feeds to pass through cleanFeedSkeleton(), offload block/mute application * impl for grabbing block/mute details by did pair * refactor getActorInfos away, use actor service * experiment with moving getFeedGenerators over to a pipeline * move getPostThread over to a pipeline * move feeds over to pipelines * move suggestions and likes over to pipelines * move reposted-by, follows, followers over to pipelines, tidy author feed and post thread * remove old block/mute checks * unify post presentation logic * move profiles endpoints over to pipelines * tidy * tidy * misc fixes * unify some profile hydration/presentation in appview * profile detail, split hydration and presentation, misc fixes * unify feed hydration w/ profile hydration * unify hydration step for embeds, tidy application of labels * setup indexing of list-blocks in bsky appview * apply list-blocks, impl getListBlocks, tidy getList, tests * tidy * update pds proxy snaps * update pds proxy snaps * fix snap * make algos return feed items, save work in getFeed * misc changes, tidy * tidy * fix aturi import * initial lexicons for interaction-gating * add interactions view to post views * codegen * model bad reply/interaction check state on posts * initial impl for checking bad reply or interaction on write * omit invalid interactions from post thread * support not-found list in interaction view * hydrate can-reply state on threads * present interaction views on posts * misc fixes, update snaps * tidy/reorg * tidy * split interaction gating into separate record in lexicon * switch interaction-gating impl to use separate record type * allow checking reply gate w/ root post deletion * fix * initial gating tests * tighten gated reply views, tests * reply-gating list rule tests * allow custom post rkeys within window * hoist actors out of composeThread() * tidy * update thread gate lexicons, codegen * lex fix * rename gate to threadgate in bsky, update views * lex fix * improve terminology around reply validation * fix down migration * remove thread gates on actor unindexing * add back .prettierignore * tidy * run ci on all prs * syntax * run ci on all prs * format * fix snap --------- Co-authored-by: Devin Ivy --- lexicons/app/bsky/feed/defs.json | 24 +- lexicons/app/bsky/feed/threadgate.json | 43 ++ packages/api/src/client/index.ts | 69 +++ packages/api/src/client/lexicons.ts | 97 +++ .../src/client/types/app/bsky/feed/defs.ts | 40 ++ .../client/types/app/bsky/feed/threadgate.ts | 80 +++ .../src/api/app/bsky/feed/getPostThread.ts | 75 ++- .../bsky/src/api/app/bsky/feed/getPosts.ts | 2 + packages/bsky/src/db/database-schema.ts | 2 + .../20230906T222220386Z-thread-gating.ts | 31 + packages/bsky/src/db/migrations/index.ts | 1 + packages/bsky/src/db/tables/post.ts | 4 +- packages/bsky/src/db/tables/thread-gate.ts | 12 + packages/bsky/src/lexicon/lexicons.ts | 97 +++ .../src/lexicon/types/app/bsky/feed/defs.ts | 40 ++ .../lexicon/types/app/bsky/feed/threadgate.ts | 80 +++ packages/bsky/src/services/feed/index.ts | 67 +- packages/bsky/src/services/feed/types.ts | 14 + packages/bsky/src/services/feed/util.ts | 112 ++++ packages/bsky/src/services/feed/views.ts | 57 +- packages/bsky/src/services/indexing/index.ts | 7 + .../src/services/indexing/plugins/post.ts | 79 ++- .../services/indexing/plugins/thread-gate.ts | 95 +++ .../tests/__snapshots__/indexing.test.ts.snap | 6 + packages/bsky/tests/_util.ts | 1 + .../__snapshots__/block-lists.test.ts.snap | 9 + .../views/__snapshots__/blocks.test.ts.snap | 9 + .../__snapshots__/mute-lists.test.ts.snap | 3 + .../views/__snapshots__/mutes.test.ts.snap | 3 + .../views/__snapshots__/thread.test.ts.snap | 30 + .../__snapshots__/threadgating.test.ts.snap | 164 +++++ .../bsky/tests/views/threadgating.test.ts | 571 ++++++++++++++++++ packages/pds/src/lexicon/lexicons.ts | 97 +++ .../src/lexicon/types/app/bsky/feed/defs.ts | 40 ++ .../lexicon/types/app/bsky/feed/threadgate.ts | 80 +++ packages/pds/src/repo/prepare.ts | 22 +- 36 files changed, 2135 insertions(+), 28 deletions(-) create mode 100644 lexicons/app/bsky/feed/threadgate.json create mode 100644 packages/api/src/client/types/app/bsky/feed/threadgate.ts create mode 100644 packages/bsky/src/db/migrations/20230906T222220386Z-thread-gating.ts create mode 100644 packages/bsky/src/db/tables/thread-gate.ts create mode 100644 packages/bsky/src/lexicon/types/app/bsky/feed/threadgate.ts create mode 100644 packages/bsky/src/services/feed/util.ts create mode 100644 packages/bsky/src/services/indexing/plugins/thread-gate.ts create mode 100644 packages/bsky/tests/views/__snapshots__/threadgating.test.ts.snap create mode 100644 packages/bsky/tests/views/threadgating.test.ts create mode 100644 packages/pds/src/lexicon/types/app/bsky/feed/threadgate.ts diff --git a/lexicons/app/bsky/feed/defs.json b/lexicons/app/bsky/feed/defs.json index 7a9fcf5e68f..10f2812ce24 100644 --- a/lexicons/app/bsky/feed/defs.json +++ b/lexicons/app/bsky/feed/defs.json @@ -30,7 +30,8 @@ "labels": { "type": "array", "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } - } + }, + "threadgate": { "type": "ref", "ref": "#threadgateView" } } }, "viewerState": { @@ -86,7 +87,8 @@ "type": "union", "refs": ["#threadViewPost", "#notFoundPost", "#blockedPost"] } - } + }, + "viewer": { "type": "ref", "ref": "#viewerThreadState" } } }, "notFoundPost": { @@ -114,6 +116,12 @@ "viewer": { "type": "ref", "ref": "app.bsky.actor.defs#viewerState" } } }, + "viewerThreadState": { + "type": "object", + "properties": { + "canReply": { "type": "boolean" } + } + }, "generatorView": { "type": "object", "required": ["uri", "cid", "did", "creator", "displayName", "indexedAt"], @@ -158,6 +166,18 @@ "properties": { "repost": { "type": "string", "format": "at-uri" } } + }, + "threadgateView": { + "type": "object", + "properties": { + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "record": { "type": "unknown" }, + "lists": { + "type": "array", + "items": { "type": "ref", "ref": "app.bsky.graph.defs#listViewBasic" } + } + } } } } diff --git a/lexicons/app/bsky/feed/threadgate.json b/lexicons/app/bsky/feed/threadgate.json new file mode 100644 index 00000000000..aa2262174d1 --- /dev/null +++ b/lexicons/app/bsky/feed/threadgate.json @@ -0,0 +1,43 @@ +{ + "lexicon": 1, + "id": "app.bsky.feed.threadgate", + "defs": { + "main": { + "type": "record", + "key": "tid", + "description": "Defines interaction gating rules for a thread. The rkey of the threadgate record should match the rkey of the thread's root post.", + "record": { + "type": "object", + "required": ["post", "createdAt"], + "properties": { + "post": { "type": "string", "format": "at-uri" }, + "allow": { + "type": "array", + "maxLength": 5, + "items": { + "type": "union", + "refs": ["#mentionRule", "#followingRule", "#listRule"] + } + }, + "createdAt": { "type": "string", "format": "datetime" } + } + } + }, + "mentionRule": { + "type": "object", + "description": "Allow replies from actors mentioned in your post." + }, + "followingRule": { + "type": "object", + "description": "Allow replies from actors you follow." + }, + "listRule": { + "type": "object", + "description": "Allow replies from actors on a list.", + "required": ["list"], + "properties": { + "list": { "type": "string", "format": "at-uri" } + } + } + } +} diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index 8a32bef870a..d72fe659e50 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -103,6 +103,7 @@ import * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline' import * as AppBskyFeedLike from './types/app/bsky/feed/like' import * as AppBskyFeedPost from './types/app/bsky/feed/post' import * as AppBskyFeedRepost from './types/app/bsky/feed/repost' +import * as AppBskyFeedThreadgate from './types/app/bsky/feed/threadgate' import * as AppBskyGraphBlock from './types/app/bsky/graph/block' import * as AppBskyGraphDefs from './types/app/bsky/graph/defs' import * as AppBskyGraphFollow from './types/app/bsky/graph/follow' @@ -228,6 +229,7 @@ export * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline' export * as AppBskyFeedLike from './types/app/bsky/feed/like' export * as AppBskyFeedPost from './types/app/bsky/feed/post' export * as AppBskyFeedRepost from './types/app/bsky/feed/repost' +export * as AppBskyFeedThreadgate from './types/app/bsky/feed/threadgate' export * as AppBskyGraphBlock from './types/app/bsky/graph/block' export * as AppBskyGraphDefs from './types/app/bsky/graph/defs' export * as AppBskyGraphFollow from './types/app/bsky/graph/follow' @@ -1204,6 +1206,7 @@ export class FeedNS { like: LikeRecord post: PostRecord repost: RepostRecord + threadgate: ThreadgateRecord constructor(service: AtpServiceClient) { this._service = service @@ -1211,6 +1214,7 @@ export class FeedNS { this.like = new LikeRecord(service) this.post = new PostRecord(service) this.repost = new RepostRecord(service) + this.threadgate = new ThreadgateRecord(service) } describeFeedGenerator( @@ -1623,6 +1627,71 @@ export class RepostRecord { } } +export class ThreadgateRecord { + _service: AtpServiceClient + + constructor(service: AtpServiceClient) { + this._service = service + } + + async list( + params: Omit, + ): Promise<{ + cursor?: string + records: { uri: string; value: AppBskyFeedThreadgate.Record }[] + }> { + const res = await this._service.xrpc.call('com.atproto.repo.listRecords', { + collection: 'app.bsky.feed.threadgate', + ...params, + }) + return res.data + } + + async get( + params: Omit, + ): Promise<{ + uri: string + cid: string + value: AppBskyFeedThreadgate.Record + }> { + const res = await this._service.xrpc.call('com.atproto.repo.getRecord', { + collection: 'app.bsky.feed.threadgate', + ...params, + }) + return res.data + } + + async create( + params: Omit< + ComAtprotoRepoCreateRecord.InputSchema, + 'collection' | 'record' + >, + record: AppBskyFeedThreadgate.Record, + headers?: Record, + ): Promise<{ uri: string; cid: string }> { + record.$type = 'app.bsky.feed.threadgate' + const res = await this._service.xrpc.call( + 'com.atproto.repo.createRecord', + undefined, + { collection: 'app.bsky.feed.threadgate', ...params, record }, + { encoding: 'application/json', headers }, + ) + return res.data + } + + async delete( + params: Omit, + headers?: Record, + ): Promise { + await this._service.xrpc.call( + 'com.atproto.repo.deleteRecord', + undefined, + { collection: 'app.bsky.feed.threadgate', ...params }, + { headers }, + ) + } +} + export class GraphNS { _service: AtpServiceClient block: BlockRecord diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index f1fd2519d74..2ca983aec4b 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -4390,6 +4390,10 @@ export const schemaDict = { ref: 'lex:com.atproto.label.defs#label', }, }, + threadgate: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#threadgateView', + }, }, }, viewerState: { @@ -4486,6 +4490,10 @@ export const schemaDict = { ], }, }, + viewer: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#viewerThreadState', + }, }, }, notFoundPost: { @@ -4534,6 +4542,14 @@ export const schemaDict = { }, }, }, + viewerThreadState: { + type: 'object', + properties: { + canReply: { + type: 'boolean', + }, + }, + }, generatorView: { type: 'object', required: ['uri', 'cid', 'did', 'creator', 'displayName', 'indexedAt'], @@ -4619,6 +4635,29 @@ export const schemaDict = { }, }, }, + threadgateView: { + type: 'object', + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + record: { + type: 'unknown', + }, + lists: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.graph.defs#listViewBasic', + }, + }, + }, + }, }, }, AppBskyFeedDescribeFeedGenerator: { @@ -5615,6 +5654,63 @@ export const schemaDict = { }, }, }, + AppBskyFeedThreadgate: { + lexicon: 1, + id: 'app.bsky.feed.threadgate', + defs: { + main: { + type: 'record', + key: 'tid', + description: + "Defines interaction gating rules for a thread. The rkey of the threadgate record should match the rkey of the thread's root post.", + record: { + type: 'object', + required: ['post', 'createdAt'], + properties: { + post: { + type: 'string', + format: 'at-uri', + }, + allow: { + type: 'array', + maxLength: 5, + items: { + type: 'union', + refs: [ + 'lex:app.bsky.feed.threadgate#mentionRule', + 'lex:app.bsky.feed.threadgate#followingRule', + 'lex:app.bsky.feed.threadgate#listRule', + ], + }, + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + mentionRule: { + type: 'object', + description: 'Allow replies from actors mentioned in your post.', + }, + followingRule: { + type: 'object', + description: 'Allow replies from actors you follow.', + }, + listRule: { + type: 'object', + description: 'Allow replies from actors on a list.', + required: ['list'], + properties: { + list: { + type: 'string', + format: 'at-uri', + }, + }, + }, + }, + }, AppBskyGraphBlock: { lexicon: 1, id: 'app.bsky.graph.block', @@ -6932,6 +7028,7 @@ export const ids = { AppBskyFeedLike: 'app.bsky.feed.like', AppBskyFeedPost: 'app.bsky.feed.post', AppBskyFeedRepost: 'app.bsky.feed.repost', + AppBskyFeedThreadgate: 'app.bsky.feed.threadgate', AppBskyGraphBlock: 'app.bsky.graph.block', AppBskyGraphDefs: 'app.bsky.graph.defs', AppBskyGraphFollow: 'app.bsky.graph.follow', diff --git a/packages/api/src/client/types/app/bsky/feed/defs.ts b/packages/api/src/client/types/app/bsky/feed/defs.ts index 1270dab250b..944fd34b072 100644 --- a/packages/api/src/client/types/app/bsky/feed/defs.ts +++ b/packages/api/src/client/types/app/bsky/feed/defs.ts @@ -12,6 +12,7 @@ import * as AppBskyEmbedRecord from '../embed/record' import * as AppBskyEmbedRecordWithMedia from '../embed/recordWithMedia' import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' import * as AppBskyRichtextFacet from '../richtext/facet' +import * as AppBskyGraphDefs from '../graph/defs' export interface PostView { uri: string @@ -30,6 +31,7 @@ export interface PostView { indexedAt: string viewer?: ViewerState labels?: ComAtprotoLabelDefs.Label[] + threadgate?: ThreadgateView [k: string]: unknown } @@ -135,6 +137,7 @@ export interface ThreadViewPost { | BlockedPost | { $type: string; [k: string]: unknown } )[] + viewer?: ViewerThreadState [k: string]: unknown } @@ -205,6 +208,23 @@ export function validateBlockedAuthor(v: unknown): ValidationResult { return lexicons.validate('app.bsky.feed.defs#blockedAuthor', v) } +export interface ViewerThreadState { + canReply?: boolean + [k: string]: unknown +} + +export function isViewerThreadState(v: unknown): v is ViewerThreadState { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.defs#viewerThreadState' + ) +} + +export function validateViewerThreadState(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.defs#viewerThreadState', v) +} + export interface GeneratorView { uri: string cid: string @@ -283,3 +303,23 @@ export function isSkeletonReasonRepost(v: unknown): v is SkeletonReasonRepost { export function validateSkeletonReasonRepost(v: unknown): ValidationResult { return lexicons.validate('app.bsky.feed.defs#skeletonReasonRepost', v) } + +export interface ThreadgateView { + uri?: string + cid?: string + record?: {} + lists?: AppBskyGraphDefs.ListViewBasic[] + [k: string]: unknown +} + +export function isThreadgateView(v: unknown): v is ThreadgateView { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.defs#threadgateView' + ) +} + +export function validateThreadgateView(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.defs#threadgateView', v) +} diff --git a/packages/api/src/client/types/app/bsky/feed/threadgate.ts b/packages/api/src/client/types/app/bsky/feed/threadgate.ts new file mode 100644 index 00000000000..20e0a62eb3a --- /dev/null +++ b/packages/api/src/client/types/app/bsky/feed/threadgate.ts @@ -0,0 +1,80 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface Record { + post: string + allow?: ( + | MentionRule + | FollowingRule + | ListRule + | { $type: string; [k: string]: unknown } + )[] + createdAt: string + [k: string]: unknown +} + +export function isRecord(v: unknown): v is Record { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'app.bsky.feed.threadgate#main' || + v.$type === 'app.bsky.feed.threadgate') + ) +} + +export function validateRecord(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.threadgate#main', v) +} + +/** Allow replies from actors mentioned in your post. */ +export interface MentionRule {} + +export function isMentionRule(v: unknown): v is MentionRule { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.threadgate#mentionRule' + ) +} + +export function validateMentionRule(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.threadgate#mentionRule', v) +} + +/** Allow replies from actors you follow. */ +export interface FollowingRule {} + +export function isFollowingRule(v: unknown): v is FollowingRule { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.threadgate#followingRule' + ) +} + +export function validateFollowingRule(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.threadgate#followingRule', v) +} + +/** Allow replies from actors on a list. */ +export interface ListRule { + list: string + [k: string]: unknown +} + +export function isListRule(v: unknown): v is ListRule { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.threadgate#listRule' + ) +} + +export function validateListRule(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.threadgate#listRule', v) +} diff --git a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts index 2d10ff98006..9292f0d5d99 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts @@ -1,26 +1,33 @@ import { InvalidRequestError } from '@atproto/xrpc-server' +import { AtUri } from '@atproto/syntax' import { Server } from '../../../../lexicon' import { BlockedPost, NotFoundPost, ThreadViewPost, isNotFoundPost, + isThreadViewPost, } from '../../../../lexicon/types/app/bsky/feed/defs' +import { Record as PostRecord } from '../../../../lexicon/types/app/bsky/feed/post' +import { Record as ThreadgateRecord } from '../../../../lexicon/types/app/bsky/feed/threadgate' import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getPostThread' import AppContext from '../../../../context' import { FeedService, FeedRow, FeedHydrationState, + PostInfo, } from '../../../../services/feed' import { getAncestorsAndSelfQb, getDescendentsQb, } from '../../../../services/util/post' import { Database } from '../../../../db' +import DatabaseSchema from '../../../../db/database-schema' import { setRepoRev } from '../../../util' -import { createPipeline, noRules } from '../../../../pipeline' import { ActorInfoMap, ActorService } from '../../../../services/actor' +import { violatesThreadGate } from '../../../../services/feed/util' +import { createPipeline, noRules } from '../../../../pipeline' export default function (server: Server, ctx: AppContext) { const getPostThread = createPipeline( @@ -73,7 +80,21 @@ const hydration = async (state: SkeletonState, ctx: Context) => { } = state const relevant = getRelevantIds(threadData) const hydrated = await feedService.feedHydration({ ...relevant, viewer }) - return { ...state, ...hydrated } + // check root reply interaction rules + const anchorPostUri = threadData.post.postUri + const rootUri = threadData.post.replyRoot || anchorPostUri + const anchor = hydrated.posts[anchorPostUri] + const root = hydrated.posts[rootUri] + const gate = hydrated.threadgates[rootUri]?.record + const viewerCanReply = await checkViewerCanReply( + ctx.db.db, + anchor ?? null, + viewer, + new AtUri(rootUri).host, + (root?.record ?? null) as PostRecord | null, + gate ?? null, + ) + return { ...state, ...hydrated, viewerCanReply } } const presentation = (state: HydrationState, ctx: Context) => { @@ -89,6 +110,9 @@ const presentation = (state: HydrationState, ctx: Context) => { // @TODO technically this could be returned as a NotFoundPost based on lexicon throw new InvalidRequestError(`Post not found: ${params.uri}`, 'NotFound') } + if (isThreadViewPost(thread) && params.viewer) { + thread.viewer = { canReply: state.viewerCanReply } + } return { thread } } @@ -99,17 +123,27 @@ const composeThread = ( ctx: Context, ) => { const { feedService } = ctx - const { posts, embeds, blocks, labels } = state + const { posts, threadgates, embeds, blocks, labels, lists } = state const post = feedService.views.formatPostView( threadData.post.postUri, actors, posts, + threadgates, embeds, labels, + lists, ) - if (!post || blocks[post.uri]?.reply) { + // replies that are invalid due to reply-gating: + // a. may appear as the anchor post, but without any parent or replies. + // b. may not appear anywhere else in the thread. + const isAnchorPost = state.threadData.post.uri === threadData.post.postUri + const info = posts[threadData.post.postUri] + const badReply = !!info?.invalidReplyRoot || !!info?.violatesThreadGate + const omitBadReply = !isAnchorPost && badReply + + if (!post || blocks[post.uri]?.reply || omitBadReply) { return { $type: 'app.bsky.feed.defs#notFoundPost', uri: threadData.post.postUri, @@ -135,7 +169,7 @@ const composeThread = ( } let parent - if (threadData.parent) { + if (threadData.parent && !badReply) { if (threadData.parent instanceof ParentNotFoundError) { parent = { $type: 'app.bsky.feed.defs#notFoundPost', @@ -148,7 +182,7 @@ const composeThread = ( } let replies: (ThreadViewPost | NotFoundPost | BlockedPost)[] | undefined - if (threadData.replies) { + if (threadData.replies && !badReply) { replies = threadData.replies.flatMap((reply) => { const thread = composeThread(reply, actors, state, ctx) // e.g. don't bother including #postNotFound reply placeholders for takedowns. either way matches api contract. @@ -184,6 +218,10 @@ const getRelevantIds = ( } dids.add(thread.post.postAuthorDid) uris.add(thread.post.postUri) + if (thread.post.replyRoot) { + // ensure root is included for checking interactions + uris.add(thread.post.replyRoot) + } return { dids, uris } } @@ -265,6 +303,26 @@ const getChildrenData = ( })) } +const checkViewerCanReply = async ( + db: DatabaseSchema, + anchor: PostInfo | null, + viewer: string | null, + owner: string, + root: PostRecord | null, + threadgate: ThreadgateRecord | null, +) => { + if (!viewer) return false + if (anchor?.invalidReplyRoot || anchor?.violatesThreadGate) return false + const viewerViolatesThreadGate = await violatesThreadGate( + db, + viewer, + owner, + root, + threadgate, + ) + return !viewerViolatesThreadGate +} + class ParentNotFoundError extends Error { constructor(public uri: string) { super(`Parent not found: ${uri}`) @@ -290,4 +348,7 @@ type SkeletonState = { threadData: PostThread } -type HydrationState = SkeletonState & FeedHydrationState +type HydrationState = SkeletonState & + FeedHydrationState & { + viewerCanReply: boolean + } diff --git a/packages/bsky/src/api/app/bsky/feed/getPosts.ts b/packages/bsky/src/api/app/bsky/feed/getPosts.ts index fc35b203034..90268e5f161 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPosts.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPosts.ts @@ -72,8 +72,10 @@ const presentation = (state: HydrationState, ctx: Context) => { uri, actors, state.posts, + state.threadgates, state.embeds, state.labels, + state.lists, ) return postView ?? SKIP }) diff --git a/packages/bsky/src/db/database-schema.ts b/packages/bsky/src/db/database-schema.ts index adb8c088207..e43ade819e6 100644 --- a/packages/bsky/src/db/database-schema.ts +++ b/packages/bsky/src/db/database-schema.ts @@ -6,6 +6,7 @@ import * as post from './tables/post' import * as postEmbed from './tables/post-embed' import * as postAgg from './tables/post-agg' import * as repost from './tables/repost' +import * as threadGate from './tables/thread-gate' import * as feedItem from './tables/feed-item' import * as follow from './tables/follow' import * as like from './tables/like' @@ -38,6 +39,7 @@ export type DatabaseSchemaType = duplicateRecord.PartialDB & postEmbed.PartialDB & postAgg.PartialDB & repost.PartialDB & + threadGate.PartialDB & feedItem.PartialDB & follow.PartialDB & like.PartialDB & diff --git a/packages/bsky/src/db/migrations/20230906T222220386Z-thread-gating.ts b/packages/bsky/src/db/migrations/20230906T222220386Z-thread-gating.ts new file mode 100644 index 00000000000..42296aaccf9 --- /dev/null +++ b/packages/bsky/src/db/migrations/20230906T222220386Z-thread-gating.ts @@ -0,0 +1,31 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .createTable('thread_gate') + .addColumn('uri', 'varchar', (col) => col.primaryKey()) + .addColumn('cid', 'varchar', (col) => col.notNull()) + .addColumn('creator', 'varchar', (col) => col.notNull()) + .addColumn('postUri', 'varchar', (col) => col.notNull().unique()) + .addColumn('createdAt', 'varchar', (col) => col.notNull()) + .addColumn('indexedAt', 'varchar', (col) => col.notNull()) + .execute() + await db.schema + .alterTable('post') + .addColumn('invalidReplyRoot', 'boolean', (col) => + col.notNull().defaultTo(false), + ) + .execute() + await db.schema + .alterTable('post') + .addColumn('violatesThreadGate', 'boolean', (col) => + col.notNull().defaultTo(false), + ) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('thread_gate').execute() + await db.schema.alterTable('post').dropColumn('invalidReplyRoot').execute() + await db.schema.alterTable('post').dropColumn('violatesThreadGate').execute() +} diff --git a/packages/bsky/src/db/migrations/index.ts b/packages/bsky/src/db/migrations/index.ts index 505f7c84909..bf18d8dd15b 100644 --- a/packages/bsky/src/db/migrations/index.ts +++ b/packages/bsky/src/db/migrations/index.ts @@ -27,3 +27,4 @@ export * as _20230810T203349843Z from './20230810T203349843Z-action-duration' export * as _20230817T195936007Z from './20230817T195936007Z-native-notifications' export * as _20230830T205507322Z from './20230830T205507322Z-suggested-feeds' export * as _20230904T211011773Z from './20230904T211011773Z-block-lists' +export * as _20230906T222220386Z from './20230906T222220386Z-thread-gating' diff --git a/packages/bsky/src/db/tables/post.ts b/packages/bsky/src/db/tables/post.ts index f878b8dab33..d70a75912a5 100644 --- a/packages/bsky/src/db/tables/post.ts +++ b/packages/bsky/src/db/tables/post.ts @@ -1,4 +1,4 @@ -import { GeneratedAlways } from 'kysely' +import { Generated, GeneratedAlways } from 'kysely' export const tableName = 'post' @@ -12,6 +12,8 @@ export interface Post { replyParent: string | null replyParentCid: string | null langs: string[] | null + invalidReplyRoot: Generated + violatesThreadGate: Generated createdAt: string indexedAt: string sortAt: GeneratedAlways diff --git a/packages/bsky/src/db/tables/thread-gate.ts b/packages/bsky/src/db/tables/thread-gate.ts new file mode 100644 index 00000000000..327ee7e41c6 --- /dev/null +++ b/packages/bsky/src/db/tables/thread-gate.ts @@ -0,0 +1,12 @@ +const tableName = 'thread_gate' + +export interface ThreadGate { + uri: string + cid: string + creator: string + postUri: string + createdAt: string + indexedAt: string +} + +export type PartialDB = { [tableName]: ThreadGate } diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index f1fd2519d74..2ca983aec4b 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -4390,6 +4390,10 @@ export const schemaDict = { ref: 'lex:com.atproto.label.defs#label', }, }, + threadgate: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#threadgateView', + }, }, }, viewerState: { @@ -4486,6 +4490,10 @@ export const schemaDict = { ], }, }, + viewer: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#viewerThreadState', + }, }, }, notFoundPost: { @@ -4534,6 +4542,14 @@ export const schemaDict = { }, }, }, + viewerThreadState: { + type: 'object', + properties: { + canReply: { + type: 'boolean', + }, + }, + }, generatorView: { type: 'object', required: ['uri', 'cid', 'did', 'creator', 'displayName', 'indexedAt'], @@ -4619,6 +4635,29 @@ export const schemaDict = { }, }, }, + threadgateView: { + type: 'object', + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + record: { + type: 'unknown', + }, + lists: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.graph.defs#listViewBasic', + }, + }, + }, + }, }, }, AppBskyFeedDescribeFeedGenerator: { @@ -5615,6 +5654,63 @@ export const schemaDict = { }, }, }, + AppBskyFeedThreadgate: { + lexicon: 1, + id: 'app.bsky.feed.threadgate', + defs: { + main: { + type: 'record', + key: 'tid', + description: + "Defines interaction gating rules for a thread. The rkey of the threadgate record should match the rkey of the thread's root post.", + record: { + type: 'object', + required: ['post', 'createdAt'], + properties: { + post: { + type: 'string', + format: 'at-uri', + }, + allow: { + type: 'array', + maxLength: 5, + items: { + type: 'union', + refs: [ + 'lex:app.bsky.feed.threadgate#mentionRule', + 'lex:app.bsky.feed.threadgate#followingRule', + 'lex:app.bsky.feed.threadgate#listRule', + ], + }, + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + mentionRule: { + type: 'object', + description: 'Allow replies from actors mentioned in your post.', + }, + followingRule: { + type: 'object', + description: 'Allow replies from actors you follow.', + }, + listRule: { + type: 'object', + description: 'Allow replies from actors on a list.', + required: ['list'], + properties: { + list: { + type: 'string', + format: 'at-uri', + }, + }, + }, + }, + }, AppBskyGraphBlock: { lexicon: 1, id: 'app.bsky.graph.block', @@ -6932,6 +7028,7 @@ export const ids = { AppBskyFeedLike: 'app.bsky.feed.like', AppBskyFeedPost: 'app.bsky.feed.post', AppBskyFeedRepost: 'app.bsky.feed.repost', + AppBskyFeedThreadgate: 'app.bsky.feed.threadgate', AppBskyGraphBlock: 'app.bsky.graph.block', AppBskyGraphDefs: 'app.bsky.graph.defs', AppBskyGraphFollow: 'app.bsky.graph.follow', diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/defs.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/defs.ts index 463445fbd49..08d34d88ebb 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/defs.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/defs.ts @@ -12,6 +12,7 @@ import * as AppBskyEmbedRecord from '../embed/record' import * as AppBskyEmbedRecordWithMedia from '../embed/recordWithMedia' import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' import * as AppBskyRichtextFacet from '../richtext/facet' +import * as AppBskyGraphDefs from '../graph/defs' export interface PostView { uri: string @@ -30,6 +31,7 @@ export interface PostView { indexedAt: string viewer?: ViewerState labels?: ComAtprotoLabelDefs.Label[] + threadgate?: ThreadgateView [k: string]: unknown } @@ -135,6 +137,7 @@ export interface ThreadViewPost { | BlockedPost | { $type: string; [k: string]: unknown } )[] + viewer?: ViewerThreadState [k: string]: unknown } @@ -205,6 +208,23 @@ export function validateBlockedAuthor(v: unknown): ValidationResult { return lexicons.validate('app.bsky.feed.defs#blockedAuthor', v) } +export interface ViewerThreadState { + canReply?: boolean + [k: string]: unknown +} + +export function isViewerThreadState(v: unknown): v is ViewerThreadState { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.defs#viewerThreadState' + ) +} + +export function validateViewerThreadState(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.defs#viewerThreadState', v) +} + export interface GeneratorView { uri: string cid: string @@ -283,3 +303,23 @@ export function isSkeletonReasonRepost(v: unknown): v is SkeletonReasonRepost { export function validateSkeletonReasonRepost(v: unknown): ValidationResult { return lexicons.validate('app.bsky.feed.defs#skeletonReasonRepost', v) } + +export interface ThreadgateView { + uri?: string + cid?: string + record?: {} + lists?: AppBskyGraphDefs.ListViewBasic[] + [k: string]: unknown +} + +export function isThreadgateView(v: unknown): v is ThreadgateView { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.defs#threadgateView' + ) +} + +export function validateThreadgateView(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.defs#threadgateView', v) +} diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/threadgate.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/threadgate.ts new file mode 100644 index 00000000000..51f9f8e9af1 --- /dev/null +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/threadgate.ts @@ -0,0 +1,80 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' + +export interface Record { + post: string + allow?: ( + | MentionRule + | FollowingRule + | ListRule + | { $type: string; [k: string]: unknown } + )[] + createdAt: string + [k: string]: unknown +} + +export function isRecord(v: unknown): v is Record { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'app.bsky.feed.threadgate#main' || + v.$type === 'app.bsky.feed.threadgate') + ) +} + +export function validateRecord(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.threadgate#main', v) +} + +/** Allow replies from actors mentioned in your post. */ +export interface MentionRule {} + +export function isMentionRule(v: unknown): v is MentionRule { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.threadgate#mentionRule' + ) +} + +export function validateMentionRule(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.threadgate#mentionRule', v) +} + +/** Allow replies from actors you follow. */ +export interface FollowingRule {} + +export function isFollowingRule(v: unknown): v is FollowingRule { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.threadgate#followingRule' + ) +} + +export function validateFollowingRule(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.threadgate#followingRule', v) +} + +/** Allow replies from actors on a list. */ +export interface ListRule { + list: string + [k: string]: unknown +} + +export function isListRule(v: unknown): v is ListRule { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.threadgate#listRule' + ) +} + +export function validateListRule(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.threadgate#listRule', v) +} diff --git a/packages/bsky/src/services/feed/index.ts b/packages/bsky/src/services/feed/index.ts index db32f1971bc..e5ee2d1c8db 100644 --- a/packages/bsky/src/services/feed/index.ts +++ b/packages/bsky/src/services/feed/index.ts @@ -9,6 +9,10 @@ import { Record as PostRecord, isRecord as isPostRecord, } from '../../lexicon/types/app/bsky/feed/post' +import { + Record as ThreadgateRecord, + isListRule, +} from '../../lexicon/types/app/bsky/feed/threadgate' import { isMain as isEmbedImages } from '../../lexicon/types/app/bsky/embed/images' import { isMain as isEmbedExternal } from '../../lexicon/types/app/bsky/embed/external' import { @@ -27,12 +31,19 @@ import { RecordEmbedViewRecord, PostBlocksMap, FeedHydrationState, + ThreadgateInfoMap, } from './types' import { LabelService } from '../label' import { ActorService } from '../actor' -import { BlockAndMuteState, GraphService, RelationshipPair } from '../graph' +import { + BlockAndMuteState, + GraphService, + ListInfoMap, + RelationshipPair, +} from '../graph' import { FeedViews } from './views' import { LabelCache } from '../../label-cache' +import { threadgateToPostUri, postToThreadgateUri } from './util' export * from './types' @@ -130,6 +141,8 @@ export class FeedService { 'post.cid as cid', 'post.creator as creator', 'post.sortAt as indexedAt', + 'post.invalidReplyRoot as invalidReplyRoot', + 'post.violatesThreadGate as violatesThreadGate', 'record.json as recordJson', 'post_agg.likeCount as likeCount', 'post_agg.repostCount as repostCount', @@ -152,11 +165,8 @@ export class FeedService { .execute() return posts.reduce((acc, cur) => { const { recordJson, ...post } = cur - const info: PostInfo = { - ...post, - record: jsonStringToLex(recordJson) as Record, - viewer, - } + const record = jsonStringToLex(recordJson) as PostRecord + const info: PostInfo = { ...post, record, viewer } return Object.assign(acc, { [post.uri]: info }) }, {} as PostInfoMap) } @@ -216,31 +226,35 @@ export class FeedService { depth = 0, ): Promise { const { viewer, dids, uris } = refs - const [posts, labels, bam] = await Promise.all([ + const [posts, threadgates, labels, bam] = await Promise.all([ this.getPostInfos(Array.from(uris), viewer), + this.threadgatesByPostUri(Array.from(uris)), this.services.label.getLabelsForSubjects([...uris, ...dids]), this.services.graph.getBlockAndMuteState( viewer ? [...dids].map((did) => [viewer, did]) : [], ), ]) + // profileState for labels and bam handled above, profileHydration() shouldn't fetch additional - const [profileState, blocks] = await Promise.all([ + const [profileState, blocks, lists] = await Promise.all([ this.services.actor.views.profileHydration( Array.from(dids), { viewer }, { bam, labels }, ), this.blocksForPosts(posts, bam), + this.listsForThreadgates(threadgates, viewer), ]) const embeds = await this.embedsForPosts(posts, blocks, viewer, depth) return { posts, + threadgates, blocks, embeds, labels, // includes info for profiles bam, // includes info for profiles profiles: profileState.profiles, - lists: profileState.lists, + lists: Object.assign(lists, profileState.lists), } } @@ -398,8 +412,10 @@ export class FeedService { uri, actorInfos, feedState.posts, + feedState.threadgates, feedState.embeds, feedState.labels, + feedState.lists, ) recordEmbedViews[uri] = this.views.getRecordEmbedView( uri, @@ -416,6 +432,39 @@ export class FeedService { } return recordEmbedViews } + + async threadgatesByPostUri(postUris: string[]): Promise { + const gates = postUris.length + ? await this.db.db + .selectFrom('record') + .where('uri', 'in', postUris.map(postToThreadgateUri)) + .select(['uri', 'cid', 'json']) + .execute() + : [] + const gatesByPostUri = gates.reduce((acc, gate) => { + const record = jsonStringToLex(gate.json) as ThreadgateRecord + const postUri = threadgateToPostUri(gate.uri) + if (record.post !== postUri) return acc // invalid, skip + acc[postUri] = { uri: gate.uri, cid: gate.cid, record } + return acc + }, {} as ThreadgateInfoMap) + return gatesByPostUri + } + + listsForThreadgates( + threadgates: ThreadgateInfoMap, + viewer: string | null, + ): Promise { + const listsUris = new Set() + Object.values(threadgates).forEach((gate) => { + gate?.record.allow?.forEach((rule) => { + if (isListRule(rule)) { + listsUris.add(rule.list) + } + }) + }) + return this.services.graph.getListViews([...listsUris], viewer) + } } const postRecordsFromInfos = ( diff --git a/packages/bsky/src/services/feed/types.ts b/packages/bsky/src/services/feed/types.ts index 894ee0a564f..8d4bd67f6bb 100644 --- a/packages/bsky/src/services/feed/types.ts +++ b/packages/bsky/src/services/feed/types.ts @@ -1,4 +1,5 @@ import { Selectable } from 'kysely' +import { Record as ThreadgateRecord } from '../../lexicon/types/app/bsky/feed/threadgate' import { View as ImagesEmbedView } from '../../lexicon/types/app/bsky/embed/images' import { View as ExternalEmbedView } from '../../lexicon/types/app/bsky/embed/external' import { @@ -41,6 +42,8 @@ export type PostInfo = { replyCount: number | null requesterRepost: string | null requesterLike: string | null + invalidReplyRoot: boolean + violatesThreadGate: boolean viewer: string | null } @@ -50,6 +53,16 @@ export type PostBlocksMap = { [uri: string]: { reply?: boolean; embed?: boolean } } +export type ThreadgateInfo = { + uri: string + cid: string + record: ThreadgateRecord +} + +export type ThreadgateInfoMap = { + [postUri: string]: ThreadgateInfo +} + export type FeedGenInfo = Selectable & { likeCount: number viewer?: { @@ -86,6 +99,7 @@ export type RecordEmbedViewRecordMap = { [uri: string]: RecordEmbedViewRecord } export type FeedHydrationState = ProfileHydrationState & { posts: PostInfoMap + threadgates: ThreadgateInfoMap embeds: PostEmbedViews labels: Labels blocks: PostBlocksMap diff --git a/packages/bsky/src/services/feed/util.ts b/packages/bsky/src/services/feed/util.ts new file mode 100644 index 00000000000..b2e2ce8d92d --- /dev/null +++ b/packages/bsky/src/services/feed/util.ts @@ -0,0 +1,112 @@ +import { sql } from 'kysely' +import { AtUri } from '@atproto/syntax' +import { + Record as PostRecord, + ReplyRef, +} from '../../lexicon/types/app/bsky/feed/post' +import { + Record as GateRecord, + isFollowingRule, + isListRule, + isMentionRule, +} from '../../lexicon/types/app/bsky/feed/threadgate' +import { isMention } from '../../lexicon/types/app/bsky/richtext/facet' +import { valuesList } from '../../db/util' +import DatabaseSchema from '../../db/database-schema' +import { ids } from '../../lexicon/lexicons' + +export const invalidReplyRoot = ( + reply: ReplyRef, + parent: { + record: PostRecord + invalidReplyRoot: boolean | null + }, +) => { + const replyRoot = reply.root.uri + const replyParent = reply.parent.uri + // if parent is not a valid reply, transitively this is not a valid one either + if (parent.invalidReplyRoot) { + return true + } + // replying to root post: ensure the root looks correct + if (replyParent === replyRoot) { + return !!parent.record.reply + } + // replying to a reply: ensure the parent is a reply for the same root post + return parent.record.reply?.root.uri !== replyRoot +} + +export const violatesThreadGate = async ( + db: DatabaseSchema, + did: string, + owner: string, + root: PostRecord | null, + gate: GateRecord | null, +) => { + if (did === owner) return false + if (!gate?.allow) return false + + const allowMentions = gate.allow.find(isMentionRule) + const allowFollowing = gate.allow.find(isFollowingRule) + const allowListUris = gate.allow?.filter(isListRule).map((item) => item.list) + + // check mentions first since it's quick and synchronous + if (allowMentions) { + const isMentioned = root?.facets?.some((facet) => { + return facet.features.some((item) => isMention(item) && item.did === did) + }) + if (isMentioned) { + return false + } + } + + // check follows and list containment + if (!allowFollowing && !allowListUris.length) { + return true + } + const { ref } = db.dynamic + const nullResult = sql`${null}` + const check = await db + .selectFrom(valuesList([did]).as(sql`subject (did)`)) + .select([ + allowFollowing + ? db + .selectFrom('follow') + .where('creator', '=', owner) + .whereRef('subjectDid', '=', ref('subject.did')) + .select('creator') + .as('isFollowed') + : nullResult.as('isFollowed'), + allowListUris.length + ? db + .selectFrom('list_item') + .where('list_item.listUri', 'in', allowListUris) + .whereRef('list_item.subjectDid', '=', ref('subject.did')) + .limit(1) + .select('listUri') + .as('isInList') + : nullResult.as('isInList'), + ]) + .executeTakeFirst() + + if (allowFollowing && check?.isFollowed) { + return false + } + if (allowListUris.length && check?.isInList) { + return false + } + + return true +} + +export const postToThreadgateUri = (postUri: string) => { + const gateUri = new AtUri(postUri) + gateUri.collection = ids.AppBskyFeedThreadgate + return gateUri.toString() +} + +export const threadgateToPostUri = (gateUri: string) => { + const postUri = new AtUri(gateUri) + postUri.collection = ids.AppBskyFeedPost + return postUri.toString() +} diff --git a/packages/bsky/src/services/feed/views.ts b/packages/bsky/src/services/feed/views.ts index 439e68f3d1f..dc5878db6cd 100644 --- a/packages/bsky/src/services/feed/views.ts +++ b/packages/bsky/src/services/feed/views.ts @@ -1,9 +1,11 @@ +import { mapDefined } from '@atproto/common' import { Database } from '../../db' import { FeedViewPost, GeneratorView, PostView, } from '../../lexicon/types/app/bsky/feed/defs' +import { isListRule } from '../../lexicon/types/app/bsky/feed/threadgate' import { Main as EmbedImages, isMain as isEmbedImages, @@ -29,11 +31,14 @@ import { RecordEmbedViewRecord, PostBlocksMap, FeedHydrationState, + ThreadgateInfoMap, + ThreadgateInfo, } from './types' import { Labels, getSelfLabels } from '../label' import { ImageUriBuilder } from '../../image/uri' import { LabelCache } from '../../label-cache' import { ActorInfoMap, ActorService } from '../actor' +import { ListInfoMap, GraphService } from '../graph' export class FeedViews { constructor( @@ -48,6 +53,7 @@ export class FeedViews { services = { actor: ActorService.creator(this.imgUriBuilder, this.labelCache)(this.db), + graph: GraphService.creator(this.imgUriBuilder)(this.db), } formatFeedGeneratorView( @@ -90,7 +96,8 @@ export class FeedViews { usePostViewUnion?: boolean }, ): FeedViewPost[] { - const { posts, profiles, blocks, embeds, labels } = state + const { posts, threadgates, profiles, blocks, embeds, labels, lists } = + state const actors = this.services.actor.views.profileBasicPresentation( Object.keys(profiles), state, @@ -98,12 +105,15 @@ export class FeedViews { ) const feed: FeedViewPost[] = [] for (const item of items) { + const info = posts[item.postUri] const post = this.formatPostView( item.postUri, actors, posts, + threadgates, embeds, labels, + lists, ) // skip over not found & blocked posts if (!post || blocks[post.uri]?.reply) { @@ -123,13 +133,21 @@ export class FeedViews { } } } - if (item.replyParent && item.replyRoot) { + // posts that violate reply-gating may appear in feeds, but without any thread context + if ( + item.replyParent && + item.replyRoot && + !info?.invalidReplyRoot && + !info?.violatesThreadGate + ) { const replyParent = this.formatMaybePostView( item.replyParent, actors, posts, + threadgates, embeds, labels, + lists, blocks, opts, ) @@ -137,8 +155,10 @@ export class FeedViews { item.replyRoot, actors, posts, + threadgates, embeds, labels, + lists, blocks, opts, ) @@ -158,10 +178,13 @@ export class FeedViews { uri: string, actors: ActorInfoMap, posts: PostInfoMap, + threadgates: ThreadgateInfoMap, embeds: PostEmbedViews, labels: Labels, + lists: ListInfoMap, ): PostView | undefined { const post = posts[uri] + const gate = threadgates[uri] const author = actors[post?.creator] if (!post || !author) return undefined const postLabels = labels[uri] ?? [] @@ -187,6 +210,10 @@ export class FeedViews { } : undefined, labels: [...postLabels, ...postSelfLabels], + threadgate: + !post.record.reply && gate + ? this.formatThreadgate(gate, lists) + : undefined, } } @@ -194,14 +221,24 @@ export class FeedViews { uri: string, actors: ActorInfoMap, posts: PostInfoMap, + threadgates: ThreadgateInfoMap, embeds: PostEmbedViews, labels: Labels, + lists: ListInfoMap, blocks: PostBlocksMap, opts?: { usePostViewUnion?: boolean }, ): MaybePostView | undefined { - const post = this.formatPostView(uri, actors, posts, embeds, labels) + const post = this.formatPostView( + uri, + actors, + posts, + threadgates, + embeds, + labels, + lists, + ) if (!post) { if (!opts?.usePostViewUnion) return return this.notFoundPost(uri) @@ -342,4 +379,18 @@ export class FeedViews { media: mediaEmbed, } } + + formatThreadgate(gate: ThreadgateInfo, lists: ListInfoMap) { + return { + uri: gate.uri, + cid: gate.cid, + record: gate.record, + lists: mapDefined(gate.record.allow ?? [], (rule) => { + if (!isListRule(rule)) return + const list = lists[rule.list] + if (!list) return + return this.services.graph.formatListViewBasic(list) + }), + } + } } diff --git a/packages/bsky/src/services/indexing/index.ts b/packages/bsky/src/services/indexing/index.ts index 05e591c92c4..03dce203f36 100644 --- a/packages/bsky/src/services/indexing/index.ts +++ b/packages/bsky/src/services/indexing/index.ts @@ -14,6 +14,7 @@ import { DAY, HOUR } from '@atproto/common' import { ValidationError } from '@atproto/lexicon' import { PrimaryDatabase } from '../../db' import * as Post from './plugins/post' +import * as Threadgate from './plugins/thread-gate' import * as Like from './plugins/like' import * as Repost from './plugins/repost' import * as Follow from './plugins/follow' @@ -34,6 +35,7 @@ import { Actor } from '../../db/tables/actor' export class IndexingService { records: { post: Post.PluginType + threadGate: Threadgate.PluginType like: Like.PluginType repost: Repost.PluginType follow: Follow.PluginType @@ -54,6 +56,7 @@ export class IndexingService { ) { this.records = { post: Post.makePlugin(this.db, backgroundQueue, notifServer), + threadGate: Threadgate.makePlugin(this.db, backgroundQueue, notifServer), like: Like.makePlugin(this.db, backgroundQueue, notifServer), repost: Repost.makePlugin(this.db, backgroundQueue, notifServer), follow: Follow.makePlugin(this.db, backgroundQueue, notifServer), @@ -360,6 +363,10 @@ export class IndexingService { .where('post_embed_record.postUri', 'in', postByUser) .execute() await this.db.db.deleteFrom('post').where('creator', '=', did).execute() + await this.db.db + .deleteFrom('thread_gate') + .where('creator', '=', did) + .execute() // notifications await this.db.db .deleteFrom('notification') diff --git a/packages/bsky/src/services/indexing/plugins/post.ts b/packages/bsky/src/services/indexing/plugins/post.ts index 7ce431fdcd8..f57bc10179b 100644 --- a/packages/bsky/src/services/indexing/plugins/post.ts +++ b/packages/bsky/src/services/indexing/plugins/post.ts @@ -1,7 +1,12 @@ import { Insertable, Selectable, sql } from 'kysely' import { CID } from 'multiformats/cid' import { AtUri } from '@atproto/syntax' -import { Record as PostRecord } from '../../../lexicon/types/app/bsky/feed/post' +import { jsonStringToLex } from '@atproto/lexicon' +import { + Record as PostRecord, + ReplyRef, +} from '../../../lexicon/types/app/bsky/feed/post' +import { Record as GateRecord } from '../../../lexicon/types/app/bsky/feed/threadgate' import { isMain as isEmbedImage } from '../../../lexicon/types/app/bsky/embed/images' import { isMain as isEmbedExternal } from '../../../lexicon/types/app/bsky/embed/external' import { isMain as isEmbedRecord } from '../../../lexicon/types/app/bsky/embed/record' @@ -20,6 +25,8 @@ import { countAll, excluded } from '../../../db/util' import { BackgroundQueue } from '../../../background' import { getAncestorsAndSelfQb, getDescendentsQb } from '../../util/post' import { NotificationServer } from '../../../notifications' +import * as feedutil from '../../feed/util' +import { postToThreadgateUri } from '../../feed/util' type Notif = Insertable type Post = Selectable @@ -96,6 +103,21 @@ const insertFn = async ( return null // Post already indexed } + if (obj.reply) { + const { invalidReplyRoot, violatesThreadGate } = await validateReply( + db, + uri.host, + obj.reply, + ) + if (invalidReplyRoot || violatesThreadGate) { + await db + .updateTable('post') + .where('uri', '=', post.uri) + .set({ invalidReplyRoot, violatesThreadGate }) + .executeTakeFirst() + } + } + const facets = (obj.facets || []) .flatMap((facet) => facet.features) .flatMap((feature) => { @@ -381,3 +403,58 @@ function separateEmbeds(embed: PostRecord['embed']) { } return [embed] } + +async function validateReply( + db: DatabaseSchema, + creator: string, + reply: ReplyRef, +) { + const replyRefs = await getReplyRefs(db, reply) + // check reply + const invalidReplyRoot = + !replyRefs.parent || feedutil.invalidReplyRoot(reply, replyRefs.parent) + // check interaction + const violatesThreadGate = await feedutil.violatesThreadGate( + db, + creator, + new AtUri(reply.root.uri).host, + replyRefs.root?.record ?? null, + replyRefs.gate?.record ?? null, + ) + return { + invalidReplyRoot, + violatesThreadGate, + } +} + +async function getReplyRefs(db: DatabaseSchema, reply: ReplyRef) { + const replyRoot = reply.root.uri + const replyParent = reply.parent.uri + const replyGate = postToThreadgateUri(replyRoot) + const results = await db + .selectFrom('record') + .where('record.uri', 'in', [replyRoot, replyGate, replyParent]) + .leftJoin('post', 'post.uri', 'record.uri') + .selectAll('post') + .select(['record.uri', 'json']) + .execute() + const root = results.find((ref) => ref.uri === replyRoot) + const parent = results.find((ref) => ref.uri === replyParent) + const gate = results.find((ref) => ref.uri === replyGate) + return { + root: root && { + uri: root.uri, + invalidReplyRoot: root.invalidReplyRoot, + record: jsonStringToLex(root.json) as PostRecord, + }, + parent: parent && { + uri: parent.uri, + invalidReplyRoot: parent.invalidReplyRoot, + record: jsonStringToLex(parent.json) as PostRecord, + }, + gate: gate && { + uri: gate.uri, + record: jsonStringToLex(gate.json) as GateRecord, + }, + } +} diff --git a/packages/bsky/src/services/indexing/plugins/thread-gate.ts b/packages/bsky/src/services/indexing/plugins/thread-gate.ts new file mode 100644 index 00000000000..fb0928f2459 --- /dev/null +++ b/packages/bsky/src/services/indexing/plugins/thread-gate.ts @@ -0,0 +1,95 @@ +import { AtUri } from '@atproto/syntax' +import { InvalidRequestError } from '@atproto/xrpc-server' +import { CID } from 'multiformats/cid' +import * as Threadgate from '../../../lexicon/types/app/bsky/feed/threadgate' +import * as lex from '../../../lexicon/lexicons' +import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' +import RecordProcessor from '../processor' +import { toSimplifiedISOSafe } from '../util' +import { PrimaryDatabase } from '../../../db' +import { BackgroundQueue } from '../../../background' +import { NotificationServer } from '../../../notifications' + +const lexId = lex.ids.AppBskyFeedThreadgate +type IndexedGate = DatabaseSchemaType['thread_gate'] + +const insertFn = async ( + db: DatabaseSchema, + uri: AtUri, + cid: CID, + obj: Threadgate.Record, + timestamp: string, +): Promise => { + const postUri = new AtUri(obj.post) + if (postUri.host !== uri.host || postUri.rkey !== uri.rkey) { + throw new InvalidRequestError( + 'Creator and rkey of thread gate does not match its post', + ) + } + const inserted = await db + .insertInto('thread_gate') + .values({ + uri: uri.toString(), + cid: cid.toString(), + creator: uri.host, + postUri: obj.post, + createdAt: toSimplifiedISOSafe(obj.createdAt), + indexedAt: timestamp, + }) + .onConflict((oc) => oc.doNothing()) + .returningAll() + .executeTakeFirst() + return inserted || null +} + +const findDuplicate = async ( + db: DatabaseSchema, + _uri: AtUri, + obj: Threadgate.Record, +): Promise => { + const found = await db + .selectFrom('thread_gate') + .where('postUri', '=', obj.post) + .selectAll() + .executeTakeFirst() + return found ? new AtUri(found.uri) : null +} + +const notifsForInsert = () => { + return [] +} + +const deleteFn = async ( + db: DatabaseSchema, + uri: AtUri, +): Promise => { + const deleted = await db + .deleteFrom('thread_gate') + .where('uri', '=', uri.toString()) + .returningAll() + .executeTakeFirst() + return deleted || null +} + +const notifsForDelete = () => { + return { notifs: [], toDelete: [] } +} + +export type PluginType = RecordProcessor + +export const makePlugin = ( + db: PrimaryDatabase, + backgroundQueue: BackgroundQueue, + notifServer?: NotificationServer, +): PluginType => { + return new RecordProcessor(db, backgroundQueue, notifServer, { + lexId, + insertFn, + findDuplicate, + deleteFn, + notifsForInsert, + notifsForDelete, + }) +} + +export default makePlugin diff --git a/packages/bsky/tests/__snapshots__/indexing.test.ts.snap b/packages/bsky/tests/__snapshots__/indexing.test.ts.snap index 9abbe8a3f64..f7ccc4e688a 100644 --- a/packages/bsky/tests/__snapshots__/indexing.test.ts.snap +++ b/packages/bsky/tests/__snapshots__/indexing.test.ts.snap @@ -518,6 +518,9 @@ Object { "viewer": Object {}, }, "replies": Array [], + "viewer": Object { + "canReply": true, + }, }, } `; @@ -584,6 +587,9 @@ Object { "viewer": Object {}, }, "replies": Array [], + "viewer": Object { + "canReply": true, + }, }, } `; diff --git a/packages/bsky/tests/_util.ts b/packages/bsky/tests/_util.ts index 4f08af9e0f6..8d39a0f9c2c 100644 --- a/packages/bsky/tests/_util.ts +++ b/packages/bsky/tests/_util.ts @@ -180,6 +180,7 @@ export const stripViewerFromPost = (postUnknown: unknown): PostView => { // @NOTE mutates export const stripViewerFromThread = (thread: T): T => { if (!isThreadViewPost(thread)) return thread + delete thread.viewer thread.post = stripViewerFromPost(thread.post) if (isThreadViewPost(thread.parent)) { thread.parent = stripViewerFromThread(thread.parent) diff --git a/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap b/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap index fae6e7f4fa9..009095947c2 100644 --- a/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap @@ -126,6 +126,9 @@ Object { "uri": "record(0)", "viewer": Object {}, }, + "viewer": Object { + "canReply": true, + }, }, } `; @@ -201,6 +204,9 @@ Object { "viewer": Object {}, }, "replies": Array [], + "viewer": Object { + "canReply": true, + }, }, } `; @@ -282,6 +288,9 @@ Object { "uri": "record(7)", }, ], + "viewer": Object { + "canReply": true, + }, }, } `; diff --git a/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap b/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap index 086f6e10d4d..5ee901c65d8 100644 --- a/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap @@ -126,6 +126,9 @@ Object { "uri": "record(0)", "viewer": Object {}, }, + "viewer": Object { + "canReply": true, + }, }, } `; @@ -201,6 +204,9 @@ Object { "viewer": Object {}, }, "replies": Array [], + "viewer": Object { + "canReply": true, + }, }, } `; @@ -353,6 +359,9 @@ Object { }, }, ], + "viewer": Object { + "canReply": true, + }, }, } `; diff --git a/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap b/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap index 0a081f91292..2585a96ec42 100644 --- a/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap @@ -292,6 +292,9 @@ Object { }, }, ], + "viewer": Object { + "canReply": true, + }, } `; diff --git a/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap b/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap index d6836a810a5..fb0eb1fc5d1 100644 --- a/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap @@ -269,5 +269,8 @@ Object { }, }, ], + "viewer": Object { + "canReply": true, + }, } `; diff --git a/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap b/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap index 5886ed56019..4cdd3555805 100644 --- a/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap @@ -195,6 +195,9 @@ Object { }, }, "replies": Array [], + "viewer": Object { + "canReply": true, + }, } `; @@ -434,6 +437,9 @@ Object { ], }, ], + "viewer": Object { + "canReply": true, + }, } `; @@ -609,6 +615,9 @@ Object { }, }, ], + "viewer": Object { + "canReply": true, + }, } `; @@ -762,6 +771,9 @@ Object { ], }, ], + "viewer": Object { + "canReply": true, + }, } `; @@ -814,6 +826,9 @@ Object { "viewer": Object {}, }, "replies": Array [], + "viewer": Object { + "canReply": true, + }, } `; @@ -881,6 +896,9 @@ Object { "viewer": Object {}, }, "replies": Array [], + "viewer": Object { + "canReply": true, + }, } `; @@ -950,6 +968,9 @@ Object { }, }, "replies": Array [], + "viewer": Object { + "canReply": true, + }, } `; @@ -1019,6 +1040,9 @@ Object { }, }, "replies": Array [], + "viewer": Object { + "canReply": true, + }, } `; @@ -1219,6 +1243,9 @@ Object { ], }, ], + "viewer": Object { + "canReply": true, + }, } `; @@ -1357,5 +1384,8 @@ Object { "replies": Array [], }, ], + "viewer": Object { + "canReply": true, + }, } `; diff --git a/packages/bsky/tests/views/__snapshots__/threadgating.test.ts.snap b/packages/bsky/tests/views/__snapshots__/threadgating.test.ts.snap new file mode 100644 index 00000000000..1545c19e9f4 --- /dev/null +++ b/packages/bsky/tests/views/__snapshots__/threadgating.test.ts.snap @@ -0,0 +1,164 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`views with thread gating applies gate after root post is deleted. 1`] = `undefined`; + +exports[`views with thread gating applies gate for empty rules. 1`] = ` +Object { + "cid": "cids(0)", + "lists": Array [], + "record": Object { + "$type": "app.bsky.feed.threadgate", + "allow": Array [], + "createdAt": "1970-01-01T00:00:00.000Z", + "post": "record(1)", + }, + "uri": "record(0)", +} +`; + +exports[`views with thread gating applies gate for following rule. 1`] = ` +Object { + "cid": "cids(0)", + "lists": Array [], + "record": Object { + "$type": "app.bsky.feed.threadgate", + "allow": Array [ + Object { + "$type": "app.bsky.feed.threadgate#followingRule", + }, + ], + "createdAt": "1970-01-01T00:00:00.000Z", + "post": "record(1)", + }, + "uri": "record(0)", +} +`; + +exports[`views with thread gating applies gate for list rule. 1`] = ` +Object { + "cid": "cids(0)", + "lists": Array [ + Object { + "cid": "cids(1)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "name": "list a", + "purpose": "app.bsky.graph.defs#modlist", + "uri": "record(2)", + "viewer": Object { + "muted": false, + }, + }, + Object { + "cid": "cids(2)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "name": "list b", + "purpose": "app.bsky.graph.defs#modlist", + "uri": "record(3)", + "viewer": Object { + "muted": false, + }, + }, + ], + "record": Object { + "$type": "app.bsky.feed.threadgate", + "allow": Array [ + Object { + "$type": "app.bsky.feed.threadgate#listRule", + "list": "record(2)", + }, + Object { + "$type": "app.bsky.feed.threadgate#listRule", + "list": "record(3)", + }, + ], + "createdAt": "1970-01-01T00:00:00.000Z", + "post": "record(1)", + }, + "uri": "record(0)", +} +`; + +exports[`views with thread gating applies gate for mention rule. 1`] = ` +Object { + "cid": "cids(0)", + "lists": Array [], + "record": Object { + "$type": "app.bsky.feed.threadgate", + "allow": Array [ + Object { + "$type": "app.bsky.feed.threadgate#mentionRule", + }, + ], + "createdAt": "1970-01-01T00:00:00.000Z", + "post": "record(1)", + }, + "uri": "record(0)", +} +`; + +exports[`views with thread gating applies gate for missing rules, takes no action. 1`] = ` +Object { + "cid": "cids(0)", + "lists": Array [], + "record": Object { + "$type": "app.bsky.feed.threadgate", + "createdAt": "1970-01-01T00:00:00.000Z", + "post": "record(1)", + }, + "uri": "record(0)", +} +`; + +exports[`views with thread gating applies gate for multiple rules. 1`] = ` +Object { + "cid": "cids(0)", + "lists": Array [], + "record": Object { + "$type": "app.bsky.feed.threadgate", + "allow": Array [ + Object { + "$type": "app.bsky.feed.threadgate#mentionRule", + }, + Object { + "$type": "app.bsky.feed.threadgate#followingRule", + }, + ], + "createdAt": "1970-01-01T00:00:00.000Z", + "post": "record(1)", + }, + "uri": "record(0)", +} +`; + +exports[`views with thread gating applies gate for unknown list rule. 1`] = ` +Object { + "cid": "cids(0)", + "lists": Array [], + "record": Object { + "$type": "app.bsky.feed.threadgate", + "allow": Array [ + Object { + "$type": "app.bsky.feed.threadgate#listRule", + "list": "record(1)", + }, + ], + "createdAt": "1970-01-01T00:00:00.000Z", + "post": "record(1)", + }, + "uri": "record(0)", +} +`; + +exports[`views with thread gating does not apply gate to original poster. 1`] = ` +Object { + "cid": "cids(0)", + "lists": Array [], + "record": Object { + "$type": "app.bsky.feed.threadgate", + "allow": Array [], + "createdAt": "1970-01-01T00:00:00.000Z", + "post": "record(1)", + }, + "uri": "record(0)", +} +`; diff --git a/packages/bsky/tests/views/threadgating.test.ts b/packages/bsky/tests/views/threadgating.test.ts new file mode 100644 index 00000000000..7d29addfcf5 --- /dev/null +++ b/packages/bsky/tests/views/threadgating.test.ts @@ -0,0 +1,571 @@ +import assert from 'assert' +import AtpAgent from '@atproto/api' +import { TestNetwork } from '@atproto/dev-env' +import { + isNotFoundPost, + isThreadViewPost, +} from '../../src/lexicon/types/app/bsky/feed/defs' +import { SeedClient } from '../seeds/client' +import basicSeed from '../seeds/basic' +import { forSnapshot } from '../_util' + +describe('views with thread gating', () => { + let network: TestNetwork + let agent: AtpAgent + let pdsAgent: AtpAgent + let sc: SeedClient + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'bsky_views_thread_gating', + }) + agent = network.bsky.getClient() + pdsAgent = network.pds.getClient() + sc = new SeedClient(pdsAgent) + await basicSeed(sc) + await network.processAll() + }) + + afterAll(async () => { + await network.close() + }) + + it('applies gate for empty rules.', async () => { + const post = await sc.post(sc.dids.carol, 'empty rules') + await pdsAgent.api.app.bsky.feed.threadgate.create( + { repo: sc.dids.carol, rkey: post.ref.uri.rkey }, + { post: post.ref.uriStr, createdAt: iso(), allow: [] }, + sc.getHeaders(sc.dids.carol), + ) + await sc.reply(sc.dids.alice, post.ref, post.ref, 'empty rules reply') + await network.processAll() + const { + data: { thread }, + } = await agent.api.app.bsky.feed.getPostThread( + { uri: post.ref.uriStr }, + { headers: await network.serviceHeaders(sc.dids.alice) }, + ) + assert(isThreadViewPost(thread)) + expect(forSnapshot(thread.post.threadgate)).toMatchSnapshot() + expect(thread.viewer).toEqual({ canReply: false }) + expect(thread.replies?.length).toEqual(0) + }) + + it('applies gate for mention rule.', async () => { + const post = await sc.post( + sc.dids.carol, + 'mention rules @carol.test @dan.test', + [ + { + index: { byteStart: 14, byteEnd: 25 }, + features: [ + { $type: 'app.bsky.richtext.facet#mention', did: sc.dids.carol }, + ], + }, + { + index: { byteStart: 26, byteEnd: 35 }, + features: [ + { $type: 'app.bsky.richtext.facet#mention', did: sc.dids.dan }, + ], + }, + ], + ) + await pdsAgent.api.app.bsky.feed.threadgate.create( + { repo: sc.dids.carol, rkey: post.ref.uri.rkey }, + { + post: post.ref.uriStr, + createdAt: iso(), + allow: [{ $type: 'app.bsky.feed.threadgate#mentionRule' }], + }, + sc.getHeaders(sc.dids.carol), + ) + await sc.reply( + sc.dids.alice, + post.ref, + post.ref, + 'mention rule reply disallow', + ) + const danReply = await sc.reply( + sc.dids.dan, + post.ref, + post.ref, + 'mention rule reply allow', + ) + await network.processAll() + const { + data: { thread: aliceThread }, + } = await agent.api.app.bsky.feed.getPostThread( + { uri: post.ref.uriStr }, + { headers: await network.serviceHeaders(sc.dids.alice) }, + ) + assert(isThreadViewPost(aliceThread)) + expect(aliceThread.viewer).toEqual({ canReply: false }) + const { + data: { thread: danThread }, + } = await agent.api.app.bsky.feed.getPostThread( + { uri: post.ref.uriStr }, + { headers: await network.serviceHeaders(sc.dids.dan) }, + ) + assert(isThreadViewPost(danThread)) + expect(forSnapshot(danThread.post.threadgate)).toMatchSnapshot() + expect(danThread.viewer).toEqual({ canReply: true }) + const [reply, ...otherReplies] = danThread.replies ?? [] + assert(isThreadViewPost(reply)) + expect(otherReplies.length).toEqual(0) + expect(reply.post.uri).toEqual(danReply.ref.uriStr) + }) + + it('applies gate for following rule.', async () => { + const post = await sc.post(sc.dids.carol, 'following rule') + await pdsAgent.api.app.bsky.feed.threadgate.create( + { repo: sc.dids.carol, rkey: post.ref.uri.rkey }, + { + post: post.ref.uriStr, + createdAt: iso(), + allow: [{ $type: 'app.bsky.feed.threadgate#followingRule' }], + }, + sc.getHeaders(sc.dids.carol), + ) + // carol only follows alice + await sc.reply( + sc.dids.dan, + post.ref, + post.ref, + 'following rule reply disallow', + ) + const aliceReply = await sc.reply( + sc.dids.alice, + post.ref, + post.ref, + 'following rule reply allow', + ) + await network.processAll() + const { + data: { thread: danThread }, + } = await agent.api.app.bsky.feed.getPostThread( + { uri: post.ref.uriStr }, + { headers: await network.serviceHeaders(sc.dids.dan) }, + ) + assert(isThreadViewPost(danThread)) + expect(danThread.viewer).toEqual({ canReply: false }) + const { + data: { thread: aliceThread }, + } = await agent.api.app.bsky.feed.getPostThread( + { uri: post.ref.uriStr }, + { headers: await network.serviceHeaders(sc.dids.alice) }, + ) + assert(isThreadViewPost(aliceThread)) + expect(forSnapshot(aliceThread.post.threadgate)).toMatchSnapshot() + expect(aliceThread.viewer).toEqual({ canReply: true }) + const [reply, ...otherReplies] = aliceThread.replies ?? [] + assert(isThreadViewPost(reply)) + expect(otherReplies.length).toEqual(0) + expect(reply.post.uri).toEqual(aliceReply.ref.uriStr) + }) + + it('applies gate for list rule.', async () => { + const post = await sc.post(sc.dids.carol, 'following rule') + // setup lists to allow alice and dan + const listA = await pdsAgent.api.app.bsky.graph.list.create( + { repo: sc.dids.carol }, + { + name: 'list a', + purpose: 'app.bsky.graph.defs#modlist', + createdAt: iso(), + }, + sc.getHeaders(sc.dids.carol), + ) + await pdsAgent.api.app.bsky.graph.listitem.create( + { repo: sc.dids.carol }, + { + list: listA.uri, + subject: sc.dids.alice, + createdAt: iso(), + }, + sc.getHeaders(sc.dids.carol), + ) + const listB = await pdsAgent.api.app.bsky.graph.list.create( + { repo: sc.dids.carol }, + { + name: 'list b', + purpose: 'app.bsky.graph.defs#modlist', + createdAt: iso(), + }, + sc.getHeaders(sc.dids.carol), + ) + await pdsAgent.api.app.bsky.graph.listitem.create( + { repo: sc.dids.carol }, + { + list: listB.uri, + subject: sc.dids.dan, + createdAt: iso(), + }, + sc.getHeaders(sc.dids.carol), + ) + await pdsAgent.api.app.bsky.feed.threadgate.create( + { repo: sc.dids.carol, rkey: post.ref.uri.rkey }, + { + post: post.ref.uriStr, + createdAt: iso(), + allow: [ + { $type: 'app.bsky.feed.threadgate#listRule', list: listA.uri }, + { $type: 'app.bsky.feed.threadgate#listRule', list: listB.uri }, + ], + }, + sc.getHeaders(sc.dids.carol), + ) + // + await sc.reply(sc.dids.bob, post.ref, post.ref, 'list rule reply disallow') + const aliceReply = await sc.reply( + sc.dids.alice, + post.ref, + post.ref, + 'list rule reply allow (list a)', + ) + const danReply = await sc.reply( + sc.dids.dan, + post.ref, + post.ref, + 'list rule reply allow (list b)', + ) + await network.processAll() + const { + data: { thread: bobThread }, + } = await agent.api.app.bsky.feed.getPostThread( + { uri: post.ref.uriStr }, + { headers: await network.serviceHeaders(sc.dids.bob) }, + ) + assert(isThreadViewPost(bobThread)) + expect(bobThread.viewer).toEqual({ canReply: false }) + const { + data: { thread: aliceThread }, + } = await agent.api.app.bsky.feed.getPostThread( + { uri: post.ref.uriStr }, + { headers: await network.serviceHeaders(sc.dids.alice) }, + ) + assert(isThreadViewPost(aliceThread)) + expect(aliceThread.viewer).toEqual({ canReply: true }) + const { + data: { thread: danThread }, + } = await agent.api.app.bsky.feed.getPostThread( + { uri: post.ref.uriStr }, + { headers: await network.serviceHeaders(sc.dids.dan) }, + ) + assert(isThreadViewPost(danThread)) + expect(forSnapshot(danThread.post.threadgate)).toMatchSnapshot() + expect(danThread.viewer).toEqual({ canReply: true }) + const [reply1, reply2, ...otherReplies] = aliceThread.replies ?? [] + assert(isThreadViewPost(reply1)) + assert(isThreadViewPost(reply2)) + expect(otherReplies.length).toEqual(0) + expect(reply1.post.uri).toEqual(danReply.ref.uriStr) + expect(reply2.post.uri).toEqual(aliceReply.ref.uriStr) + }) + + it('applies gate for unknown list rule.', async () => { + const post = await sc.post(sc.dids.carol, 'unknown list rules') + await pdsAgent.api.app.bsky.feed.threadgate.create( + { repo: sc.dids.carol, rkey: post.ref.uri.rkey }, + { + post: post.ref.uriStr, + createdAt: iso(), + allow: [ + { + $type: 'app.bsky.feed.threadgate#listRule', + list: post.ref.uriStr, // bad list link, references a post + }, + ], + }, + sc.getHeaders(sc.dids.carol), + ) + await sc.reply( + sc.dids.alice, + post.ref, + post.ref, + 'unknown list rules reply', + ) + await network.processAll() + const { + data: { thread }, + } = await agent.api.app.bsky.feed.getPostThread( + { uri: post.ref.uriStr }, + { headers: await network.serviceHeaders(sc.dids.alice) }, + ) + assert(isThreadViewPost(thread)) + expect(forSnapshot(thread.post.threadgate)).toMatchSnapshot() + expect(thread.viewer).toEqual({ canReply: false }) + expect(thread.replies?.length).toEqual(0) + }) + + it('applies gate for multiple rules.', async () => { + const post = await sc.post(sc.dids.carol, 'multi rules @dan.test', [ + { + index: { byteStart: 12, byteEnd: 21 }, + features: [ + { $type: 'app.bsky.richtext.facet#mention', did: sc.dids.dan }, + ], + }, + ]) + await pdsAgent.api.app.bsky.feed.threadgate.create( + { repo: sc.dids.carol, rkey: post.ref.uri.rkey }, + { + post: post.ref.uriStr, + createdAt: iso(), + allow: [ + { $type: 'app.bsky.feed.threadgate#mentionRule' }, + { $type: 'app.bsky.feed.threadgate#followingRule' }, + ], + }, + sc.getHeaders(sc.dids.carol), + ) + // carol only follows alice, and the post mentions dan. + await sc.reply(sc.dids.bob, post.ref, post.ref, 'multi rule reply disallow') + const aliceReply = await sc.reply( + sc.dids.alice, + post.ref, + post.ref, + 'multi rule reply allow (following)', + ) + const danReply = await sc.reply( + sc.dids.dan, + post.ref, + post.ref, + 'multi rule reply allow (mention)', + ) + await network.processAll() + const { + data: { thread: bobThread }, + } = await agent.api.app.bsky.feed.getPostThread( + { uri: post.ref.uriStr }, + { headers: await network.serviceHeaders(sc.dids.bob) }, + ) + assert(isThreadViewPost(bobThread)) + expect(bobThread.viewer).toEqual({ canReply: false }) + const { + data: { thread: aliceThread }, + } = await agent.api.app.bsky.feed.getPostThread( + { uri: post.ref.uriStr }, + { headers: await network.serviceHeaders(sc.dids.alice) }, + ) + assert(isThreadViewPost(aliceThread)) + expect(aliceThread.viewer).toEqual({ canReply: true }) + const { + data: { thread: danThread }, + } = await agent.api.app.bsky.feed.getPostThread( + { uri: post.ref.uriStr }, + { headers: await network.serviceHeaders(sc.dids.dan) }, + ) + assert(isThreadViewPost(danThread)) + expect(forSnapshot(danThread.post.threadgate)).toMatchSnapshot() + expect(danThread.viewer).toEqual({ canReply: true }) + const [reply1, reply2, ...otherReplies] = aliceThread.replies ?? [] + assert(isThreadViewPost(reply1)) + assert(isThreadViewPost(reply2)) + expect(otherReplies.length).toEqual(0) + expect(reply1.post.uri).toEqual(danReply.ref.uriStr) + expect(reply2.post.uri).toEqual(aliceReply.ref.uriStr) + }) + + it('applies gate for missing rules, takes no action.', async () => { + const post = await sc.post(sc.dids.carol, 'missing rules') + await pdsAgent.api.app.bsky.feed.threadgate.create( + { repo: sc.dids.carol, rkey: post.ref.uri.rkey }, + { post: post.ref.uriStr, createdAt: iso() }, + sc.getHeaders(sc.dids.carol), + ) + const aliceReply = await sc.reply( + sc.dids.alice, + post.ref, + post.ref, + 'missing rules reply', + ) + await network.processAll() + const { + data: { thread }, + } = await agent.api.app.bsky.feed.getPostThread( + { uri: post.ref.uriStr }, + { headers: await network.serviceHeaders(sc.dids.alice) }, + ) + assert(isThreadViewPost(thread)) + expect(forSnapshot(thread.post.threadgate)).toMatchSnapshot() + expect(thread.viewer).toEqual({ canReply: true }) + const [reply, ...otherReplies] = thread.replies ?? [] + assert(isThreadViewPost(reply)) + expect(otherReplies.length).toEqual(0) + expect(reply.post.uri).toEqual(aliceReply.ref.uriStr) + }) + + it('applies gate after root post is deleted.', async () => { + // @NOTE also covers rule application more than one level deep + const post = await sc.post(sc.dids.carol, 'following rule w/ post deletion') + await pdsAgent.api.app.bsky.feed.threadgate.create( + { repo: sc.dids.carol, rkey: post.ref.uri.rkey }, + { + post: post.ref.uriStr, + createdAt: iso(), + allow: [{ $type: 'app.bsky.feed.threadgate#followingRule' }], + }, + sc.getHeaders(sc.dids.carol), + ) + // carol only follows alice + const orphanedReply = await sc.reply( + sc.dids.alice, + post.ref, + post.ref, + 'following rule reply allow', + ) + await pdsAgent.api.app.bsky.feed.post.delete( + { repo: sc.dids.carol, rkey: post.ref.uri.rkey }, + sc.getHeaders(sc.dids.carol), + ) + await network.processAll() + await sc.reply( + sc.dids.dan, + post.ref, + orphanedReply.ref, + 'following rule reply disallow', + ) + const aliceReply = await sc.reply( + sc.dids.alice, + post.ref, + orphanedReply.ref, + 'following rule reply allow', + ) + await network.processAll() + const { + data: { thread: danThread }, + } = await agent.api.app.bsky.feed.getPostThread( + { uri: orphanedReply.ref.uriStr }, + { headers: await network.serviceHeaders(sc.dids.dan) }, + ) + assert(isThreadViewPost(danThread)) + expect(danThread.viewer).toEqual({ canReply: false }) + const { + data: { thread: aliceThread }, + } = await agent.api.app.bsky.feed.getPostThread( + { uri: orphanedReply.ref.uriStr }, + { headers: await network.serviceHeaders(sc.dids.alice) }, + ) + assert(isThreadViewPost(aliceThread)) + assert( + isNotFoundPost(aliceThread.parent) && + aliceThread.parent.uri === post.ref.uriStr, + ) + expect(aliceThread.post.threadgate).toMatchSnapshot() + expect(aliceThread.viewer).toEqual({ canReply: true }) + const [reply, ...otherReplies] = aliceThread.replies ?? [] + assert(isThreadViewPost(reply)) + expect(otherReplies.length).toEqual(0) + expect(reply.post.uri).toEqual(aliceReply.ref.uriStr) + }) + + it('does not apply gate to original poster.', async () => { + const post = await sc.post(sc.dids.carol, 'empty rules') + await pdsAgent.api.app.bsky.feed.threadgate.create( + { repo: sc.dids.carol, rkey: post.ref.uri.rkey }, + { post: post.ref.uriStr, createdAt: iso(), allow: [] }, + sc.getHeaders(sc.dids.carol), + ) + const selfReply = await sc.reply( + sc.dids.carol, + post.ref, + post.ref, + 'empty rules reply allow', + ) + await network.processAll() + const { + data: { thread }, + } = await agent.api.app.bsky.feed.getPostThread( + { uri: post.ref.uriStr }, + { headers: await network.serviceHeaders(sc.dids.carol) }, + ) + assert(isThreadViewPost(thread)) + expect(forSnapshot(thread.post.threadgate)).toMatchSnapshot() + expect(thread.viewer).toEqual({ canReply: true }) + const [reply, ...otherReplies] = thread.replies ?? [] + assert(isThreadViewPost(reply)) + expect(otherReplies.length).toEqual(0) + expect(reply.post.uri).toEqual(selfReply.ref.uriStr) + }) + + it('displays gated posts in feed and thread anchor without reply context.', async () => { + const post = await sc.post(sc.dids.carol, 'following rule') + await pdsAgent.api.app.bsky.feed.threadgate.create( + { repo: sc.dids.carol, rkey: post.ref.uri.rkey }, + { + post: post.ref.uriStr, + createdAt: iso(), + allow: [{ $type: 'app.bsky.feed.threadgate#followingRule' }], + }, + sc.getHeaders(sc.dids.carol), + ) + // carol only follows alice + const badReply = await sc.reply( + sc.dids.dan, + post.ref, + post.ref, + 'following rule reply disallow', + ) + // going to ensure this one doesn't appear in badReply's thread + await sc.reply(sc.dids.alice, post.ref, badReply.ref, 'reply to disallowed') + await network.processAll() + // check thread view + const { + data: { thread }, + } = await agent.api.app.bsky.feed.getPostThread( + { uri: badReply.ref.uriStr }, + { headers: await network.serviceHeaders(sc.dids.alice) }, + ) + assert(isThreadViewPost(thread)) + expect(thread.viewer).toEqual({ canReply: false }) // nobody can reply to this, not even alice. + expect(thread.replies).toBeUndefined() + expect(thread.parent).toBeUndefined() + expect(thread.post.threadgate).toBeUndefined() + // check feed view + const { + data: { feed }, + } = await agent.api.app.bsky.feed.getAuthorFeed( + { actor: sc.dids.dan }, + { headers: await network.serviceHeaders(sc.dids.alice) }, + ) + const [feedItem] = feed + expect(feedItem.post.uri).toEqual(badReply.ref.uriStr) + expect(feedItem.post.threadgate).toBeUndefined() + expect(feedItem.reply).toBeUndefined() + }) + + it('does not apply gate unless it matches post rkey.', async () => { + const postA = await sc.post(sc.dids.carol, 'ungated a') + const postB = await sc.post(sc.dids.carol, 'ungated b') + await pdsAgent.api.app.bsky.feed.threadgate.create( + { repo: sc.dids.carol, rkey: postA.ref.uri.rkey }, + { post: postB.ref.uriStr, createdAt: iso(), allow: [] }, + sc.getHeaders(sc.dids.carol), + ) + await sc.reply(sc.dids.alice, postA.ref, postA.ref, 'ungated reply') + await sc.reply(sc.dids.alice, postB.ref, postB.ref, 'ungated reply') + await network.processAll() + const { + data: { thread: threadA }, + } = await agent.api.app.bsky.feed.getPostThread( + { uri: postA.ref.uriStr }, + { headers: await network.serviceHeaders(sc.dids.alice) }, + ) + assert(isThreadViewPost(threadA)) + expect(threadA.post.threadgate).toBeUndefined() + expect(threadA.viewer).toEqual({ canReply: true }) + expect(threadA.replies?.length).toEqual(1) + const { + data: { thread: threadB }, + } = await agent.api.app.bsky.feed.getPostThread( + { uri: postB.ref.uriStr }, + { headers: await network.serviceHeaders(sc.dids.alice) }, + ) + assert(isThreadViewPost(threadB)) + expect(threadB.post.threadgate).toBeUndefined() + expect(threadB.viewer).toEqual({ canReply: true }) + expect(threadB.replies?.length).toEqual(1) + }) +}) + +const iso = (date = new Date()) => date.toISOString() diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index f1fd2519d74..2ca983aec4b 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -4390,6 +4390,10 @@ export const schemaDict = { ref: 'lex:com.atproto.label.defs#label', }, }, + threadgate: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#threadgateView', + }, }, }, viewerState: { @@ -4486,6 +4490,10 @@ export const schemaDict = { ], }, }, + viewer: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#viewerThreadState', + }, }, }, notFoundPost: { @@ -4534,6 +4542,14 @@ export const schemaDict = { }, }, }, + viewerThreadState: { + type: 'object', + properties: { + canReply: { + type: 'boolean', + }, + }, + }, generatorView: { type: 'object', required: ['uri', 'cid', 'did', 'creator', 'displayName', 'indexedAt'], @@ -4619,6 +4635,29 @@ export const schemaDict = { }, }, }, + threadgateView: { + type: 'object', + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + record: { + type: 'unknown', + }, + lists: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.graph.defs#listViewBasic', + }, + }, + }, + }, }, }, AppBskyFeedDescribeFeedGenerator: { @@ -5615,6 +5654,63 @@ export const schemaDict = { }, }, }, + AppBskyFeedThreadgate: { + lexicon: 1, + id: 'app.bsky.feed.threadgate', + defs: { + main: { + type: 'record', + key: 'tid', + description: + "Defines interaction gating rules for a thread. The rkey of the threadgate record should match the rkey of the thread's root post.", + record: { + type: 'object', + required: ['post', 'createdAt'], + properties: { + post: { + type: 'string', + format: 'at-uri', + }, + allow: { + type: 'array', + maxLength: 5, + items: { + type: 'union', + refs: [ + 'lex:app.bsky.feed.threadgate#mentionRule', + 'lex:app.bsky.feed.threadgate#followingRule', + 'lex:app.bsky.feed.threadgate#listRule', + ], + }, + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + mentionRule: { + type: 'object', + description: 'Allow replies from actors mentioned in your post.', + }, + followingRule: { + type: 'object', + description: 'Allow replies from actors you follow.', + }, + listRule: { + type: 'object', + description: 'Allow replies from actors on a list.', + required: ['list'], + properties: { + list: { + type: 'string', + format: 'at-uri', + }, + }, + }, + }, + }, AppBskyGraphBlock: { lexicon: 1, id: 'app.bsky.graph.block', @@ -6932,6 +7028,7 @@ export const ids = { AppBskyFeedLike: 'app.bsky.feed.like', AppBskyFeedPost: 'app.bsky.feed.post', AppBskyFeedRepost: 'app.bsky.feed.repost', + AppBskyFeedThreadgate: 'app.bsky.feed.threadgate', AppBskyGraphBlock: 'app.bsky.graph.block', AppBskyGraphDefs: 'app.bsky.graph.defs', AppBskyGraphFollow: 'app.bsky.graph.follow', diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/defs.ts b/packages/pds/src/lexicon/types/app/bsky/feed/defs.ts index 463445fbd49..08d34d88ebb 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/defs.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/defs.ts @@ -12,6 +12,7 @@ import * as AppBskyEmbedRecord from '../embed/record' import * as AppBskyEmbedRecordWithMedia from '../embed/recordWithMedia' import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' import * as AppBskyRichtextFacet from '../richtext/facet' +import * as AppBskyGraphDefs from '../graph/defs' export interface PostView { uri: string @@ -30,6 +31,7 @@ export interface PostView { indexedAt: string viewer?: ViewerState labels?: ComAtprotoLabelDefs.Label[] + threadgate?: ThreadgateView [k: string]: unknown } @@ -135,6 +137,7 @@ export interface ThreadViewPost { | BlockedPost | { $type: string; [k: string]: unknown } )[] + viewer?: ViewerThreadState [k: string]: unknown } @@ -205,6 +208,23 @@ export function validateBlockedAuthor(v: unknown): ValidationResult { return lexicons.validate('app.bsky.feed.defs#blockedAuthor', v) } +export interface ViewerThreadState { + canReply?: boolean + [k: string]: unknown +} + +export function isViewerThreadState(v: unknown): v is ViewerThreadState { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.defs#viewerThreadState' + ) +} + +export function validateViewerThreadState(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.defs#viewerThreadState', v) +} + export interface GeneratorView { uri: string cid: string @@ -283,3 +303,23 @@ export function isSkeletonReasonRepost(v: unknown): v is SkeletonReasonRepost { export function validateSkeletonReasonRepost(v: unknown): ValidationResult { return lexicons.validate('app.bsky.feed.defs#skeletonReasonRepost', v) } + +export interface ThreadgateView { + uri?: string + cid?: string + record?: {} + lists?: AppBskyGraphDefs.ListViewBasic[] + [k: string]: unknown +} + +export function isThreadgateView(v: unknown): v is ThreadgateView { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.defs#threadgateView' + ) +} + +export function validateThreadgateView(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.defs#threadgateView', v) +} diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/threadgate.ts b/packages/pds/src/lexicon/types/app/bsky/feed/threadgate.ts new file mode 100644 index 00000000000..51f9f8e9af1 --- /dev/null +++ b/packages/pds/src/lexicon/types/app/bsky/feed/threadgate.ts @@ -0,0 +1,80 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' + +export interface Record { + post: string + allow?: ( + | MentionRule + | FollowingRule + | ListRule + | { $type: string; [k: string]: unknown } + )[] + createdAt: string + [k: string]: unknown +} + +export function isRecord(v: unknown): v is Record { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'app.bsky.feed.threadgate#main' || + v.$type === 'app.bsky.feed.threadgate') + ) +} + +export function validateRecord(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.threadgate#main', v) +} + +/** Allow replies from actors mentioned in your post. */ +export interface MentionRule {} + +export function isMentionRule(v: unknown): v is MentionRule { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.threadgate#mentionRule' + ) +} + +export function validateMentionRule(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.threadgate#mentionRule', v) +} + +/** Allow replies from actors you follow. */ +export interface FollowingRule {} + +export function isFollowingRule(v: unknown): v is FollowingRule { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.threadgate#followingRule' + ) +} + +export function validateFollowingRule(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.threadgate#followingRule', v) +} + +/** Allow replies from actors on a list. */ +export interface ListRule { + list: string + [k: string]: unknown +} + +export function isListRule(v: unknown): v is ListRule { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.threadgate#listRule' + ) +} + +export function validateListRule(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.threadgate#listRule', v) +} diff --git a/packages/pds/src/repo/prepare.ts b/packages/pds/src/repo/prepare.ts index 60fbe2d81cd..581701f1f01 100644 --- a/packages/pds/src/repo/prepare.ts +++ b/packages/pds/src/repo/prepare.ts @@ -1,6 +1,6 @@ import { CID } from 'multiformats/cid' import { AtUri } from '@atproto/syntax' -import { TID, dataToCborBlock } from '@atproto/common' +import { MINUTE, TID, dataToCborBlock } from '@atproto/common' import { LexiconDefNotFoundError, RepoRecord, @@ -155,14 +155,20 @@ export const prepareCreate = async (opts: { if (validate) { assertValidRecord(record) } - if (collection === lex.ids.AppBskyFeedPost && opts.rkey) { - // @TODO temporary + + const nextRkey = TID.next() + if ( + collection === lex.ids.AppBskyFeedPost && + opts.rkey && + !rkeyIsInWindow(nextRkey, new TID(opts.rkey)) + ) { + // @TODO temporary. allowing a window supports creation of post and gate records at the same time. throw new InvalidRequestError( - 'Custom rkeys for post records are not currently supported.', + 'Custom rkeys for post records should be near the present.', ) } - const rkey = opts.rkey || TID.nextStr() + const rkey = opts.rkey || nextRkey.toString() assertNoExplicitSlurs(rkey, record) return { action: WriteOpAction.Create, @@ -298,3 +304,9 @@ function assertNoExplicitSlurs(rkey: string, record: RepoRecord) { throw new InvalidRecordError('Unacceptable slur in record') } } + +// ensures two rkeys are not far apart +function rkeyIsInWindow(rkey1: TID, rkey2: TID) { + const ms = Math.abs(rkey1.timestamp() - rkey2.timestamp()) / 1000 + return ms < 10 * MINUTE +} From cf6002a1bd6b4a8ea79f31cd2416f1d5b58dd72a Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 14 Sep 2023 15:38:27 -0500 Subject: [PATCH 06/46] order by `like.indexedAt` in app view (#1592) * order by like.indexedAt * use keyset for ordering * simplify * ok ok ok I get it now * Update packages/bsky/src/api/app/bsky/feed/getActorLikes.ts Co-authored-by: Daniel Holmgren --------- Co-authored-by: Daniel Holmgren --- packages/bsky/src/api/app/bsky/feed/getActorLikes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts index 73b8b070262..3da1b0d042a 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts @@ -72,7 +72,7 @@ const skeleton = async ( .innerJoin('like', 'like.subject', 'feed_item.uri') .where('like.creator', '=', actorDid) - const keyset = new FeedKeyset(ref('feed_item.sortAt'), ref('feed_item.cid')) + const keyset = new FeedKeyset(ref('like.sortAt'), ref('like.cid')) feedItemsQb = paginate(feedItemsQb, { limit, From 79867d272596092f876b54bdbd556b18839fcc0c Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 14 Sep 2023 16:43:04 -0500 Subject: [PATCH 07/46] Remove default value for post table invalid attrs (#1601) remove default value for post table attrs --- .../db/migrations/20230906T222220386Z-thread-gating.ts | 8 ++------ packages/bsky/src/db/tables/post.ts | 6 +++--- packages/bsky/src/services/feed/index.ts | 8 +++++++- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/bsky/src/db/migrations/20230906T222220386Z-thread-gating.ts b/packages/bsky/src/db/migrations/20230906T222220386Z-thread-gating.ts index 42296aaccf9..4c417278d67 100644 --- a/packages/bsky/src/db/migrations/20230906T222220386Z-thread-gating.ts +++ b/packages/bsky/src/db/migrations/20230906T222220386Z-thread-gating.ts @@ -12,15 +12,11 @@ export async function up(db: Kysely): Promise { .execute() await db.schema .alterTable('post') - .addColumn('invalidReplyRoot', 'boolean', (col) => - col.notNull().defaultTo(false), - ) + .addColumn('invalidReplyRoot', 'boolean') .execute() await db.schema .alterTable('post') - .addColumn('violatesThreadGate', 'boolean', (col) => - col.notNull().defaultTo(false), - ) + .addColumn('violatesThreadGate', 'boolean') .execute() } diff --git a/packages/bsky/src/db/tables/post.ts b/packages/bsky/src/db/tables/post.ts index d70a75912a5..c627efa39e7 100644 --- a/packages/bsky/src/db/tables/post.ts +++ b/packages/bsky/src/db/tables/post.ts @@ -1,4 +1,4 @@ -import { Generated, GeneratedAlways } from 'kysely' +import { GeneratedAlways } from 'kysely' export const tableName = 'post' @@ -12,8 +12,8 @@ export interface Post { replyParent: string | null replyParentCid: string | null langs: string[] | null - invalidReplyRoot: Generated - violatesThreadGate: Generated + invalidReplyRoot: boolean | null + violatesThreadGate: boolean | null createdAt: string indexedAt: string sortAt: GeneratedAlways diff --git a/packages/bsky/src/services/feed/index.ts b/packages/bsky/src/services/feed/index.ts index e5ee2d1c8db..f955979e81e 100644 --- a/packages/bsky/src/services/feed/index.ts +++ b/packages/bsky/src/services/feed/index.ts @@ -166,7 +166,13 @@ export class FeedService { return posts.reduce((acc, cur) => { const { recordJson, ...post } = cur const record = jsonStringToLex(recordJson) as PostRecord - const info: PostInfo = { ...post, record, viewer } + const info: PostInfo = { + ...post, + invalidReplyRoot: post.invalidReplyRoot ?? false, + violatesThreadGate: post.violatesThreadGate ?? false, + record, + viewer, + } return Object.assign(acc, { [post.uri]: info }) }, {} as PostInfoMap) } From d7d7a043dedf147e90e76d838c5bf436f9ab93fa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 14 Sep 2023 17:53:14 -0500 Subject: [PATCH 08/46] Version packages (#1602) Co-authored-by: github-actions[bot] --- .changeset/seven-schools-switch.md | 6 ------ packages/api/CHANGELOG.md | 8 ++++++++ packages/api/package.json | 2 +- packages/bsky/CHANGELOG.md | 8 ++++++++ packages/bsky/package.json | 2 +- packages/dev-env/CHANGELOG.md | 10 ++++++++++ packages/dev-env/package.json | 2 +- packages/pds/CHANGELOG.md | 8 ++++++++ packages/pds/package.json | 2 +- 9 files changed, 38 insertions(+), 10 deletions(-) delete mode 100644 .changeset/seven-schools-switch.md create mode 100644 packages/api/CHANGELOG.md create mode 100644 packages/bsky/CHANGELOG.md create mode 100644 packages/dev-env/CHANGELOG.md create mode 100644 packages/pds/CHANGELOG.md diff --git a/.changeset/seven-schools-switch.md b/.changeset/seven-schools-switch.md deleted file mode 100644 index 012cf392426..00000000000 --- a/.changeset/seven-schools-switch.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@atproto/api': patch ---- - -Adds a new method `app.bsky.graph.getSuggestedFollowsByActor`. This method -returns suggested follows for a given actor based on their likes and follows. diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md new file mode 100644 index 00000000000..c1922cd53b0 --- /dev/null +++ b/packages/api/CHANGELOG.md @@ -0,0 +1,8 @@ +# @atproto/api + +## 0.6.13 + +### Patch Changes + +- [#1553](https://github.com/bluesky-social/atproto/pull/1553) [`3877210e`](https://github.com/bluesky-social/atproto/commit/3877210e7fb3c76dfb1a11eb9ba3f18426301d9f) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds a new method `app.bsky.graph.getSuggestedFollowsByActor`. This method + returns suggested follows for a given actor based on their likes and follows. diff --git a/packages/api/package.json b/packages/api/package.json index 3f4f0be06d8..5f1970ebb82 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.6.12", + "version": "0.6.13", "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", diff --git a/packages/bsky/CHANGELOG.md b/packages/bsky/CHANGELOG.md new file mode 100644 index 00000000000..3c7f5af7887 --- /dev/null +++ b/packages/bsky/CHANGELOG.md @@ -0,0 +1,8 @@ +# @atproto/bsky + +## 0.0.4 + +### Patch Changes + +- Updated dependencies [[`3877210e`](https://github.com/bluesky-social/atproto/commit/3877210e7fb3c76dfb1a11eb9ba3f18426301d9f)]: + - @atproto/api@0.6.13 diff --git a/packages/bsky/package.json b/packages/bsky/package.json index 508d9ef569f..96f8c263991 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/bsky", - "version": "0.0.3", + "version": "0.0.4", "license": "MIT", "repository": { "type": "git", diff --git a/packages/dev-env/CHANGELOG.md b/packages/dev-env/CHANGELOG.md new file mode 100644 index 00000000000..807835c470e --- /dev/null +++ b/packages/dev-env/CHANGELOG.md @@ -0,0 +1,10 @@ +# @atproto/dev-env + +## 0.2.4 + +### Patch Changes + +- Updated dependencies [[`3877210e`](https://github.com/bluesky-social/atproto/commit/3877210e7fb3c76dfb1a11eb9ba3f18426301d9f)]: + - @atproto/api@0.6.13 + - @atproto/bsky@0.0.4 + - @atproto/pds@0.1.13 diff --git a/packages/dev-env/package.json b/packages/dev-env/package.json index adc1a4e21c8..0d54306543b 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/dev-env", - "version": "0.2.3", + "version": "0.2.4", "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", diff --git a/packages/pds/CHANGELOG.md b/packages/pds/CHANGELOG.md new file mode 100644 index 00000000000..226cc576ca0 --- /dev/null +++ b/packages/pds/CHANGELOG.md @@ -0,0 +1,8 @@ +# @atproto/pds + +## 0.1.13 + +### Patch Changes + +- Updated dependencies [[`3877210e`](https://github.com/bluesky-social/atproto/commit/3877210e7fb3c76dfb1a11eb9ba3f18426301d9f)]: + - @atproto/api@0.6.13 diff --git a/packages/pds/package.json b/packages/pds/package.json index 8803b6409ee..92befab8e55 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/pds", - "version": "0.1.12", + "version": "0.1.13", "license": "MIT", "repository": { "type": "git", From 498e4ace314252823969bfe967c3d820a3877b54 Mon Sep 17 00:00:00 2001 From: bnewbold Date: Thu, 14 Sep 2023 16:29:31 -0700 Subject: [PATCH 09/46] update Bluesky PBLLC to PBC (Public Benefit Corporation) (#1600) --- LICENSE | 2 +- README.md | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index c947c234799..042cffe65ef 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022-2023 Bluesky PBLLC +Copyright (c) 2022-2023 Bluesky PBC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 12c6e58b880..39a0f6bc346 100644 --- a/README.md +++ b/README.md @@ -54,4 +54,4 @@ If you discover any security issues, please send an email to security@bsky.app. MIT License -Copyright (c) 2023 Bluesky PBLLC +Copyright (c) 2023 Bluesky PBC diff --git a/package.json b/package.json index 13d9555429f..9a3f4b42993 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "atp", "version": "0.0.1", "repository": "git@github.com:bluesky-social/atproto.git", - "author": "Bluesky PBLLC ", + "author": "Bluesky PBC ", "license": "MIT", "private": true, "engines": { From ba78d225b1fe90389bdc6e44bbeab670f7aaff07 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Fri, 15 Sep 2023 11:23:33 -0500 Subject: [PATCH 10/46] Temporarily disable filtering `invalidReplyRoot`s (#1609) temporarily disable invalidReplyRoot check --- packages/bsky/src/api/app/bsky/feed/getPostThread.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts index 9292f0d5d99..0c26285b384 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts @@ -140,7 +140,9 @@ const composeThread = ( // b. may not appear anywhere else in the thread. const isAnchorPost = state.threadData.post.uri === threadData.post.postUri const info = posts[threadData.post.postUri] - const badReply = !!info?.invalidReplyRoot || !!info?.violatesThreadGate + // @TODO re-enable invalidReplyRoot check + // const badReply = !!info?.invalidReplyRoot || !!info?.violatesThreadGate + const badReply = !!info?.violatesThreadGate const omitBadReply = !isAnchorPost && badReply if (!post || blocks[post.uri]?.reply || omitBadReply) { @@ -312,7 +314,9 @@ const checkViewerCanReply = async ( threadgate: ThreadgateRecord | null, ) => { if (!viewer) return false - if (anchor?.invalidReplyRoot || anchor?.violatesThreadGate) return false + // @TODO re-enable invalidReplyRoot check + // if (anchor?.invalidReplyRoot || anchor?.violatesThreadGate) return false + if (anchor?.violatesThreadGate) return false const viewerViolatesThreadGate = await violatesThreadGate( db, viewer, From b1dc355504f9f2e047093dc56682b8034518cf80 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 15 Sep 2023 14:05:55 -0500 Subject: [PATCH 11/46] fix syntax docs (#1611) --- .changeset/tasty-countries-approve.md | 5 +++++ packages/syntax/README.md | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/tasty-countries-approve.md diff --git a/.changeset/tasty-countries-approve.md b/.changeset/tasty-countries-approve.md new file mode 100644 index 00000000000..4892b30e071 --- /dev/null +++ b/.changeset/tasty-countries-approve.md @@ -0,0 +1,5 @@ +--- +'@atproto/syntax': patch +--- + +Fix imports in `README.md`. diff --git a/packages/syntax/README.md b/packages/syntax/README.md index 5fd557fa323..5bc6c01b646 100644 --- a/packages/syntax/README.md +++ b/packages/syntax/README.md @@ -1,11 +1,11 @@ # Syntax -Validation logic for AT identifiers - DIDs, Handles, NSIDs, and AT URIs +Validation logic for AT identifiers - DIDs, Handles, NSIDs, and AT URIs. ## Usage ```typescript -import * as identifier from '@atproto/syntax' +import { isValidHandle, ensureValidHandle, isValidDid } from '@atproto/syntax' isValidHandle('alice.test') // returns true ensureValidHandle('alice.test') // returns void From 38f8f44c41644fef78c49aa34c7316309efe1dd7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 15 Sep 2023 15:49:31 -0500 Subject: [PATCH 12/46] Version packages (#1612) Co-authored-by: github-actions[bot] --- .changeset/tasty-countries-approve.md | 5 ----- packages/api/CHANGELOG.md | 9 +++++++++ packages/api/package.json | 2 +- packages/aws/CHANGELOG.md | 8 ++++++++ packages/aws/package.json | 2 +- packages/bsky/CHANGELOG.md | 11 +++++++++++ packages/bsky/package.json | 2 +- packages/dev-env/CHANGELOG.md | 11 +++++++++++ packages/dev-env/package.json | 2 +- packages/identifier/CHANGELOG.md | 8 ++++++++ packages/identifier/package.json | 2 +- packages/lex-cli/CHANGELOG.md | 9 +++++++++ packages/lex-cli/package.json | 2 +- packages/lexicon/CHANGELOG.md | 8 ++++++++ packages/lexicon/package.json | 2 +- packages/nsid/CHANGELOG.md | 8 ++++++++ packages/nsid/package.json | 2 +- packages/pds/CHANGELOG.md | 12 ++++++++++++ packages/pds/package.json | 2 +- packages/repo/CHANGELOG.md | 9 +++++++++ packages/repo/package.json | 2 +- packages/syntax/CHANGELOG.md | 7 +++++++ packages/syntax/package.json | 2 +- packages/uri/CHANGELOG.md | 8 ++++++++ packages/uri/package.json | 2 +- packages/xrpc-server/CHANGELOG.md | 8 ++++++++ packages/xrpc-server/package.json | 2 +- packages/xrpc/CHANGELOG.md | 8 ++++++++ packages/xrpc/package.json | 2 +- 29 files changed, 138 insertions(+), 19 deletions(-) delete mode 100644 .changeset/tasty-countries-approve.md create mode 100644 packages/aws/CHANGELOG.md create mode 100644 packages/identifier/CHANGELOG.md create mode 100644 packages/lex-cli/CHANGELOG.md create mode 100644 packages/lexicon/CHANGELOG.md create mode 100644 packages/nsid/CHANGELOG.md create mode 100644 packages/repo/CHANGELOG.md create mode 100644 packages/syntax/CHANGELOG.md create mode 100644 packages/uri/CHANGELOG.md create mode 100644 packages/xrpc-server/CHANGELOG.md create mode 100644 packages/xrpc/CHANGELOG.md diff --git a/.changeset/tasty-countries-approve.md b/.changeset/tasty-countries-approve.md deleted file mode 100644 index 4892b30e071..00000000000 --- a/.changeset/tasty-countries-approve.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@atproto/syntax': patch ---- - -Fix imports in `README.md`. diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index c1922cd53b0..19149433dca 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -1,5 +1,14 @@ # @atproto/api +## 0.6.14 + +### Patch Changes + +- Updated dependencies [[`b1dc3555`](https://github.com/bluesky-social/atproto/commit/b1dc355504f9f2e047093dc56682b8034518cf80)]: + - @atproto/syntax@0.1.1 + - @atproto/lexicon@0.2.1 + - @atproto/xrpc@0.3.1 + ## 0.6.13 ### Patch Changes diff --git a/packages/api/package.json b/packages/api/package.json index 5f1970ebb82..8f3c00fbab4 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.6.13", + "version": "0.6.14", "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", diff --git a/packages/aws/CHANGELOG.md b/packages/aws/CHANGELOG.md new file mode 100644 index 00000000000..a446e35adbe --- /dev/null +++ b/packages/aws/CHANGELOG.md @@ -0,0 +1,8 @@ +# @atproto/aws + +## 0.1.1 + +### Patch Changes + +- Updated dependencies []: + - @atproto/repo@0.3.1 diff --git a/packages/aws/package.json b/packages/aws/package.json index 70fe9f72698..da1de3439ba 100644 --- a/packages/aws/package.json +++ b/packages/aws/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/aws", - "version": "0.1.0", + "version": "0.1.1", "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", diff --git a/packages/bsky/CHANGELOG.md b/packages/bsky/CHANGELOG.md index 3c7f5af7887..6841f093f0d 100644 --- a/packages/bsky/CHANGELOG.md +++ b/packages/bsky/CHANGELOG.md @@ -1,5 +1,16 @@ # @atproto/bsky +## 0.0.5 + +### Patch Changes + +- Updated dependencies [[`b1dc3555`](https://github.com/bluesky-social/atproto/commit/b1dc355504f9f2e047093dc56682b8034518cf80)]: + - @atproto/syntax@0.1.1 + - @atproto/api@0.6.14 + - @atproto/lexicon@0.2.1 + - @atproto/repo@0.3.1 + - @atproto/xrpc-server@0.3.1 + ## 0.0.4 ### Patch Changes diff --git a/packages/bsky/package.json b/packages/bsky/package.json index 96f8c263991..e9309577801 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/bsky", - "version": "0.0.4", + "version": "0.0.5", "license": "MIT", "repository": { "type": "git", diff --git a/packages/dev-env/CHANGELOG.md b/packages/dev-env/CHANGELOG.md index 807835c470e..1d3f46c8f1f 100644 --- a/packages/dev-env/CHANGELOG.md +++ b/packages/dev-env/CHANGELOG.md @@ -1,5 +1,16 @@ # @atproto/dev-env +## 0.2.5 + +### Patch Changes + +- Updated dependencies [[`b1dc3555`](https://github.com/bluesky-social/atproto/commit/b1dc355504f9f2e047093dc56682b8034518cf80)]: + - @atproto/syntax@0.1.1 + - @atproto/api@0.6.14 + - @atproto/bsky@0.0.5 + - @atproto/pds@0.1.14 + - @atproto/xrpc-server@0.3.1 + ## 0.2.4 ### Patch Changes diff --git a/packages/dev-env/package.json b/packages/dev-env/package.json index 0d54306543b..d5313408135 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/dev-env", - "version": "0.2.4", + "version": "0.2.5", "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", diff --git a/packages/identifier/CHANGELOG.md b/packages/identifier/CHANGELOG.md new file mode 100644 index 00000000000..f01512f9e96 --- /dev/null +++ b/packages/identifier/CHANGELOG.md @@ -0,0 +1,8 @@ +# @atproto/identifier + +## 0.2.1 + +### Patch Changes + +- Updated dependencies [[`b1dc3555`](https://github.com/bluesky-social/atproto/commit/b1dc355504f9f2e047093dc56682b8034518cf80)]: + - @atproto/syntax@0.1.1 diff --git a/packages/identifier/package.json b/packages/identifier/package.json index 1642c1046d0..074da48a9fc 100644 --- a/packages/identifier/package.json +++ b/packages/identifier/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/identifier", - "version": "0.2.0", + "version": "0.2.1", "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", diff --git a/packages/lex-cli/CHANGELOG.md b/packages/lex-cli/CHANGELOG.md new file mode 100644 index 00000000000..bc4a4c0ff3a --- /dev/null +++ b/packages/lex-cli/CHANGELOG.md @@ -0,0 +1,9 @@ +# @atproto/lex-cli + +## 0.2.1 + +### Patch Changes + +- Updated dependencies [[`b1dc3555`](https://github.com/bluesky-social/atproto/commit/b1dc355504f9f2e047093dc56682b8034518cf80)]: + - @atproto/syntax@0.1.1 + - @atproto/lexicon@0.2.1 diff --git a/packages/lex-cli/package.json b/packages/lex-cli/package.json index 6a79be7d34c..43ddf227fd3 100644 --- a/packages/lex-cli/package.json +++ b/packages/lex-cli/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/lex-cli", - "version": "0.2.0", + "version": "0.2.1", "bin": { "lex": "dist/index.js" }, diff --git a/packages/lexicon/CHANGELOG.md b/packages/lexicon/CHANGELOG.md new file mode 100644 index 00000000000..502d2142522 --- /dev/null +++ b/packages/lexicon/CHANGELOG.md @@ -0,0 +1,8 @@ +# @atproto/lexicon + +## 0.2.1 + +### Patch Changes + +- Updated dependencies [[`b1dc3555`](https://github.com/bluesky-social/atproto/commit/b1dc355504f9f2e047093dc56682b8034518cf80)]: + - @atproto/syntax@0.1.1 diff --git a/packages/lexicon/package.json b/packages/lexicon/package.json index 4b5f7470671..18f254b6d82 100644 --- a/packages/lexicon/package.json +++ b/packages/lexicon/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/lexicon", - "version": "0.2.0", + "version": "0.2.1", "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", diff --git a/packages/nsid/CHANGELOG.md b/packages/nsid/CHANGELOG.md new file mode 100644 index 00000000000..b351d372ae5 --- /dev/null +++ b/packages/nsid/CHANGELOG.md @@ -0,0 +1,8 @@ +# @atproto/nsid + +## 0.1.1 + +### Patch Changes + +- Updated dependencies [[`b1dc3555`](https://github.com/bluesky-social/atproto/commit/b1dc355504f9f2e047093dc56682b8034518cf80)]: + - @atproto/syntax@0.1.1 diff --git a/packages/nsid/package.json b/packages/nsid/package.json index af156718f49..8f0ce82f366 100644 --- a/packages/nsid/package.json +++ b/packages/nsid/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/nsid", - "version": "0.1.0", + "version": "0.1.1", "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", diff --git a/packages/pds/CHANGELOG.md b/packages/pds/CHANGELOG.md index 226cc576ca0..ad324f11cf8 100644 --- a/packages/pds/CHANGELOG.md +++ b/packages/pds/CHANGELOG.md @@ -1,5 +1,17 @@ # @atproto/pds +## 0.1.14 + +### Patch Changes + +- Updated dependencies [[`b1dc3555`](https://github.com/bluesky-social/atproto/commit/b1dc355504f9f2e047093dc56682b8034518cf80)]: + - @atproto/syntax@0.1.1 + - @atproto/api@0.6.14 + - @atproto/lexicon@0.2.1 + - @atproto/repo@0.3.1 + - @atproto/xrpc@0.3.1 + - @atproto/xrpc-server@0.3.1 + ## 0.1.13 ### Patch Changes diff --git a/packages/pds/package.json b/packages/pds/package.json index 92befab8e55..422880426f9 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/pds", - "version": "0.1.13", + "version": "0.1.14", "license": "MIT", "repository": { "type": "git", diff --git a/packages/repo/CHANGELOG.md b/packages/repo/CHANGELOG.md new file mode 100644 index 00000000000..74852b0b0bb --- /dev/null +++ b/packages/repo/CHANGELOG.md @@ -0,0 +1,9 @@ +# @atproto/repo + +## 0.3.1 + +### Patch Changes + +- Updated dependencies [[`b1dc3555`](https://github.com/bluesky-social/atproto/commit/b1dc355504f9f2e047093dc56682b8034518cf80)]: + - @atproto/syntax@0.1.1 + - @atproto/lexicon@0.2.1 diff --git a/packages/repo/package.json b/packages/repo/package.json index 9c37695f1d8..7ea37ca4d03 100644 --- a/packages/repo/package.json +++ b/packages/repo/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/repo", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", diff --git a/packages/syntax/CHANGELOG.md b/packages/syntax/CHANGELOG.md new file mode 100644 index 00000000000..99a4bd635f4 --- /dev/null +++ b/packages/syntax/CHANGELOG.md @@ -0,0 +1,7 @@ +# @atproto/syntax + +## 0.1.1 + +### Patch Changes + +- [#1611](https://github.com/bluesky-social/atproto/pull/1611) [`b1dc3555`](https://github.com/bluesky-social/atproto/commit/b1dc355504f9f2e047093dc56682b8034518cf80) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Fix imports in `README.md`. diff --git a/packages/syntax/package.json b/packages/syntax/package.json index 60fabdfe8d2..0be28698de7 100644 --- a/packages/syntax/package.json +++ b/packages/syntax/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/syntax", - "version": "0.1.0", + "version": "0.1.1", "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", diff --git a/packages/uri/CHANGELOG.md b/packages/uri/CHANGELOG.md new file mode 100644 index 00000000000..b6bf96be8a1 --- /dev/null +++ b/packages/uri/CHANGELOG.md @@ -0,0 +1,8 @@ +# @atproto/uri + +## 0.1.1 + +### Patch Changes + +- Updated dependencies [[`b1dc3555`](https://github.com/bluesky-social/atproto/commit/b1dc355504f9f2e047093dc56682b8034518cf80)]: + - @atproto/syntax@0.1.1 diff --git a/packages/uri/package.json b/packages/uri/package.json index 1060dda6890..65664ff5634 100644 --- a/packages/uri/package.json +++ b/packages/uri/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/uri", - "version": "0.1.0", + "version": "0.1.1", "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", diff --git a/packages/xrpc-server/CHANGELOG.md b/packages/xrpc-server/CHANGELOG.md new file mode 100644 index 00000000000..8f0d8d9093e --- /dev/null +++ b/packages/xrpc-server/CHANGELOG.md @@ -0,0 +1,8 @@ +# @atproto/xrpc-server + +## 0.3.1 + +### Patch Changes + +- Updated dependencies []: + - @atproto/lexicon@0.2.1 diff --git a/packages/xrpc-server/package.json b/packages/xrpc-server/package.json index e51db0f0a0f..b6df825c8d8 100644 --- a/packages/xrpc-server/package.json +++ b/packages/xrpc-server/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/xrpc-server", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", diff --git a/packages/xrpc/CHANGELOG.md b/packages/xrpc/CHANGELOG.md new file mode 100644 index 00000000000..60e4ef37152 --- /dev/null +++ b/packages/xrpc/CHANGELOG.md @@ -0,0 +1,8 @@ +# @atproto/xrpc + +## 0.3.1 + +### Patch Changes + +- Updated dependencies []: + - @atproto/lexicon@0.2.1 diff --git a/packages/xrpc/package.json b/packages/xrpc/package.json index 4fd825acd06..f6358602256 100644 --- a/packages/xrpc/package.json +++ b/packages/xrpc/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/xrpc", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", From a3a32de0ffaa5b687f862daa583efa8bd6fad1d5 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Fri, 15 Sep 2023 17:16:57 -0500 Subject: [PATCH 13/46] Allow bypass on ratelimit ip (#1613) allow bypass on ratelimit ip --- packages/pds/src/config.ts | 15 +++++++++++++++ packages/pds/src/index.ts | 2 ++ packages/xrpc-server/src/rate-limiter.ts | 14 ++++++++++---- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/pds/src/config.ts b/packages/pds/src/config.ts index c6a176bfe35..acca18b580a 100644 --- a/packages/pds/src/config.ts +++ b/packages/pds/src/config.ts @@ -45,6 +45,7 @@ export interface ServerConfigValues { rateLimitsEnabled: boolean rateLimitBypassKey?: string + rateLimitBypassIps?: string[] redisScratchAddress?: string redisScratchPassword?: string @@ -163,6 +164,15 @@ export class ServerConfig { const rateLimitsEnabled = process.env.RATE_LIMITS_ENABLED === 'true' const rateLimitBypassKey = nonemptyString(process.env.RATE_LIMIT_BYPASS_KEY) + const rateLimitBypassIpsStr = nonemptyString( + process.env.RATE_LIMIT_BYPASS_IPS, + ) + const rateLimitBypassIps = rateLimitBypassIpsStr + ? rateLimitBypassIpsStr.split(',').map((ipOrCidr) => { + const ip = ipOrCidr.split('/')[0] + return ip.trim() + }) + : undefined const redisScratchAddress = nonemptyString( process.env.REDIS_SCRATCH_ADDRESS, ) @@ -266,6 +276,7 @@ export class ServerConfig { blobCacheLocation, rateLimitsEnabled, rateLimitBypassKey, + rateLimitBypassIps, redisScratchAddress, redisScratchPassword, appUrlPasswordReset, @@ -454,6 +465,10 @@ export class ServerConfig { return this.cfg.rateLimitBypassKey } + get rateLimitBypassIps() { + return this.cfg.rateLimitBypassIps + } + get redisScratchAddress() { return this.cfg.redisScratchAddress } diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index dc7e36b8d62..4db744481e5 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -268,12 +268,14 @@ export class PDS { rlCreator = (opts: RateLimiterOpts) => RateLimiter.redis(redisScratch, { bypassSecret: config.rateLimitBypassKey, + bypassIps: config.rateLimitBypassIps, ...opts, }) } else { rlCreator = (opts: RateLimiterOpts) => RateLimiter.memory({ bypassSecret: config.rateLimitBypassKey, + bypassIps: config.rateLimitBypassIps, ...opts, }) } diff --git a/packages/xrpc-server/src/rate-limiter.ts b/packages/xrpc-server/src/rate-limiter.ts index ab6d79d07af..e9bf8a40e22 100644 --- a/packages/xrpc-server/src/rate-limiter.ts +++ b/packages/xrpc-server/src/rate-limiter.ts @@ -20,6 +20,7 @@ export type RateLimiterOpts = { durationMs: number points: number bypassSecret?: string + bypassIps?: string[] calcKey?: CalcKeyFn calcPoints?: CalcPointsFn failClosed?: boolean @@ -27,14 +28,16 @@ export type RateLimiterOpts = { export class RateLimiter implements RateLimiterI { public limiter: RateLimiterAbstract - private byPassSecret?: string + private bypassSecret?: string + private bypassIps?: string[] private failClosed?: boolean public calcKey: CalcKeyFn public calcPoints: CalcPointsFn constructor(limiter: RateLimiterAbstract, opts: RateLimiterOpts) { this.limiter = limiter - this.byPassSecret = opts.bypassSecret + this.bypassSecret = opts.bypassSecret + this.bypassIps = opts.bypassIps this.calcKey = opts.calcKey ?? defaultKey this.calcPoints = opts.calcPoints ?? defaultPoints } @@ -63,11 +66,14 @@ export class RateLimiter implements RateLimiterI { opts?: { calcKey?: CalcKeyFn; calcPoints?: CalcPointsFn }, ): Promise { if ( - this.byPassSecret && - ctx.req.header('x-ratelimit-bypass') === this.byPassSecret + this.bypassSecret && + ctx.req.header('x-ratelimit-bypass') === this.bypassSecret ) { return null } + if (this.bypassIps && this.bypassIps.includes(ctx.req.ip)) { + return null + } const key = opts?.calcKey ? opts.calcKey(ctx) : this.calcKey(ctx) const points = opts?.calcPoints ? opts.calcPoints(ctx) From 1cec2dd7d2c4635bf12a8b62baeff8fa4de6e9e6 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Fri, 15 Sep 2023 17:35:11 -0500 Subject: [PATCH 14/46] Write rate limits (#1578) * get rate limit ip correctly * add write rate-limits --- .../src/api/com/atproto/repo/applyWrites.ts | 28 +++++++++++++++++++ .../src/api/com/atproto/repo/createRecord.ts | 12 ++++++++ .../src/api/com/atproto/repo/deleteRecord.ts | 12 ++++++++ .../pds/src/api/com/atproto/repo/putRecord.ts | 12 ++++++++ packages/pds/src/index.ts | 14 +++++++++- 5 files changed, 77 insertions(+), 1 deletion(-) diff --git a/packages/pds/src/api/com/atproto/repo/applyWrites.ts b/packages/pds/src/api/com/atproto/repo/applyWrites.ts index 5dc65100855..d5be8bb720d 100644 --- a/packages/pds/src/api/com/atproto/repo/applyWrites.ts +++ b/packages/pds/src/api/com/atproto/repo/applyWrites.ts @@ -3,6 +3,7 @@ import { InvalidRequestError, AuthRequiredError } from '@atproto/xrpc-server' import { prepareCreate, prepareDelete, prepareUpdate } from '../../../../repo' import { Server } from '../../../../lexicon' import { + HandlerInput, isCreate, isUpdate, isDelete, @@ -15,9 +16,36 @@ import { import AppContext from '../../../../context' import { ConcurrentWriteError } from '../../../../services/repo' +const ratelimitPoints = ({ input }: { input: HandlerInput }) => { + let points = 0 + for (const op of input.body.writes) { + if (isCreate(op)) { + points += 3 + } else if (isUpdate(op)) { + points += 2 + } else { + points += 1 + } + } + return points +} + export default function (server: Server, ctx: AppContext) { server.com.atproto.repo.applyWrites({ auth: ctx.accessVerifierCheckTakedown, + rateLimit: [ + { + name: 'repo-write-hour', + calcKey: ({ auth }) => auth.credentials.did, + calcPoints: ratelimitPoints, + }, + { + name: 'repo-write-day', + calcKey: ({ auth }) => auth.credentials.did, + calcPoints: ratelimitPoints, + }, + ], + handler: async ({ input, auth }) => { const tx = input.body const { repo, validate, swapCommit } = tx diff --git a/packages/pds/src/api/com/atproto/repo/createRecord.ts b/packages/pds/src/api/com/atproto/repo/createRecord.ts index 08ff46ecf78..26bc5614785 100644 --- a/packages/pds/src/api/com/atproto/repo/createRecord.ts +++ b/packages/pds/src/api/com/atproto/repo/createRecord.ts @@ -16,6 +16,18 @@ import { ConcurrentWriteError } from '../../../../services/repo' export default function (server: Server, ctx: AppContext) { server.com.atproto.repo.createRecord({ auth: ctx.accessVerifierCheckTakedown, + rateLimit: [ + { + name: 'repo-write-hour', + calcKey: ({ auth }) => auth.credentials.did, + calcPoints: () => 3, + }, + { + name: 'repo-write-day', + calcKey: ({ auth }) => auth.credentials.did, + calcPoints: () => 3, + }, + ], handler: async ({ input, auth }) => { const { repo, collection, rkey, record, swapCommit, validate } = input.body diff --git a/packages/pds/src/api/com/atproto/repo/deleteRecord.ts b/packages/pds/src/api/com/atproto/repo/deleteRecord.ts index 042367e5b65..99f171e0849 100644 --- a/packages/pds/src/api/com/atproto/repo/deleteRecord.ts +++ b/packages/pds/src/api/com/atproto/repo/deleteRecord.ts @@ -9,6 +9,18 @@ import { ConcurrentWriteError } from '../../../../services/repo' export default function (server: Server, ctx: AppContext) { server.com.atproto.repo.deleteRecord({ auth: ctx.accessVerifierCheckTakedown, + rateLimit: [ + { + name: 'repo-write-hour', + calcKey: ({ auth }) => auth.credentials.did, + calcPoints: () => 1, + }, + { + name: 'repo-write-day', + calcKey: ({ auth }) => auth.credentials.did, + calcPoints: () => 1, + }, + ], handler: async ({ input, auth }) => { const { repo, collection, rkey, swapCommit, swapRecord } = input.body const did = await ctx.services.account(ctx.db).getDidForActor(repo) diff --git a/packages/pds/src/api/com/atproto/repo/putRecord.ts b/packages/pds/src/api/com/atproto/repo/putRecord.ts index 311083359f2..8fdcc776bb9 100644 --- a/packages/pds/src/api/com/atproto/repo/putRecord.ts +++ b/packages/pds/src/api/com/atproto/repo/putRecord.ts @@ -16,6 +16,18 @@ import { ConcurrentWriteError } from '../../../../services/repo' export default function (server: Server, ctx: AppContext) { server.com.atproto.repo.putRecord({ auth: ctx.accessVerifierCheckTakedown, + rateLimit: [ + { + name: 'repo-write-hour', + calcKey: ({ auth }) => auth.credentials.did, + calcPoints: () => 2, + }, + { + name: 'repo-write-day', + calcKey: ({ auth }) => auth.credentials.did, + calcPoints: () => 2, + }, + ], handler: async ({ auth, input }) => { const { repo, diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index 4db744481e5..a156d8f7b8e 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -20,7 +20,7 @@ import { RateLimiterOpts, Options as XrpcServerOptions, } from '@atproto/xrpc-server' -import { MINUTE } from '@atproto/common' +import { DAY, HOUR, MINUTE } from '@atproto/common' import * as appviewConsumers from './app-view/event-stream/consumers' import inProcessAppView from './app-view/api' import API from './api' @@ -288,6 +288,18 @@ export class PDS { points: 3000, }, ], + shared: [ + { + name: 'repo-write-hour', + durationMs: HOUR, + points: 5000, // creates=3, puts=2, deletes=1 + }, + { + name: 'repo-write-day', + durationMs: DAY, + points: 35000, // creates=3, puts=2, deletes=1 + }, + ], } } From 17ce804ffaaafb1b1efb61174217a1da275feda8 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Fri, 15 Sep 2023 18:11:48 -0500 Subject: [PATCH 15/46] Tweak createSession rate limit key (#1614) tweak create session rl key --- packages/pds/src/api/com/atproto/server/createSession.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pds/src/api/com/atproto/server/createSession.ts b/packages/pds/src/api/com/atproto/server/createSession.ts index aee0063d86c..3769cda08a6 100644 --- a/packages/pds/src/api/com/atproto/server/createSession.ts +++ b/packages/pds/src/api/com/atproto/server/createSession.ts @@ -11,12 +11,12 @@ export default function (server: Server, ctx: AppContext) { { durationMs: DAY, points: 300, - calcKey: ({ input }) => input.body.identifier, + calcKey: ({ input, req }) => `${input.body.identifier}-${req.ip}`, }, { durationMs: 5 * MINUTE, points: 30, - calcKey: ({ input }) => input.body.identifier, + calcKey: ({ input, req }) => `${input.body.identifier}-${req.ip}`, }, ], handler: async ({ input }) => { From a441da85f1ab6e47990f2efd77b935858f7deed6 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 18 Sep 2023 13:57:11 -0500 Subject: [PATCH 16/46] Filter preferences for app passwords (#1626) filter preferences for app passwords --- packages/pds/src/api/app/bsky/actor/getPreferences.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/pds/src/api/app/bsky/actor/getPreferences.ts b/packages/pds/src/api/app/bsky/actor/getPreferences.ts index 782ef34869a..1bca50f0bd1 100644 --- a/packages/pds/src/api/app/bsky/actor/getPreferences.ts +++ b/packages/pds/src/api/app/bsky/actor/getPreferences.ts @@ -1,5 +1,6 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { AuthScope } from '../../../../auth' export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.getPreferences({ @@ -7,9 +8,15 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ auth }) => { const requester = auth.credentials.did const { services, db } = ctx - const preferences = await services + let preferences = await services .account(db) .getPreferences(requester, 'app.bsky') + if (auth.credentials.scope !== AuthScope.Access) { + // filter out personal details for app passwords + preferences = preferences.filter( + (pref) => pref.$type !== 'app.bsky.actor.defs#personalDetailsPref', + ) + } return { encoding: 'application/json', body: { preferences }, From f69c1a73022ab7d742fff81be833d6e5cab989c7 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 18 Sep 2023 14:19:20 -0500 Subject: [PATCH 17/46] Tweak rate limit setup for multi rate limit routes (#1627) tweak rate limit setup for multi rate limit routes --- packages/xrpc-server/src/server.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/xrpc-server/src/server.ts b/packages/xrpc-server/src/server.ts index 5d27e3e45a8..4e0a84ce4b7 100644 --- a/packages/xrpc-server/src/server.ts +++ b/packages/xrpc-server/src/server.ts @@ -405,7 +405,8 @@ export class Server { ? config.rateLimit : [config.rateLimit] this.routeRateLimiterFns[nsid] = [] - for (const limit of limits) { + for (let i = 0; i < limits.length; i++) { + const limit = limits[i] const { calcKey, calcPoints } = limit if (isShared(limit)) { const rateLimiter = this.sharedRateLimiters[limit.name] @@ -420,7 +421,7 @@ export class Server { } else { const { durationMs, points } = limit const rateLimiter = this.options.rateLimits?.creator({ - keyPrefix: nsid, + keyPrefix: `nsid-${i}`, durationMs, points, calcKey, From a5a0e4a420bd33c926c8ffb29615a01c5ee42b3d Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 18 Sep 2023 19:35:09 -0500 Subject: [PATCH 18/46] Remove zod from xrpc-server error handling (#1631) remove zod from xrpc-server error handling check --- packages/xrpc-server/src/types.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/xrpc-server/src/types.ts b/packages/xrpc-server/src/types.ts index 829f8a270d2..dc75627a95f 100644 --- a/packages/xrpc-server/src/types.ts +++ b/packages/xrpc-server/src/types.ts @@ -201,7 +201,13 @@ export class XRPCError extends Error { } export function isHandlerError(v: unknown): v is HandlerError { - return handlerError.safeParse(v).success + return ( + !!v && + typeof v === 'object' && + typeof v['status'] === 'number' && + (v['error'] === undefined || typeof v['error'] === 'string') && + (v['message'] === undefined || typeof v['message'] === 'string') + ) } export class InvalidRequestError extends XRPCError { From 828dfa8a8af523858ebd4dc164c996a8884c50b9 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Tue, 19 Sep 2023 10:51:26 -0500 Subject: [PATCH 19/46] Enforce properties field on lexicon object schemas (#1628) * add empty properites to thread gate schema fragments * tweak lexicon type --- lexicons/app/bsky/feed/threadgate.json | 6 ++++-- packages/api/src/client/lexicons.ts | 2 ++ packages/api/src/client/types/app/bsky/feed/threadgate.ts | 8 ++++++-- packages/bsky/src/lexicon/lexicons.ts | 2 ++ .../bsky/src/lexicon/types/app/bsky/feed/threadgate.ts | 8 ++++++-- packages/lexicon/src/types.ts | 8 +++----- packages/pds/src/lexicon/lexicons.ts | 2 ++ .../pds/src/lexicon/types/app/bsky/feed/threadgate.ts | 8 ++++++-- 8 files changed, 31 insertions(+), 13 deletions(-) diff --git a/lexicons/app/bsky/feed/threadgate.json b/lexicons/app/bsky/feed/threadgate.json index aa2262174d1..7969b6360a6 100644 --- a/lexicons/app/bsky/feed/threadgate.json +++ b/lexicons/app/bsky/feed/threadgate.json @@ -25,11 +25,13 @@ }, "mentionRule": { "type": "object", - "description": "Allow replies from actors mentioned in your post." + "description": "Allow replies from actors mentioned in your post.", + "properties": {} }, "followingRule": { "type": "object", - "description": "Allow replies from actors you follow." + "description": "Allow replies from actors you follow.", + "properties": {} }, "listRule": { "type": "object", diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 2ca983aec4b..7ee31c74e62 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -5693,10 +5693,12 @@ export const schemaDict = { mentionRule: { type: 'object', description: 'Allow replies from actors mentioned in your post.', + properties: {}, }, followingRule: { type: 'object', description: 'Allow replies from actors you follow.', + properties: {}, }, listRule: { type: 'object', diff --git a/packages/api/src/client/types/app/bsky/feed/threadgate.ts b/packages/api/src/client/types/app/bsky/feed/threadgate.ts index 20e0a62eb3a..a1afec85673 100644 --- a/packages/api/src/client/types/app/bsky/feed/threadgate.ts +++ b/packages/api/src/client/types/app/bsky/feed/threadgate.ts @@ -32,7 +32,9 @@ export function validateRecord(v: unknown): ValidationResult { } /** Allow replies from actors mentioned in your post. */ -export interface MentionRule {} +export interface MentionRule { + [k: string]: unknown +} export function isMentionRule(v: unknown): v is MentionRule { return ( @@ -47,7 +49,9 @@ export function validateMentionRule(v: unknown): ValidationResult { } /** Allow replies from actors you follow. */ -export interface FollowingRule {} +export interface FollowingRule { + [k: string]: unknown +} export function isFollowingRule(v: unknown): v is FollowingRule { return ( diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 2ca983aec4b..7ee31c74e62 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -5693,10 +5693,12 @@ export const schemaDict = { mentionRule: { type: 'object', description: 'Allow replies from actors mentioned in your post.', + properties: {}, }, followingRule: { type: 'object', description: 'Allow replies from actors you follow.', + properties: {}, }, listRule: { type: 'object', diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/threadgate.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/threadgate.ts index 51f9f8e9af1..6a190d6e98a 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/threadgate.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/threadgate.ts @@ -32,7 +32,9 @@ export function validateRecord(v: unknown): ValidationResult { } /** Allow replies from actors mentioned in your post. */ -export interface MentionRule {} +export interface MentionRule { + [k: string]: unknown +} export function isMentionRule(v: unknown): v is MentionRule { return ( @@ -47,7 +49,9 @@ export function validateMentionRule(v: unknown): ValidationResult { } /** Allow replies from actors you follow. */ -export interface FollowingRule {} +export interface FollowingRule { + [k: string]: unknown +} export function isFollowingRule(v: unknown): v is FollowingRule { return ( diff --git a/packages/lexicon/src/types.ts b/packages/lexicon/src/types.ts index 98616c9e59a..906cd353328 100644 --- a/packages/lexicon/src/types.ts +++ b/packages/lexicon/src/types.ts @@ -173,11 +173,9 @@ export const lexObject = z description: z.string().optional(), required: z.string().array().optional(), nullable: z.string().array().optional(), - properties: z - .record( - z.union([lexRefVariant, lexIpldType, lexArray, lexBlob, lexPrimitive]), - ) - .optional(), + properties: z.record( + z.union([lexRefVariant, lexIpldType, lexArray, lexBlob, lexPrimitive]), + ), }) .strict() .superRefine(requiredPropertiesRefinement) diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 2ca983aec4b..7ee31c74e62 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -5693,10 +5693,12 @@ export const schemaDict = { mentionRule: { type: 'object', description: 'Allow replies from actors mentioned in your post.', + properties: {}, }, followingRule: { type: 'object', description: 'Allow replies from actors you follow.', + properties: {}, }, listRule: { type: 'object', diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/threadgate.ts b/packages/pds/src/lexicon/types/app/bsky/feed/threadgate.ts index 51f9f8e9af1..6a190d6e98a 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/threadgate.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/threadgate.ts @@ -32,7 +32,9 @@ export function validateRecord(v: unknown): ValidationResult { } /** Allow replies from actors mentioned in your post. */ -export interface MentionRule {} +export interface MentionRule { + [k: string]: unknown +} export function isMentionRule(v: unknown): v is MentionRule { return ( @@ -47,7 +49,9 @@ export function validateMentionRule(v: unknown): ValidationResult { } /** Allow replies from actors you follow. */ -export interface FollowingRule {} +export interface FollowingRule { + [k: string]: unknown +} export function isFollowingRule(v: unknown): v is FollowingRule { return ( From 061865c8bf13a88640261fd3a98ef35c436d9910 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 20 Sep 2023 10:22:27 -0700 Subject: [PATCH 20/46] Add feed-vew and thread-view preferences (#1638) * Add feed and thread preference lexicons * Add feed-view and thread-view preference APIs --- lexicons/app/bsky/actor/defs.json | 48 +- packages/api/src/bsky-agent.ts | 70 +++ packages/api/src/client/lexicons.ts | 49 ++ .../src/client/types/app/bsky/actor/defs.ts | 50 ++ packages/api/src/types.ts | 3 + packages/api/tests/bsky-agent.test.ts | 508 +++++++++++++++++- packages/bsky/src/lexicon/lexicons.ts | 49 ++ .../src/lexicon/types/app/bsky/actor/defs.ts | 50 ++ packages/pds/src/lexicon/lexicons.ts | 49 ++ .../src/lexicon/types/app/bsky/actor/defs.ts | 50 ++ 10 files changed, 908 insertions(+), 18 deletions(-) diff --git a/lexicons/app/bsky/actor/defs.json b/lexicons/app/bsky/actor/defs.json index 6b6017d049d..eada4e53897 100644 --- a/lexicons/app/bsky/actor/defs.json +++ b/lexicons/app/bsky/actor/defs.json @@ -98,7 +98,9 @@ "#adultContentPref", "#contentLabelPref", "#savedFeedsPref", - "#personalDetailsPref" + "#personalDetailsPref", + "#feedViewPref", + "#threadViewPref" ] } }, @@ -149,6 +151,50 @@ "description": "The birth date of the owner of the account." } } + }, + "feedViewPref": { + "type": "object", + "required": ["feed"], + "properties": { + "feed": { + "type": "string", + "description": "The URI of the feed, or an identifier which describes the feed." + }, + "hideReplies": { + "type": "boolean", + "description": "Hide replies in the feed." + }, + "hideRepliesByUnfollowed": { + "type": "boolean", + "description": "Hide replies in the feed if they are not by followed users." + }, + "hideRepliesByLikeCount": { + "type": "integer", + "description": "Hide replies in the feed if they do not have this number of likes." + }, + "hideReposts": { + "type": "boolean", + "description": "Hide reposts in the feed." + }, + "hideQuotePosts": { + "type": "boolean", + "description": "Hide quote posts in the feed." + } + } + }, + "threadViewPref": { + "type": "object", + "properties": { + "sort": { + "type": "string", + "description": "Sorting mode.", + "knownValues": ["oldest", "newest", "most-likes", "random"] + }, + "prioritizeFollowedUsers": { + "type": "boolean", + "description": "Show followed users at the top of all replies." + } + } } } } diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index b7dd1bc1931..7ccbf43f71b 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -8,6 +8,18 @@ import { } from './client' import { BskyPreferences, BskyLabelPreference } from './types' +const FEED_VIEW_PREF_DEFAULTS = { + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, +} +const THREAD_VIEW_PREF_DEFAULTS = { + sort: 'oldest', + prioritizeFollowedUsers: true, +} + declare global { interface Array { findLast( @@ -254,6 +266,12 @@ export class BskyAgent extends AtpAgent { saved: undefined, pinned: undefined, }, + feedViewPrefs: { + home: { + ...FEED_VIEW_PREF_DEFAULTS, + }, + }, + threadViewPrefs: { ...THREAD_VIEW_PREF_DEFAULTS }, adultContentEnabled: false, contentLabels: {}, birthDate: undefined, @@ -289,6 +307,18 @@ export class BskyAgent extends AtpAgent { if (pref.birthDate) { prefs.birthDate = new Date(pref.birthDate) } + } else if ( + AppBskyActorDefs.isFeedViewPref(pref) && + AppBskyActorDefs.validateFeedViewPref(pref).success + ) { + const { $type, feed, ...v } = pref + prefs.feedViewPrefs[pref.feed] = { ...FEED_VIEW_PREF_DEFAULTS, ...v } + } else if ( + AppBskyActorDefs.isThreadViewPref(pref) && + AppBskyActorDefs.validateThreadViewPref(pref).success + ) { + const { $type, ...v } = pref + prefs.threadViewPrefs = { ...prefs.threadViewPrefs, ...v } } } return prefs @@ -406,6 +436,46 @@ export class BskyAgent extends AtpAgent { .concat([personalDetailsPref]) }) } + + async setFeedViewPrefs( + feed: string, + pref: Omit, + ) { + await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => { + const existing = prefs.findLast( + (pref) => + AppBskyActorDefs.isFeedViewPref(pref) && + AppBskyActorDefs.validateFeedViewPref(pref).success && + pref.feed === feed, + ) + if (existing) { + pref = { ...existing, ...pref } + } + return prefs + .filter( + (p) => !AppBskyActorDefs.isFeedViewPref(pref) || p.feed !== feed, + ) + .concat([{ ...pref, $type: 'app.bsky.actor.defs#feedViewPref', feed }]) + }) + } + + async setThreadViewPrefs( + pref: Omit, + ) { + await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => { + const existing = prefs.findLast( + (pref) => + AppBskyActorDefs.isThreadViewPref(pref) && + AppBskyActorDefs.validateThreadViewPref(pref).success, + ) + if (existing) { + pref = { ...existing, ...pref } + } + return prefs + .filter((p) => !AppBskyActorDefs.isThreadViewPref(p)) + .concat([{ ...pref, $type: 'app.bsky.actor.defs#threadViewPref' }]) + }) + } } /** diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 7ee31c74e62..48b1276adc8 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -3687,6 +3687,8 @@ export const schemaDict = { 'lex:app.bsky.actor.defs#contentLabelPref', 'lex:app.bsky.actor.defs#savedFeedsPref', 'lex:app.bsky.actor.defs#personalDetailsPref', + 'lex:app.bsky.actor.defs#feedViewPref', + 'lex:app.bsky.actor.defs#threadViewPref', ], }, }, @@ -3743,6 +3745,53 @@ export const schemaDict = { }, }, }, + feedViewPref: { + type: 'object', + required: ['feed'], + properties: { + feed: { + type: 'string', + description: + 'The URI of the feed, or an identifier which describes the feed.', + }, + hideReplies: { + type: 'boolean', + description: 'Hide replies in the feed.', + }, + hideRepliesByUnfollowed: { + type: 'boolean', + description: + 'Hide replies in the feed if they are not by followed users.', + }, + hideRepliesByLikeCount: { + type: 'integer', + description: + 'Hide replies in the feed if they do not have this number of likes.', + }, + hideReposts: { + type: 'boolean', + description: 'Hide reposts in the feed.', + }, + hideQuotePosts: { + type: 'boolean', + description: 'Hide quote posts in the feed.', + }, + }, + }, + threadViewPref: { + type: 'object', + properties: { + sort: { + type: 'string', + description: 'Sorting mode.', + knownValues: ['oldest', 'newest', 'most-likes', 'random'], + }, + prioritizeFollowedUsers: { + type: 'boolean', + description: 'Show followed users at the top of all replies.', + }, + }, + }, }, }, AppBskyActorGetPreferences: { diff --git a/packages/api/src/client/types/app/bsky/actor/defs.ts b/packages/api/src/client/types/app/bsky/actor/defs.ts index 7d3c9fcaac6..340010680d0 100644 --- a/packages/api/src/client/types/app/bsky/actor/defs.ts +++ b/packages/api/src/client/types/app/bsky/actor/defs.ts @@ -109,6 +109,8 @@ export type Preferences = ( | ContentLabelPref | SavedFeedsPref | PersonalDetailsPref + | FeedViewPref + | ThreadViewPref | { $type: string; [k: string]: unknown } )[] @@ -182,3 +184,51 @@ export function isPersonalDetailsPref(v: unknown): v is PersonalDetailsPref { export function validatePersonalDetailsPref(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#personalDetailsPref', v) } + +export interface FeedViewPref { + /** The URI of the feed, or an identifier which describes the feed. */ + feed: string + /** Hide replies in the feed. */ + hideReplies?: boolean + /** Hide replies in the feed if they are not by followed users. */ + hideRepliesByUnfollowed?: boolean + /** Hide replies in the feed if they do not have this number of likes. */ + hideRepliesByLikeCount?: number + /** Hide reposts in the feed. */ + hideReposts?: boolean + /** Hide quote posts in the feed. */ + hideQuotePosts?: boolean + [k: string]: unknown +} + +export function isFeedViewPref(v: unknown): v is FeedViewPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#feedViewPref' + ) +} + +export function validateFeedViewPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#feedViewPref', v) +} + +export interface ThreadViewPref { + /** Sorting mode. */ + sort?: 'oldest' | 'newest' | 'most-likes' | 'random' | (string & {}) + /** Show followed users at the top of all replies. */ + prioritizeFollowedUsers?: boolean + [k: string]: unknown +} + +export function isThreadViewPref(v: unknown): v is ThreadViewPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#threadViewPref' + ) +} + +export function validateThreadViewPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#threadViewPref', v) +} diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index 0310d6743b8..985ceb44c31 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -1,4 +1,5 @@ import { LabelPreference } from './moderation/types' +import { AppBskyActorDefs } from './client' /** * Used by the PersistSessionHandler to indicate what change occurred @@ -87,6 +88,8 @@ export interface BskyPreferences { saved?: string[] pinned?: string[] } + feedViewPrefs: Record> + threadViewPrefs: Omit adultContentEnabled: boolean contentLabels: Record birthDate: Date | undefined diff --git a/packages/api/tests/bsky-agent.test.ts b/packages/api/tests/bsky-agent.test.ts index 24b40153458..33a416574d8 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/bsky-agent.test.ts @@ -216,6 +216,19 @@ describe('agent', () => { adultContentEnabled: false, contentLabels: {}, birthDate: undefined, + feedViewPrefs: { + home: { + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + }, + threadViewPrefs: { + sort: 'oldest', + prioritizeFollowedUsers: true, + }, }) await agent.setAdultContentEnabled(true) @@ -224,6 +237,19 @@ describe('agent', () => { adultContentEnabled: true, contentLabels: {}, birthDate: undefined, + feedViewPrefs: { + home: { + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + }, + threadViewPrefs: { + sort: 'oldest', + prioritizeFollowedUsers: true, + }, }) await agent.setAdultContentEnabled(false) @@ -232,6 +258,19 @@ describe('agent', () => { adultContentEnabled: false, contentLabels: {}, birthDate: undefined, + feedViewPrefs: { + home: { + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + }, + threadViewPrefs: { + sort: 'oldest', + prioritizeFollowedUsers: true, + }, }) await agent.setContentLabelPref('impersonation', 'warn') @@ -242,6 +281,19 @@ describe('agent', () => { impersonation: 'warn', }, birthDate: undefined, + feedViewPrefs: { + home: { + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + }, + threadViewPrefs: { + sort: 'oldest', + prioritizeFollowedUsers: true, + }, }) await agent.setContentLabelPref('spam', 'show') // will convert to 'ignore' @@ -254,6 +306,19 @@ describe('agent', () => { spam: 'ignore', }, birthDate: undefined, + feedViewPrefs: { + home: { + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + }, + threadViewPrefs: { + sort: 'oldest', + prioritizeFollowedUsers: true, + }, }) await agent.addSavedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -268,6 +333,19 @@ describe('agent', () => { spam: 'ignore', }, birthDate: undefined, + feedViewPrefs: { + home: { + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + }, + threadViewPrefs: { + sort: 'oldest', + prioritizeFollowedUsers: true, + }, }) await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -282,6 +360,19 @@ describe('agent', () => { spam: 'ignore', }, birthDate: undefined, + feedViewPrefs: { + home: { + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + }, + threadViewPrefs: { + sort: 'oldest', + prioritizeFollowedUsers: true, + }, }) await agent.removePinnedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -296,6 +387,19 @@ describe('agent', () => { spam: 'ignore', }, birthDate: undefined, + feedViewPrefs: { + home: { + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + }, + threadViewPrefs: { + sort: 'oldest', + prioritizeFollowedUsers: true, + }, }) await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -310,6 +414,19 @@ describe('agent', () => { spam: 'ignore', }, birthDate: undefined, + feedViewPrefs: { + home: { + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + }, + threadViewPrefs: { + sort: 'oldest', + prioritizeFollowedUsers: true, + }, }) await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -324,6 +441,19 @@ describe('agent', () => { spam: 'ignore', }, birthDate: undefined, + feedViewPrefs: { + home: { + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + }, + threadViewPrefs: { + sort: 'oldest', + prioritizeFollowedUsers: true, + }, }) await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake2') @@ -344,6 +474,19 @@ describe('agent', () => { spam: 'ignore', }, birthDate: undefined, + feedViewPrefs: { + home: { + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + }, + threadViewPrefs: { + sort: 'oldest', + prioritizeFollowedUsers: true, + }, }) await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -358,6 +501,19 @@ describe('agent', () => { spam: 'ignore', }, birthDate: undefined, + feedViewPrefs: { + home: { + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + }, + threadViewPrefs: { + sort: 'oldest', + prioritizeFollowedUsers: true, + }, }) await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' }) @@ -372,6 +528,175 @@ describe('agent', () => { spam: 'ignore', }, birthDate: new Date('2023-09-11T18:05:42.556Z'), + feedViewPrefs: { + home: { + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + }, + threadViewPrefs: { + sort: 'oldest', + prioritizeFollowedUsers: true, + }, + }) + + await agent.setFeedViewPrefs('home', { hideReplies: true }) + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: ['at://bob.com/app.bsky.feed.generator/fake2'], + saved: ['at://bob.com/app.bsky.feed.generator/fake2'], + }, + adultContentEnabled: false, + contentLabels: { + impersonation: 'hide', + spam: 'ignore', + }, + birthDate: new Date('2023-09-11T18:05:42.556Z'), + feedViewPrefs: { + home: { + hideReplies: true, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + }, + threadViewPrefs: { + sort: 'oldest', + prioritizeFollowedUsers: true, + }, + }) + + await agent.setFeedViewPrefs('home', { hideReplies: false }) + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: ['at://bob.com/app.bsky.feed.generator/fake2'], + saved: ['at://bob.com/app.bsky.feed.generator/fake2'], + }, + adultContentEnabled: false, + contentLabels: { + impersonation: 'hide', + spam: 'ignore', + }, + birthDate: new Date('2023-09-11T18:05:42.556Z'), + feedViewPrefs: { + home: { + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + }, + threadViewPrefs: { + sort: 'oldest', + prioritizeFollowedUsers: true, + }, + }) + + await agent.setFeedViewPrefs('other', { hideReplies: true }) + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: ['at://bob.com/app.bsky.feed.generator/fake2'], + saved: ['at://bob.com/app.bsky.feed.generator/fake2'], + }, + adultContentEnabled: false, + contentLabels: { + impersonation: 'hide', + spam: 'ignore', + }, + birthDate: new Date('2023-09-11T18:05:42.556Z'), + feedViewPrefs: { + home: { + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + other: { + hideReplies: true, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + }, + threadViewPrefs: { + sort: 'oldest', + prioritizeFollowedUsers: true, + }, + }) + + await agent.setThreadViewPrefs({ sort: 'random' }) + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: ['at://bob.com/app.bsky.feed.generator/fake2'], + saved: ['at://bob.com/app.bsky.feed.generator/fake2'], + }, + adultContentEnabled: false, + contentLabels: { + impersonation: 'hide', + spam: 'ignore', + }, + birthDate: new Date('2023-09-11T18:05:42.556Z'), + feedViewPrefs: { + home: { + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + other: { + hideReplies: true, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + }, + threadViewPrefs: { + sort: 'random', + prioritizeFollowedUsers: true, + }, + }) + + await agent.setThreadViewPrefs({ sort: 'oldest' }) + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: ['at://bob.com/app.bsky.feed.generator/fake2'], + saved: ['at://bob.com/app.bsky.feed.generator/fake2'], + }, + adultContentEnabled: false, + contentLabels: { + impersonation: 'hide', + spam: 'ignore', + }, + birthDate: new Date('2023-09-11T18:05:42.556Z'), + feedViewPrefs: { + home: { + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + other: { + hideReplies: true, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + }, + threadViewPrefs: { + sort: 'oldest', + prioritizeFollowedUsers: true, + }, }) }) @@ -442,6 +767,34 @@ describe('agent', () => { $type: 'app.bsky.actor.defs#personalDetailsPref', birthDate: '2021-09-11T18:05:42.556Z', }, + { + $type: 'app.bsky.actor.defs#feedViewPref', + feed: 'home', + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + { + $type: 'app.bsky.actor.defs#feedViewPref', + feed: 'home', + hideReplies: true, + hideRepliesByUnfollowed: true, + hideRepliesByLikeCount: 10, + hideReposts: true, + hideQuotePosts: true, + }, + { + $type: 'app.bsky.actor.defs#threadViewPref', + sort: 'oldest', + prioritizeFollowedUsers: true, + }, + { + $type: 'app.bsky.actor.defs#threadViewPref', + sort: 'newest', + prioritizeFollowedUsers: false, + }, ], }) await expect(agent.getPreferences()).resolves.toStrictEqual({ @@ -454,6 +807,19 @@ describe('agent', () => { nsfw: 'warn', }, birthDate: new Date('2021-09-11T18:05:42.556Z'), + feedViewPrefs: { + home: { + hideReplies: true, + hideRepliesByUnfollowed: true, + hideRepliesByLikeCount: 10, + hideReposts: true, + hideQuotePosts: true, + }, + }, + threadViewPrefs: { + sort: 'newest', + prioritizeFollowedUsers: false, + }, }) await agent.setAdultContentEnabled(false) @@ -467,6 +833,19 @@ describe('agent', () => { nsfw: 'warn', }, birthDate: new Date('2021-09-11T18:05:42.556Z'), + feedViewPrefs: { + home: { + hideReplies: true, + hideRepliesByUnfollowed: true, + hideRepliesByLikeCount: 10, + hideReposts: true, + hideQuotePosts: true, + }, + }, + threadViewPrefs: { + sort: 'newest', + prioritizeFollowedUsers: false, + }, }) await agent.setContentLabelPref('nsfw', 'hide') @@ -480,6 +859,19 @@ describe('agent', () => { nsfw: 'hide', }, birthDate: new Date('2021-09-11T18:05:42.556Z'), + feedViewPrefs: { + home: { + hideReplies: true, + hideRepliesByUnfollowed: true, + hideRepliesByLikeCount: 10, + hideReposts: true, + hideQuotePosts: true, + }, + }, + threadViewPrefs: { + sort: 'newest', + prioritizeFollowedUsers: false, + }, }) await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -493,6 +885,19 @@ describe('agent', () => { nsfw: 'hide', }, birthDate: new Date('2021-09-11T18:05:42.556Z'), + feedViewPrefs: { + home: { + hideReplies: true, + hideRepliesByUnfollowed: true, + hideRepliesByLikeCount: 10, + hideReposts: true, + hideQuotePosts: true, + }, + }, + threadViewPrefs: { + sort: 'newest', + prioritizeFollowedUsers: false, + }, }) await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' }) @@ -506,29 +911,98 @@ describe('agent', () => { nsfw: 'hide', }, birthDate: new Date('2023-09-11T18:05:42.556Z'), + feedViewPrefs: { + home: { + hideReplies: true, + hideRepliesByUnfollowed: true, + hideRepliesByLikeCount: 10, + hideReposts: true, + hideQuotePosts: true, + }, + }, + threadViewPrefs: { + sort: 'newest', + prioritizeFollowedUsers: false, + }, }) - const res = await agent.app.bsky.actor.getPreferences() - await expect(res.data.preferences).toStrictEqual([ - { - $type: 'app.bsky.actor.defs#adultContentPref', - enabled: false, - }, - { - $type: 'app.bsky.actor.defs#contentLabelPref', - label: 'nsfw', - visibility: 'hide', - }, - { - $type: 'app.bsky.actor.defs#savedFeedsPref', + await agent.setFeedViewPrefs('home', { + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }) + await agent.setThreadViewPrefs({ + sort: 'oldest', + prioritizeFollowedUsers: true, + }) + await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' }) + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { pinned: ['at://bob.com/app.bsky.feed.generator/fake'], saved: ['at://bob.com/app.bsky.feed.generator/fake'], }, - { - $type: 'app.bsky.actor.defs#personalDetailsPref', - birthDate: '2023-09-11T18:05:42.556Z', + adultContentEnabled: false, + contentLabels: { + nsfw: 'hide', }, - ]) + birthDate: new Date('2023-09-11T18:05:42.556Z'), + feedViewPrefs: { + home: { + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + }, + threadViewPrefs: { + sort: 'oldest', + prioritizeFollowedUsers: true, + }, + }) + + const res = await agent.app.bsky.actor.getPreferences() + await expect(res.data.preferences.sort(byType)).toStrictEqual( + [ + { + $type: 'app.bsky.actor.defs#adultContentPref', + enabled: false, + }, + { + $type: 'app.bsky.actor.defs#contentLabelPref', + label: 'nsfw', + visibility: 'hide', + }, + { + $type: 'app.bsky.actor.defs#savedFeedsPref', + pinned: ['at://bob.com/app.bsky.feed.generator/fake'], + saved: ['at://bob.com/app.bsky.feed.generator/fake'], + }, + { + $type: 'app.bsky.actor.defs#personalDetailsPref', + birthDate: '2023-09-11T18:05:42.556Z', + }, + + { + $type: 'app.bsky.actor.defs#feedViewPref', + feed: 'home', + hideReplies: false, + hideRepliesByUnfollowed: false, + hideRepliesByLikeCount: 0, + hideReposts: false, + hideQuotePosts: false, + }, + { + $type: 'app.bsky.actor.defs#threadViewPref', + sort: 'oldest', + prioritizeFollowedUsers: true, + }, + ].sort(byType), + ) }) }) }) + +const byType = (a, b) => a.$type.localeCompare(b.$type) diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 7ee31c74e62..48b1276adc8 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -3687,6 +3687,8 @@ export const schemaDict = { 'lex:app.bsky.actor.defs#contentLabelPref', 'lex:app.bsky.actor.defs#savedFeedsPref', 'lex:app.bsky.actor.defs#personalDetailsPref', + 'lex:app.bsky.actor.defs#feedViewPref', + 'lex:app.bsky.actor.defs#threadViewPref', ], }, }, @@ -3743,6 +3745,53 @@ export const schemaDict = { }, }, }, + feedViewPref: { + type: 'object', + required: ['feed'], + properties: { + feed: { + type: 'string', + description: + 'The URI of the feed, or an identifier which describes the feed.', + }, + hideReplies: { + type: 'boolean', + description: 'Hide replies in the feed.', + }, + hideRepliesByUnfollowed: { + type: 'boolean', + description: + 'Hide replies in the feed if they are not by followed users.', + }, + hideRepliesByLikeCount: { + type: 'integer', + description: + 'Hide replies in the feed if they do not have this number of likes.', + }, + hideReposts: { + type: 'boolean', + description: 'Hide reposts in the feed.', + }, + hideQuotePosts: { + type: 'boolean', + description: 'Hide quote posts in the feed.', + }, + }, + }, + threadViewPref: { + type: 'object', + properties: { + sort: { + type: 'string', + description: 'Sorting mode.', + knownValues: ['oldest', 'newest', 'most-likes', 'random'], + }, + prioritizeFollowedUsers: { + type: 'boolean', + description: 'Show followed users at the top of all replies.', + }, + }, + }, }, }, AppBskyActorGetPreferences: { diff --git a/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts b/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts index 4446c1f7a03..b24b04b34d7 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts @@ -109,6 +109,8 @@ export type Preferences = ( | ContentLabelPref | SavedFeedsPref | PersonalDetailsPref + | FeedViewPref + | ThreadViewPref | { $type: string; [k: string]: unknown } )[] @@ -182,3 +184,51 @@ export function isPersonalDetailsPref(v: unknown): v is PersonalDetailsPref { export function validatePersonalDetailsPref(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#personalDetailsPref', v) } + +export interface FeedViewPref { + /** The URI of the feed, or an identifier which describes the feed. */ + feed: string + /** Hide replies in the feed. */ + hideReplies?: boolean + /** Hide replies in the feed if they are not by followed users. */ + hideRepliesByUnfollowed?: boolean + /** Hide replies in the feed if they do not have this number of likes. */ + hideRepliesByLikeCount?: number + /** Hide reposts in the feed. */ + hideReposts?: boolean + /** Hide quote posts in the feed. */ + hideQuotePosts?: boolean + [k: string]: unknown +} + +export function isFeedViewPref(v: unknown): v is FeedViewPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#feedViewPref' + ) +} + +export function validateFeedViewPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#feedViewPref', v) +} + +export interface ThreadViewPref { + /** Sorting mode. */ + sort?: 'oldest' | 'newest' | 'most-likes' | 'random' | (string & {}) + /** Show followed users at the top of all replies. */ + prioritizeFollowedUsers?: boolean + [k: string]: unknown +} + +export function isThreadViewPref(v: unknown): v is ThreadViewPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#threadViewPref' + ) +} + +export function validateThreadViewPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#threadViewPref', v) +} diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 7ee31c74e62..48b1276adc8 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -3687,6 +3687,8 @@ export const schemaDict = { 'lex:app.bsky.actor.defs#contentLabelPref', 'lex:app.bsky.actor.defs#savedFeedsPref', 'lex:app.bsky.actor.defs#personalDetailsPref', + 'lex:app.bsky.actor.defs#feedViewPref', + 'lex:app.bsky.actor.defs#threadViewPref', ], }, }, @@ -3743,6 +3745,53 @@ export const schemaDict = { }, }, }, + feedViewPref: { + type: 'object', + required: ['feed'], + properties: { + feed: { + type: 'string', + description: + 'The URI of the feed, or an identifier which describes the feed.', + }, + hideReplies: { + type: 'boolean', + description: 'Hide replies in the feed.', + }, + hideRepliesByUnfollowed: { + type: 'boolean', + description: + 'Hide replies in the feed if they are not by followed users.', + }, + hideRepliesByLikeCount: { + type: 'integer', + description: + 'Hide replies in the feed if they do not have this number of likes.', + }, + hideReposts: { + type: 'boolean', + description: 'Hide reposts in the feed.', + }, + hideQuotePosts: { + type: 'boolean', + description: 'Hide quote posts in the feed.', + }, + }, + }, + threadViewPref: { + type: 'object', + properties: { + sort: { + type: 'string', + description: 'Sorting mode.', + knownValues: ['oldest', 'newest', 'most-likes', 'random'], + }, + prioritizeFollowedUsers: { + type: 'boolean', + description: 'Show followed users at the top of all replies.', + }, + }, + }, }, }, AppBskyActorGetPreferences: { diff --git a/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts b/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts index 4446c1f7a03..b24b04b34d7 100644 --- a/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts @@ -109,6 +109,8 @@ export type Preferences = ( | ContentLabelPref | SavedFeedsPref | PersonalDetailsPref + | FeedViewPref + | ThreadViewPref | { $type: string; [k: string]: unknown } )[] @@ -182,3 +184,51 @@ export function isPersonalDetailsPref(v: unknown): v is PersonalDetailsPref { export function validatePersonalDetailsPref(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#personalDetailsPref', v) } + +export interface FeedViewPref { + /** The URI of the feed, or an identifier which describes the feed. */ + feed: string + /** Hide replies in the feed. */ + hideReplies?: boolean + /** Hide replies in the feed if they are not by followed users. */ + hideRepliesByUnfollowed?: boolean + /** Hide replies in the feed if they do not have this number of likes. */ + hideRepliesByLikeCount?: number + /** Hide reposts in the feed. */ + hideReposts?: boolean + /** Hide quote posts in the feed. */ + hideQuotePosts?: boolean + [k: string]: unknown +} + +export function isFeedViewPref(v: unknown): v is FeedViewPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#feedViewPref' + ) +} + +export function validateFeedViewPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#feedViewPref', v) +} + +export interface ThreadViewPref { + /** Sorting mode. */ + sort?: 'oldest' | 'newest' | 'most-likes' | 'random' | (string & {}) + /** Show followed users at the top of all replies. */ + prioritizeFollowedUsers?: boolean + [k: string]: unknown +} + +export function isThreadViewPref(v: unknown): v is ThreadViewPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#threadViewPref' + ) +} + +export function validateThreadViewPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#threadViewPref', v) +} From 2cc329f26547217dd94b6bb11ee590d707cbd14f Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 20 Sep 2023 11:07:06 -0700 Subject: [PATCH 21/46] Add changeset for new preferences (#1639) Add changeset --- .changeset/quiet-carrots-leave.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/quiet-carrots-leave.md diff --git a/.changeset/quiet-carrots-leave.md b/.changeset/quiet-carrots-leave.md new file mode 100644 index 00000000000..6376ee6f5b0 --- /dev/null +++ b/.changeset/quiet-carrots-leave.md @@ -0,0 +1,5 @@ +--- +'@atproto/api': patch +--- + +Added new preferences for feed and thread view behaviors. From 49278430e037f4090c8a2e4fcb24ea53c23fe024 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 20 Sep 2023 11:12:04 -0700 Subject: [PATCH 22/46] Version packages (#1640) Co-authored-by: github-actions[bot] --- .changeset/quiet-carrots-leave.md | 5 ----- packages/api/CHANGELOG.md | 6 ++++++ packages/api/package.json | 2 +- packages/bsky/CHANGELOG.md | 7 +++++++ packages/bsky/package.json | 2 +- packages/dev-env/CHANGELOG.md | 9 +++++++++ packages/dev-env/package.json | 2 +- packages/pds/CHANGELOG.md | 7 +++++++ packages/pds/package.json | 2 +- 9 files changed, 33 insertions(+), 9 deletions(-) delete mode 100644 .changeset/quiet-carrots-leave.md diff --git a/.changeset/quiet-carrots-leave.md b/.changeset/quiet-carrots-leave.md deleted file mode 100644 index 6376ee6f5b0..00000000000 --- a/.changeset/quiet-carrots-leave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@atproto/api': patch ---- - -Added new preferences for feed and thread view behaviors. diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index 19149433dca..ab2448d2bbb 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -1,5 +1,11 @@ # @atproto/api +## 0.6.15 + +### Patch Changes + +- [#1639](https://github.com/bluesky-social/atproto/pull/1639) [`2cc329f2`](https://github.com/bluesky-social/atproto/commit/2cc329f26547217dd94b6bb11ee590d707cbd14f) Thanks [@pfrazee](https://github.com/pfrazee)! - Added new preferences for feed and thread view behaviors. + ## 0.6.14 ### Patch Changes diff --git a/packages/api/package.json b/packages/api/package.json index 8f3c00fbab4..5e34b416d71 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.6.14", + "version": "0.6.15", "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", diff --git a/packages/bsky/CHANGELOG.md b/packages/bsky/CHANGELOG.md index 6841f093f0d..df07140737f 100644 --- a/packages/bsky/CHANGELOG.md +++ b/packages/bsky/CHANGELOG.md @@ -1,5 +1,12 @@ # @atproto/bsky +## 0.0.6 + +### Patch Changes + +- Updated dependencies [[`2cc329f2`](https://github.com/bluesky-social/atproto/commit/2cc329f26547217dd94b6bb11ee590d707cbd14f)]: + - @atproto/api@0.6.15 + ## 0.0.5 ### Patch Changes diff --git a/packages/bsky/package.json b/packages/bsky/package.json index e9309577801..2ee0f9bd7d3 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/bsky", - "version": "0.0.5", + "version": "0.0.6", "license": "MIT", "repository": { "type": "git", diff --git a/packages/dev-env/CHANGELOG.md b/packages/dev-env/CHANGELOG.md index 1d3f46c8f1f..d12edd580ef 100644 --- a/packages/dev-env/CHANGELOG.md +++ b/packages/dev-env/CHANGELOG.md @@ -1,5 +1,14 @@ # @atproto/dev-env +## 0.2.6 + +### Patch Changes + +- Updated dependencies [[`2cc329f2`](https://github.com/bluesky-social/atproto/commit/2cc329f26547217dd94b6bb11ee590d707cbd14f)]: + - @atproto/api@0.6.15 + - @atproto/bsky@0.0.6 + - @atproto/pds@0.1.15 + ## 0.2.5 ### Patch Changes diff --git a/packages/dev-env/package.json b/packages/dev-env/package.json index d5313408135..ad7b0f420b0 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/dev-env", - "version": "0.2.5", + "version": "0.2.6", "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", diff --git a/packages/pds/CHANGELOG.md b/packages/pds/CHANGELOG.md index ad324f11cf8..3aa0bc4d812 100644 --- a/packages/pds/CHANGELOG.md +++ b/packages/pds/CHANGELOG.md @@ -1,5 +1,12 @@ # @atproto/pds +## 0.1.15 + +### Patch Changes + +- Updated dependencies [[`2cc329f2`](https://github.com/bluesky-social/atproto/commit/2cc329f26547217dd94b6bb11ee590d707cbd14f)]: + - @atproto/api@0.6.15 + ## 0.1.14 ### Patch Changes diff --git a/packages/pds/package.json b/packages/pds/package.json index 422880426f9..469d1523d1d 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/pds", - "version": "0.1.14", + "version": "0.1.15", "license": "MIT", "repository": { "type": "git", From 4b132fc8090f047b2fdd3b48fd03f38af528fb6d Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Wed, 20 Sep 2023 19:57:01 -0500 Subject: [PATCH 23/46] Disable getAccountInviteCodes for app passwords (#1642) disable getAccountInviteCodes for app passwords --- .../pds/src/api/com/atproto/server/getAccountInviteCodes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/src/api/com/atproto/server/getAccountInviteCodes.ts b/packages/pds/src/api/com/atproto/server/getAccountInviteCodes.ts index c7269d095a7..671528fcdd4 100644 --- a/packages/pds/src/api/com/atproto/server/getAccountInviteCodes.ts +++ b/packages/pds/src/api/com/atproto/server/getAccountInviteCodes.ts @@ -6,7 +6,7 @@ import { CodeDetail } from '../../../../services/account' export default function (server: Server, ctx: AppContext) { server.com.atproto.server.getAccountInviteCodes({ - auth: ctx.accessVerifierCheckTakedown, + auth: ctx.accessVerifierNotAppPassword, handler: async ({ params, auth }) => { const requester = auth.credentials.did const { includeUsed, createAvailable } = params From d23f5ec3f15258701e85f1020426da4ee2919be7 Mon Sep 17 00:00:00 2001 From: bnewbold Date: Wed, 20 Sep 2023 18:32:12 -0700 Subject: [PATCH 24/46] remove cruft packages (uri, nsid, identifier) (#1606) * remove @atproto/nsid (previously moved to syntax) * remove @atproto/uri (previously moved to syntax) * remove @atproto/identifier (previously moved to syntax) * bump lockfile to remove old packages --------- Co-authored-by: Eric Bailey --- packages/identifier/CHANGELOG.md | 8 ------- packages/identifier/README.md | 22 ------------------ packages/identifier/babel.config.js | 1 - packages/identifier/build.js | 14 ----------- packages/identifier/jest.config.js | 6 ----- packages/identifier/package.json | 24 ------------------- packages/identifier/src/index.ts | 25 -------------------- packages/identifier/tsconfig.build.json | 4 ---- packages/identifier/tsconfig.json | 10 -------- packages/nsid/CHANGELOG.md | 8 ------- packages/nsid/README.md | 31 ------------------------- packages/nsid/babel.config.js | 1 - packages/nsid/build.js | 14 ----------- packages/nsid/jest.config.js | 6 ----- packages/nsid/package.json | 24 ------------------- packages/nsid/src/index.ts | 6 ----- packages/nsid/tsconfig.build.json | 4 ---- packages/nsid/tsconfig.json | 8 ------- packages/uri/CHANGELOG.md | 8 ------- packages/uri/README.md | 18 -------------- packages/uri/babel.config.js | 1 - packages/uri/build.js | 14 ----------- packages/uri/jest.config.js | 6 ----- packages/uri/package.json | 24 ------------------- packages/uri/src/index.ts | 6 ----- packages/uri/tsconfig.build.json | 4 ---- packages/uri/tsconfig.json | 13 ----------- pnpm-lock.yaml | 18 -------------- 28 files changed, 328 deletions(-) delete mode 100644 packages/identifier/CHANGELOG.md delete mode 100644 packages/identifier/README.md delete mode 100644 packages/identifier/babel.config.js delete mode 100644 packages/identifier/build.js delete mode 100644 packages/identifier/jest.config.js delete mode 100644 packages/identifier/package.json delete mode 100644 packages/identifier/src/index.ts delete mode 100644 packages/identifier/tsconfig.build.json delete mode 100644 packages/identifier/tsconfig.json delete mode 100644 packages/nsid/CHANGELOG.md delete mode 100644 packages/nsid/README.md delete mode 100644 packages/nsid/babel.config.js delete mode 100644 packages/nsid/build.js delete mode 100644 packages/nsid/jest.config.js delete mode 100644 packages/nsid/package.json delete mode 100644 packages/nsid/src/index.ts delete mode 100644 packages/nsid/tsconfig.build.json delete mode 100644 packages/nsid/tsconfig.json delete mode 100644 packages/uri/CHANGELOG.md delete mode 100644 packages/uri/README.md delete mode 100644 packages/uri/babel.config.js delete mode 100644 packages/uri/build.js delete mode 100644 packages/uri/jest.config.js delete mode 100644 packages/uri/package.json delete mode 100644 packages/uri/src/index.ts delete mode 100644 packages/uri/tsconfig.build.json delete mode 100644 packages/uri/tsconfig.json diff --git a/packages/identifier/CHANGELOG.md b/packages/identifier/CHANGELOG.md deleted file mode 100644 index f01512f9e96..00000000000 --- a/packages/identifier/CHANGELOG.md +++ /dev/null @@ -1,8 +0,0 @@ -# @atproto/identifier - -## 0.2.1 - -### Patch Changes - -- Updated dependencies [[`b1dc3555`](https://github.com/bluesky-social/atproto/commit/b1dc355504f9f2e047093dc56682b8034518cf80)]: - - @atproto/syntax@0.1.1 diff --git a/packages/identifier/README.md b/packages/identifier/README.md deleted file mode 100644 index cfbd0ac5cc6..00000000000 --- a/packages/identifier/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Identifier - -Validation logic for AT identifiers - DIDs & Handles - -## Usage - -```typescript -import * as identifier from '@atproto/identifier' - -isValidHandle('alice.test') // returns true -ensureValidHandle('alice.test') // returns void - -isValidHandle('al!ce.test') // returns false -ensureValidHandle('al!ce.test') // throws - -ensureValidDid('did:method:val') // returns void -ensureValidDid(':did:method:val') // throws -``` - -## License - -MIT diff --git a/packages/identifier/babel.config.js b/packages/identifier/babel.config.js deleted file mode 100644 index 0126e9dbaa6..00000000000 --- a/packages/identifier/babel.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../babel.config.js') diff --git a/packages/identifier/build.js b/packages/identifier/build.js deleted file mode 100644 index e880ae9930b..00000000000 --- a/packages/identifier/build.js +++ /dev/null @@ -1,14 +0,0 @@ -const { nodeExternalsPlugin } = require('esbuild-node-externals') - -const buildShallow = - process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true' - -require('esbuild').build({ - logLevel: 'info', - entryPoints: ['src/index.ts'], - bundle: true, - sourcemap: true, - outdir: 'dist', - platform: 'node', - plugins: buildShallow ? [nodeExternalsPlugin()] : [], -}) diff --git a/packages/identifier/jest.config.js b/packages/identifier/jest.config.js deleted file mode 100644 index 096d01562c4..00000000000 --- a/packages/identifier/jest.config.js +++ /dev/null @@ -1,6 +0,0 @@ -const base = require('../../jest.config.base.js') - -module.exports = { - ...base, - displayName: 'Identifier', -} diff --git a/packages/identifier/package.json b/packages/identifier/package.json deleted file mode 100644 index 074da48a9fc..00000000000 --- a/packages/identifier/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "@atproto/identifier", - "version": "0.2.1", - "main": "src/index.ts", - "publishConfig": { - "main": "dist/index.js", - "types": "dist/index.d.ts" - }, - "scripts": { - "test": "true", - "build": "node ./build.js", - "postbuild": "tsc --build tsconfig.build.json", - "update-main-to-dist": "node ../../update-main-to-dist.js packages/identifier" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/bluesky-social/atproto.git", - "directory": "packages/identifier" - }, - "dependencies": { - "@atproto/syntax": "workspace:^" - } -} diff --git a/packages/identifier/src/index.ts b/packages/identifier/src/index.ts deleted file mode 100644 index a590a9da09c..00000000000 --- a/packages/identifier/src/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -export { - ATP_URI_REGEX, - AtUri, - DISALLOWED_TLDS, - DisallowedDomainError, - INVALID_HANDLE, - InvalidDidError, - InvalidHandleError, - InvalidNsidError, - NSID, - ReservedHandleError, - UnsupportedDomainError, - ensureValidAtUri, - ensureValidAtUriRegex, - ensureValidDid, - ensureValidDidRegex, - ensureValidHandle, - ensureValidHandleRegex, - ensureValidNsid, - ensureValidNsidRegex, - isValidHandle, - isValidTld, - normalizeAndEnsureValidHandle, - normalizeHandle, -} from '@atproto/syntax' diff --git a/packages/identifier/tsconfig.build.json b/packages/identifier/tsconfig.build.json deleted file mode 100644 index 02a84823b65..00000000000 --- a/packages/identifier/tsconfig.build.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./tsconfig.json", - "exclude": ["**/*.spec.ts", "**/*.test.ts"] -} diff --git a/packages/identifier/tsconfig.json b/packages/identifier/tsconfig.json deleted file mode 100644 index db7a7c4ad35..00000000000 --- a/packages/identifier/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "rootDir": "./src", - "outDir": "./dist", // Your outDir, - "emitDeclarationOnly": true - }, - "include": ["./src", "__tests__/**/**.ts"], - "references": [{ "path": "../common/tsconfig.build.json" }] -} diff --git a/packages/nsid/CHANGELOG.md b/packages/nsid/CHANGELOG.md deleted file mode 100644 index b351d372ae5..00000000000 --- a/packages/nsid/CHANGELOG.md +++ /dev/null @@ -1,8 +0,0 @@ -# @atproto/nsid - -## 0.1.1 - -### Patch Changes - -- Updated dependencies [[`b1dc3555`](https://github.com/bluesky-social/atproto/commit/b1dc355504f9f2e047093dc56682b8034518cf80)]: - - @atproto/syntax@0.1.1 diff --git a/packages/nsid/README.md b/packages/nsid/README.md deleted file mode 100644 index 180a88338bb..00000000000 --- a/packages/nsid/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# NameSpaced IDs (NSID) API - -## Usage - -```typescript -import { NSID } from '@atproto/nsid' - -const id1 = NSID.parse('com.example.foo') -id1.authority // => 'example.com' -id1.name // => 'foo' -id1.toString() // => 'com.example.foo' - -const id2 = NSID.create('example.com', 'foo') -id2.authority // => 'example.com' -id2.name // => 'foo' -id2.toString() // => 'com.example.foo' - -const id3 = NSID.create('example.com', 'someRecord') -id3.authority // => 'example.com' -id3.name // => 'someRecord' -id3.toString() // => 'com.example.someRecord' - -NSID.isValid('com.example.foo') // => true -NSID.isValid('com.example.someRecord') // => true -NSID.isValid('example.com/foo') // => false -NSID.isValid('foo') // => false -``` - -## License - -MIT diff --git a/packages/nsid/babel.config.js b/packages/nsid/babel.config.js deleted file mode 100644 index 0126e9dbaa6..00000000000 --- a/packages/nsid/babel.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../babel.config.js') diff --git a/packages/nsid/build.js b/packages/nsid/build.js deleted file mode 100644 index e880ae9930b..00000000000 --- a/packages/nsid/build.js +++ /dev/null @@ -1,14 +0,0 @@ -const { nodeExternalsPlugin } = require('esbuild-node-externals') - -const buildShallow = - process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true' - -require('esbuild').build({ - logLevel: 'info', - entryPoints: ['src/index.ts'], - bundle: true, - sourcemap: true, - outdir: 'dist', - platform: 'node', - plugins: buildShallow ? [nodeExternalsPlugin()] : [], -}) diff --git a/packages/nsid/jest.config.js b/packages/nsid/jest.config.js deleted file mode 100644 index 8dcec1ddc1e..00000000000 --- a/packages/nsid/jest.config.js +++ /dev/null @@ -1,6 +0,0 @@ -const base = require('../../jest.config.base.js') - -module.exports = { - ...base, - displayName: 'NSID', -} diff --git a/packages/nsid/package.json b/packages/nsid/package.json deleted file mode 100644 index 8f0ce82f366..00000000000 --- a/packages/nsid/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "@atproto/nsid", - "version": "0.1.1", - "main": "src/index.ts", - "publishConfig": { - "main": "dist/index.js", - "types": "dist/src/index.d.ts" - }, - "scripts": { - "test": "true", - "build": "node ./build.js", - "postbuild": "tsc --build tsconfig.build.json", - "update-main-to-dist": "node ../../update-main-to-dist.js packages/nsid" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/bluesky-social/atproto.git", - "directory": "packages/nsid" - }, - "dependencies": { - "@atproto/syntax": "workspace:^" - } -} diff --git a/packages/nsid/src/index.ts b/packages/nsid/src/index.ts deleted file mode 100644 index 7a2efe52465..00000000000 --- a/packages/nsid/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { - NSID, - ensureValidNsid, - ensureValidNsidRegex, - InvalidNsidError, -} from '@atproto/syntax' diff --git a/packages/nsid/tsconfig.build.json b/packages/nsid/tsconfig.build.json deleted file mode 100644 index 02a84823b65..00000000000 --- a/packages/nsid/tsconfig.build.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./tsconfig.json", - "exclude": ["**/*.spec.ts", "**/*.test.ts"] -} diff --git a/packages/nsid/tsconfig.json b/packages/nsid/tsconfig.json deleted file mode 100644 index fee83b7f23b..00000000000 --- a/packages/nsid/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist", // Your outDir, - "emitDeclarationOnly": true - }, - "include": ["./src", "__tests__/**/**.ts"] -} diff --git a/packages/uri/CHANGELOG.md b/packages/uri/CHANGELOG.md deleted file mode 100644 index b6bf96be8a1..00000000000 --- a/packages/uri/CHANGELOG.md +++ /dev/null @@ -1,8 +0,0 @@ -# @atproto/uri - -## 0.1.1 - -### Patch Changes - -- Updated dependencies [[`b1dc3555`](https://github.com/bluesky-social/atproto/commit/b1dc355504f9f2e047093dc56682b8034518cf80)]: - - @atproto/syntax@0.1.1 diff --git a/packages/uri/README.md b/packages/uri/README.md deleted file mode 100644 index 8aaee47f7f2..00000000000 --- a/packages/uri/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# ATP URI API - -## Usage - -```typescript -import { AtUri } from '@atproto/uri' - -const uri = new AtUri('at://bob.com/com.example.post/1234') -uri.protocol // => 'at:' -uri.origin // => 'at://bob.com' -uri.hostname // => 'bob.com' -uri.collection // => 'com.example.post' -uri.rkey // => '1234' -``` - -## License - -MIT diff --git a/packages/uri/babel.config.js b/packages/uri/babel.config.js deleted file mode 100644 index 0126e9dbaa6..00000000000 --- a/packages/uri/babel.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../babel.config.js') diff --git a/packages/uri/build.js b/packages/uri/build.js deleted file mode 100644 index e880ae9930b..00000000000 --- a/packages/uri/build.js +++ /dev/null @@ -1,14 +0,0 @@ -const { nodeExternalsPlugin } = require('esbuild-node-externals') - -const buildShallow = - process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true' - -require('esbuild').build({ - logLevel: 'info', - entryPoints: ['src/index.ts'], - bundle: true, - sourcemap: true, - outdir: 'dist', - platform: 'node', - plugins: buildShallow ? [nodeExternalsPlugin()] : [], -}) diff --git a/packages/uri/jest.config.js b/packages/uri/jest.config.js deleted file mode 100644 index b2aa92b9eca..00000000000 --- a/packages/uri/jest.config.js +++ /dev/null @@ -1,6 +0,0 @@ -const base = require('../../jest.config.base.js') - -module.exports = { - ...base, - displayName: 'URI', -} diff --git a/packages/uri/package.json b/packages/uri/package.json deleted file mode 100644 index 65664ff5634..00000000000 --- a/packages/uri/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "@atproto/uri", - "version": "0.1.1", - "main": "src/index.ts", - "publishConfig": { - "main": "dist/index.js", - "types": "dist/index.d.ts" - }, - "scripts": { - "test": "true", - "build": "node ./build.js", - "postbuild": "tsc --build tsconfig.build.json", - "update-main-to-dist": "node ../../update-main-to-dist.js packages/uri" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/bluesky-social/atproto.git", - "directory": "packages/uri" - }, - "dependencies": { - "@atproto/syntax": "workspace:^" - } -} diff --git a/packages/uri/src/index.ts b/packages/uri/src/index.ts deleted file mode 100644 index 1c657b16049..00000000000 --- a/packages/uri/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { - ATP_URI_REGEX, - AtUri, - ensureValidAtUri, - ensureValidAtUriRegex, -} from '@atproto/syntax' diff --git a/packages/uri/tsconfig.build.json b/packages/uri/tsconfig.build.json deleted file mode 100644 index 02a84823b65..00000000000 --- a/packages/uri/tsconfig.build.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./tsconfig.json", - "exclude": ["**/*.spec.ts", "**/*.test.ts"] -} diff --git a/packages/uri/tsconfig.json b/packages/uri/tsconfig.json deleted file mode 100644 index 4faf3966f41..00000000000 --- a/packages/uri/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "rootDir": "./src", - "outDir": "./dist", // Your outDir, - "emitDeclarationOnly": true - }, - "include": ["./src", "__tests__/**/**.ts"], - "references": [ - { "path": "../identifier/tsconfig.build.json" }, - { "path": "../nsid/tsconfig.build.json" } - ] -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb80b8c6a35..843e3acd27e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -387,12 +387,6 @@ importers: specifier: ^10.8.1 version: 10.8.2(@swc/core@1.3.42)(@types/node@18.17.8)(typescript@4.9.5) - packages/identifier: - dependencies: - '@atproto/syntax': - specifier: workspace:^ - version: link:../syntax - packages/identity: dependencies: '@atproto/common-web': @@ -466,12 +460,6 @@ importers: specifier: ^3.21.4 version: 3.21.4 - packages/nsid: - dependencies: - '@atproto/syntax': - specifier: workspace:^ - version: link:../syntax - packages/pds: dependencies: '@atproto/api': @@ -668,12 +656,6 @@ importers: specifier: workspace:^ version: link:../common-web - packages/uri: - dependencies: - '@atproto/syntax': - specifier: workspace:^ - version: link:../syntax - packages/xrpc: dependencies: '@atproto/lexicon': From 462de219eedc3c862ca6f88ec130d55c2f6af954 Mon Sep 17 00:00:00 2001 From: bnewbold Date: Wed, 20 Sep 2023 19:15:12 -0700 Subject: [PATCH 25/46] api: update login/resumeSession examples in README (#1634) * api: update login/resumeSession examples in README * Update packages/api/README.md Co-authored-by: Daniel Holmgren --------- Co-authored-by: Daniel Holmgren --- packages/api/README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/api/README.md b/packages/api/README.md index fdfcbc48b73..069bdb50a5e 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -31,6 +31,8 @@ Log into a server or create accounts using these APIs. You'll need an active ses ```typescript import { BskyAgent, AtpSessionEvent, AtpSessionData } from '@atproto/api' + +// configure connection to the server, without account authentication const agent = new BskyAgent({ service: 'https://example.com', persistSession: (evt: AtpSessionEvent, sess?: AtpSessionData) => { @@ -38,13 +40,19 @@ const agent = new BskyAgent({ }, }) -await agent.login({ identifier: 'alice@mail.com', password: 'hunter2' }) -await agent.resumeSession(savedSessionData) +// create a new account on the server await agent.createAccount({ email: 'alice@mail.com', password: 'hunter2', handle: 'alice.example.com', + inviteCode: 'some-code-12345-abcde', }) + +// if an existing session (accessed with 'agent.session') was securely stored previously, then reuse that +await agent.resumeSession(savedSessionData) + +// if no old session was available, create a new one by logging in with password (App Password) +await agent.login({ identifier: 'alice@mail.com', password: 'hunter2' }) ``` ### API calls From 7e36c48a77a5ddb165800399eb28de8f761bf6b7 Mon Sep 17 00:00:00 2001 From: bnewbold Date: Thu, 21 Sep 2023 10:07:54 -0700 Subject: [PATCH 26/46] small syntax lints (#1646) * lint: remove unused imports and variables * lint: prefix unused args with '_' * eslint: skip no-explicit-any; ignore unused _var (prefix) * eslint: explicitly mark ignores for tricky cases --- .eslintrc | 5 +++-- packages/api/src/agent.ts | 1 - packages/api/src/bsky-agent.ts | 2 ++ .../api/src/moderation/subjects/feed-generator.ts | 4 ++-- packages/api/src/moderation/subjects/user-list.ts | 4 ++-- packages/api/src/moderation/util.ts | 6 +----- packages/api/src/rich-text/sanitization.ts | 2 ++ packages/api/tests/bsky-agent.test.ts | 6 +++--- .../api/com/atproto/admin/reverseModerationAction.ts | 1 - packages/bsky/tests/seeds/client.ts | 1 - packages/bsky/tests/views/actor-likes.test.ts | 6 ------ packages/common/tests/streams.test.ts | 2 +- packages/crypto/tests/signatures.test.ts | 1 + packages/lexicon/tests/general.test.ts | 1 - packages/pds/src/app-view/services/label/index.ts | 1 - packages/pds/src/feed-gen/with-friends.ts | 11 +++-------- packages/pds/src/services/moderation/views.ts | 1 - packages/pds/tests/views/actor-likes.test.ts | 2 -- packages/repo/tests/util.test.ts | 2 +- 19 files changed, 21 insertions(+), 38 deletions(-) diff --git a/.eslintrc b/.eslintrc index 8a278deb2c7..11c6e76318a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -29,11 +29,12 @@ "no-misleading-character-class": "warn", "@typescript-eslint/no-unused-vars": [ "warn", - { "argsIgnorePattern": "^_" } + { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" } ], "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-empty-function": "off" + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-explicit-any": "off" } } diff --git a/packages/api/src/agent.ts b/packages/api/src/agent.ts index 6d259046e1f..d5af9b63ddc 100644 --- a/packages/api/src/agent.ts +++ b/packages/api/src/agent.ts @@ -7,7 +7,6 @@ import { ComAtprotoServerCreateSession, ComAtprotoServerGetSession, ComAtprotoServerRefreshSession, - ComAtprotoRepoUploadBlob, } from './client' import { AtpSessionData, diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index 7ccbf43f71b..2ba8d7bf48c 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -311,12 +311,14 @@ export class BskyAgent extends AtpAgent { AppBskyActorDefs.isFeedViewPref(pref) && AppBskyActorDefs.validateFeedViewPref(pref).success ) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { $type, feed, ...v } = pref prefs.feedViewPrefs[pref.feed] = { ...FEED_VIEW_PREF_DEFAULTS, ...v } } else if ( AppBskyActorDefs.isThreadViewPref(pref) && AppBskyActorDefs.validateThreadViewPref(pref).success ) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { $type, ...v } = pref prefs.threadViewPrefs = { ...prefs.threadViewPrefs, ...v } } diff --git a/packages/api/src/moderation/subjects/feed-generator.ts b/packages/api/src/moderation/subjects/feed-generator.ts index 1b75d502810..ad1cde8d0de 100644 --- a/packages/api/src/moderation/subjects/feed-generator.ts +++ b/packages/api/src/moderation/subjects/feed-generator.ts @@ -5,8 +5,8 @@ import { } from '../types' export function decideFeedGenerator( - subject: ModerationSubjectFeedGenerator, - opts: ModerationOpts, + _subject: ModerationSubjectFeedGenerator, + _opts: ModerationOpts, ): ModerationDecision { // TODO handle labels applied on the feed generator itself return ModerationDecision.noop() diff --git a/packages/api/src/moderation/subjects/user-list.ts b/packages/api/src/moderation/subjects/user-list.ts index 20c48ae523f..a437fead036 100644 --- a/packages/api/src/moderation/subjects/user-list.ts +++ b/packages/api/src/moderation/subjects/user-list.ts @@ -5,8 +5,8 @@ import { } from '../types' export function decideUserList( - subject: ModerationSubjectUserList, - opts: ModerationOpts, + _subject: ModerationSubjectUserList, + _opts: ModerationOpts, ): ModerationDecision { // TODO handle labels applied on the list itself return ModerationDecision.noop() diff --git a/packages/api/src/moderation/util.ts b/packages/api/src/moderation/util.ts index 7b42f4dfffe..b567a886857 100644 --- a/packages/api/src/moderation/util.ts +++ b/packages/api/src/moderation/util.ts @@ -1,8 +1,4 @@ -import { - AppBskyEmbedRecord, - AppBskyEmbedRecordWithMedia, - AppBskyFeedPost, -} from '../client' +import { AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia } from '../client' import { ModerationDecision, ModerationUI } from './types' export function takeHighestPriorityDecision( diff --git a/packages/api/src/rich-text/sanitization.ts b/packages/api/src/rich-text/sanitization.ts index 48a52bc2f9e..31aa7fb0e7e 100644 --- a/packages/api/src/rich-text/sanitization.ts +++ b/packages/api/src/rich-text/sanitization.ts @@ -1,6 +1,8 @@ import { RichText } from './rich-text' import { UnicodeString } from './unicode' +// this regex is intentionally matching on the zero-with-separator codepoint +// eslint-disable-next-line no-misleading-character-class const EXCESS_SPACE_RE = /[\r\n]([\u00AD\u2060\u200D\u200C\u200B\s]*[\r\n]){2,}/ const REPLACEMENT_STR = '\n\n' diff --git a/packages/api/tests/bsky-agent.test.ts b/packages/api/tests/bsky-agent.test.ts index 33a416574d8..8ff041d3c14 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/bsky-agent.test.ts @@ -67,7 +67,7 @@ describe('agent', () => { let hasConflicted = false let ranTwice = false - await agent.upsertProfile(async (existing) => { + await agent.upsertProfile(async (_existing) => { if (!hasConflicted) { await agent.com.atproto.repo.putRecord({ repo: agent.session?.did || '', @@ -104,7 +104,7 @@ describe('agent', () => { const profile1 = await agent.getProfile({ actor: agent.session?.did || '' }) expect(profile1.data.displayName).toBeFalsy() - const p = agent.upsertProfile(async (existing) => { + const p = agent.upsertProfile(async (_existing) => { await agent.com.atproto.repo.putRecord({ repo: agent.session?.did || '', collection: 'app.bsky.actor.profile', @@ -130,7 +130,7 @@ describe('agent', () => { password: 'password', }) - const p = agent.upsertProfile((existing) => { + const p = agent.upsertProfile((_existing) => { return { displayName: { string: 'Bob' }, } as unknown as AppBskyActorProfile.Record diff --git a/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts b/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts index e0c70103359..bd478285204 100644 --- a/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts +++ b/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts @@ -1,4 +1,3 @@ -import { AtUri } from '@atproto/syntax' import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import { ACKNOWLEDGE, diff --git a/packages/bsky/tests/seeds/client.ts b/packages/bsky/tests/seeds/client.ts index b0a04db8c5e..ee551214789 100644 --- a/packages/bsky/tests/seeds/client.ts +++ b/packages/bsky/tests/seeds/client.ts @@ -9,7 +9,6 @@ import { InputSchema as CreateReportInput } from '@atproto/api/src/client/types/ import { Record as PostRecord } from '@atproto/api/src/client/types/app/bsky/feed/post' import { Record as LikeRecord } from '@atproto/api/src/client/types/app/bsky/feed/like' import { Record as FollowRecord } from '@atproto/api/src/client/types/app/bsky/graph/follow' -import { Record as ListRecord } from '@atproto/api/src/client/types/app/bsky/graph/list' // Makes it simple to create data via the XRPC client, // and keeps track of all created data in memory for convenience. diff --git a/packages/bsky/tests/views/actor-likes.test.ts b/packages/bsky/tests/views/actor-likes.test.ts index 774c3e63141..cf0281fdde3 100644 --- a/packages/bsky/tests/views/actor-likes.test.ts +++ b/packages/bsky/tests/views/actor-likes.test.ts @@ -2,10 +2,6 @@ import AtpAgent, { AtUri } from '@atproto/api' import { TestNetwork } from '@atproto/dev-env' import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' -import { - BlockedByActorError, - BlockedActorError, -} from '@atproto/api/src/client/types/app/bsky/feed/getActorLikes' describe('bsky actor likes feed views', () => { let network: TestNetwork @@ -17,7 +13,6 @@ describe('bsky actor likes feed views', () => { let alice: string let bob: string let carol: string - let dan: string beforeAll(async () => { network = await TestNetwork.create({ @@ -31,7 +26,6 @@ describe('bsky actor likes feed views', () => { alice = sc.dids.alice bob = sc.dids.bob carol = sc.dids.carol - dan = sc.dids.dan }) afterAll(async () => { diff --git a/packages/common/tests/streams.test.ts b/packages/common/tests/streams.test.ts index 6396290c09e..735b19a8341 100644 --- a/packages/common/tests/streams.test.ts +++ b/packages/common/tests/streams.test.ts @@ -1,5 +1,5 @@ import * as streams from '../src/streams' -import { PassThrough, Readable, pipeline } from 'node:stream' +import { PassThrough, Readable } from 'node:stream' describe('streams', () => { describe('forwardStreamErrors', () => { diff --git a/packages/crypto/tests/signatures.test.ts b/packages/crypto/tests/signatures.test.ts index 1c8f6a4961b..cebc8126b3a 100644 --- a/packages/crypto/tests/signatures.test.ts +++ b/packages/crypto/tests/signatures.test.ts @@ -59,6 +59,7 @@ describe('signatures', () => { }) }) +// eslint-disable-next-line @typescript-eslint/no-unused-vars async function generateTestVectors(): Promise { const p256Key = await EcdsaKeypair.create({ exportable: true }) const secpKey = await Secp256k1Keypair.create({ exportable: true }) diff --git a/packages/lexicon/tests/general.test.ts b/packages/lexicon/tests/general.test.ts index 23328750cd5..5217ad49c52 100644 --- a/packages/lexicon/tests/general.test.ts +++ b/packages/lexicon/tests/general.test.ts @@ -1,6 +1,5 @@ import { CID } from 'multiformats/cid' import { lexiconDoc, Lexicons } from '../src/index' -import { object } from '../src/validators/complex' import LexiconDocs from './_scaffolds/lexicons' describe('Lexicons collection', () => { diff --git a/packages/pds/src/app-view/services/label/index.ts b/packages/pds/src/app-view/services/label/index.ts index 338475ba633..a3534666d5d 100644 --- a/packages/pds/src/app-view/services/label/index.ts +++ b/packages/pds/src/app-view/services/label/index.ts @@ -1,5 +1,4 @@ import { sql } from 'kysely' -import { CID } from 'multiformats/cid' import { AtUri } from '@atproto/syntax' import Database from '../../../db' import { diff --git a/packages/pds/src/feed-gen/with-friends.ts b/packages/pds/src/feed-gen/with-friends.ts index 682387a5e61..a36f435286c 100644 --- a/packages/pds/src/feed-gen/with-friends.ts +++ b/packages/pds/src/feed-gen/with-friends.ts @@ -1,16 +1,11 @@ import AppContext from '../context' import { QueryParams as SkeletonParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton' -import { paginate } from '../db/pagination' import { AlgoHandler, AlgoResponse } from './types' -import { - FeedKeyset, - getFeedDateThreshold, -} from '../app-view/api/app/bsky/util/feed' const handler: AlgoHandler = async ( - ctx: AppContext, - params: SkeletonParams, - requester: string, + _ctx: AppContext, + _params: SkeletonParams, + _requester: string, ): Promise => { // Temporary change to only return a post notifying users that the feed is down return { diff --git a/packages/pds/src/services/moderation/views.ts b/packages/pds/src/services/moderation/views.ts index 5676e086f35..2403de729fb 100644 --- a/packages/pds/src/services/moderation/views.ts +++ b/packages/pds/src/services/moderation/views.ts @@ -23,7 +23,6 @@ import { AccountService } from '../account' import { RecordService } from '../record' import { ModerationReportRowWithHandle } from '.' import { getSelfLabels } from '../../app-view/services/label' -import { jsonStringToLex } from '@atproto/lexicon' export class ModerationViews { constructor(private db: Database, private messageDispatcher: MessageQueue) {} diff --git a/packages/pds/tests/views/actor-likes.test.ts b/packages/pds/tests/views/actor-likes.test.ts index 27c1ab4385c..15e2dc3e934 100644 --- a/packages/pds/tests/views/actor-likes.test.ts +++ b/packages/pds/tests/views/actor-likes.test.ts @@ -12,7 +12,6 @@ describe('pds actor likes feed views', () => { let alice: string let bob: string let carol: string - let dan: string beforeAll(async () => { const server = await runTestServer({ @@ -25,7 +24,6 @@ describe('pds actor likes feed views', () => { alice = sc.dids.alice bob = sc.dids.bob carol = sc.dids.carol - dan = sc.dids.dan await server.processAll() }) diff --git a/packages/repo/tests/util.test.ts b/packages/repo/tests/util.test.ts index f341cadfea9..7c1c618a579 100644 --- a/packages/repo/tests/util.test.ts +++ b/packages/repo/tests/util.test.ts @@ -11,7 +11,7 @@ describe('Utils', () => { await car.put(block) throw new Error('Oops!') }) - for await (const bytes of iter) { + for await (const _bytes of iter) { // no-op } } From 1487c9f1fe811cf2293c674baf8e66686c74fb0c Mon Sep 17 00:00:00 2001 From: bnewbold Date: Thu, 21 Sep 2023 14:50:23 -0700 Subject: [PATCH 27/46] indicate that getPopular is deprecated (#1647) * indicate that getPopular is deprecated * codegen for deprecating getPopular --- lexicons/app/bsky/unspecced/getPopular.json | 2 +- packages/api/src/client/lexicons.ts | 3 ++- packages/bsky/src/lexicon/lexicons.ts | 3 ++- packages/pds/src/lexicon/lexicons.ts | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lexicons/app/bsky/unspecced/getPopular.json b/lexicons/app/bsky/unspecced/getPopular.json index 791968c5ef9..2aac00fed14 100644 --- a/lexicons/app/bsky/unspecced/getPopular.json +++ b/lexicons/app/bsky/unspecced/getPopular.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "An unspecced view of globally popular items", + "description": "DEPRECATED: will be removed soon, please find a feed generator alternative", "parameters": { "type": "params", "properties": { diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 48b1276adc8..6b9b7b7f14f 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -6838,7 +6838,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'An unspecced view of globally popular items', + description: + 'DEPRECATED: will be removed soon, please find a feed generator alternative', parameters: { type: 'params', properties: { diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 48b1276adc8..6b9b7b7f14f 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -6838,7 +6838,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'An unspecced view of globally popular items', + description: + 'DEPRECATED: will be removed soon, please find a feed generator alternative', parameters: { type: 'params', properties: { diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 48b1276adc8..6b9b7b7f14f 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -6838,7 +6838,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'An unspecced view of globally popular items', + description: + 'DEPRECATED: will be removed soon, please find a feed generator alternative', parameters: { type: 'params', properties: { From 584dea52c475d389c2260d0a9312f805151d2b62 Mon Sep 17 00:00:00 2001 From: bnewbold Date: Thu, 21 Sep 2023 18:07:33 -0700 Subject: [PATCH 28/46] tidy up package.json and READMEs (#1649) * identity: README example and tidy * tidy up package metadata (package.json files) * updated README headers/stubs for several packages * crypto: longer README, with usage * syntax: tweak README * Apply suggestions from code review Co-authored-by: Eric Bailey Co-authored-by: devin ivy --------- Co-authored-by: Eric Bailey Co-authored-by: devin ivy --- packages/api/package.json | 19 +++++++++---- packages/aws/package.json | 18 ++++++++---- packages/bsky/README.md | 11 ++++++-- packages/bsky/package.json | 8 +++++- packages/common-web/README.md | 11 ++++++-- packages/common-web/package.json | 17 +++++++---- packages/common/README.md | 11 ++++++-- packages/common/package.json | 17 +++++++---- packages/crypto/README.md | 47 +++++++++++++++++++++++++++++-- packages/crypto/package.json | 18 ++++++++---- packages/dev-env/README.md | 9 +++++- packages/dev-env/package.json | 17 +++++++---- packages/identity/README.md | 41 +++++++++++++++++++++++++-- packages/identity/package.json | 19 +++++++++---- packages/lex-cli/package.json | 18 ++++++++---- packages/lexicon/README.md | 13 +++++---- packages/lexicon/package.json | 18 ++++++++---- packages/pds/README.md | 13 +++++++-- packages/pds/package.json | 8 +++++- packages/repo/README.md | 13 +++++++-- packages/repo/package.json | 18 ++++++++---- packages/syntax/README.md | 19 ++++++++++--- packages/syntax/package.json | 20 +++++++++---- packages/xrpc-server/README.md | 7 ++++- packages/xrpc-server/package.json | 18 ++++++++---- packages/xrpc/README.md | 7 ++++- packages/xrpc/package.json | 18 ++++++++---- 27 files changed, 347 insertions(+), 106 deletions(-) diff --git a/packages/api/package.json b/packages/api/package.json index 5e34b416d71..6189e180aa8 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,19 @@ { "name": "@atproto/api", "version": "0.6.15", + "license": "MIT", + "description": "Client library for atproto and Bluesky", + "keywords": [ + "atproto", + "bluesky", + "api" + ], + "homepage": "https://atproto.com", + "repository": { + "type": "git", + "url": "https://github.com/bluesky-social/atproto", + "directory": "packages/api" + }, "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", @@ -16,12 +29,6 @@ "bench": "jest --config jest.bench.config.js", "bench:profile": "node --inspect-brk ../../node_modules/.bin/jest --config jest.bench.config.js" }, - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/bluesky-social/atproto.git", - "directory": "packages/api" - }, "dependencies": { "@atproto/common-web": "workspace:^", "@atproto/lexicon": "workspace:^", diff --git a/packages/aws/package.json b/packages/aws/package.json index da1de3439ba..3cd4dddcf2e 100644 --- a/packages/aws/package.json +++ b/packages/aws/package.json @@ -1,17 +1,23 @@ { "name": "@atproto/aws", "version": "0.1.1", - "main": "src/index.ts", - "publishConfig": { - "main": "dist/index.js", - "types": "dist/src/index.d.ts" - }, "license": "MIT", + "description": "Shared AWS cloud API helpers for atproto services", + "keywords": [ + "atproto", + "aws" + ], + "homepage": "https://atproto.com", "repository": { "type": "git", - "url": "https://github.com/bluesky-social/atproto.git", + "url": "https://github.com/bluesky-social/atproto", "directory": "packages/aws" }, + "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/src/index.d.ts" + }, "scripts": { "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", diff --git a/packages/bsky/README.md b/packages/bsky/README.md index 2b0582044a0..8066ae30e1f 100644 --- a/packages/bsky/README.md +++ b/packages/bsky/README.md @@ -1,3 +1,10 @@ -# Bsky App View +# @atproto/bsky: Bluesky AppView Service -The Bsky App View. This contains the indexers and XRPC methods for reading data from the Bsky application. +TypeScript implementation of the `app.bsky` Lexicons backing the https://bsky.app microblogging application. + +[![NPM](https://img.shields.io/npm/v/@atproto/bsky)](https://www.npmjs.com/package/@atproto/bsky) +[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml) + +## License + +MIT License diff --git a/packages/bsky/package.json b/packages/bsky/package.json index 2ee0f9bd7d3..aac20db5209 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -2,9 +2,15 @@ "name": "@atproto/bsky", "version": "0.0.6", "license": "MIT", + "description": "Reference implementation of app.bsky App View (Bluesky API)", + "keywords": [ + "atproto", + "bluesky" + ], + "homepage": "https://atproto.com", "repository": { "type": "git", - "url": "https://github.com/bluesky-social/atproto.git", + "url": "https://github.com/bluesky-social/atproto", "directory": "packages/bsky" }, "main": "src/index.ts", diff --git a/packages/common-web/README.md b/packages/common-web/README.md index 0eb7f5c3e04..74426aebe37 100644 --- a/packages/common-web/README.md +++ b/packages/common-web/README.md @@ -1,3 +1,10 @@ -# ATP Common Library for Web +# @atproto/common-web -A library containing code which is shared between web-friendly ATP packages. +Shared TypeScript code for other `@atproto/*` packages, which is web-friendly (runs in-browser). + +[![NPM](https://img.shields.io/npm/v/@atproto/common)](https://www.npmjs.com/package/@atproto/common-web) +[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml) + +## License + +MIT License diff --git a/packages/common-web/package.json b/packages/common-web/package.json index 92387c502ab..ad5ee4d8ff3 100644 --- a/packages/common-web/package.json +++ b/packages/common-web/package.json @@ -1,17 +1,22 @@ { "name": "@atproto/common-web", "version": "0.2.0", - "main": "src/index.ts", - "publishConfig": { - "main": "dist/index.js", - "types": "dist/index.d.ts" - }, "license": "MIT", + "description": "Shared web-platform-friendly code for atproto libraries", + "keywords": [ + "atproto" + ], + "homepage": "https://atproto.com", "repository": { "type": "git", - "url": "https://github.com/bluesky-social/atproto.git", + "url": "https://github.com/bluesky-social/atproto", "directory": "packages/common-web" }, + "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/index.d.ts" + }, "scripts": { "test": "jest", "build": "node ./build.js", diff --git a/packages/common/README.md b/packages/common/README.md index 1c5a0609639..c08104cebe1 100644 --- a/packages/common/README.md +++ b/packages/common/README.md @@ -1,3 +1,10 @@ -# ATP Common Library +# @atproto/common -A library containing code which is shared between ATP packages. +Shared TypeScript code for other `@atproto/*` packages. This package is oriented towards writing servers, and is not designed to be browser-compatible. + +[![NPM](https://img.shields.io/npm/v/@atproto/common)](https://www.npmjs.com/package/@atproto/common) +[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml) + +## License + +MIT License diff --git a/packages/common/package.json b/packages/common/package.json index c7f518c1a99..4794bdae698 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,17 +1,22 @@ { "name": "@atproto/common", "version": "0.3.0", - "main": "src/index.ts", - "publishConfig": { - "main": "dist/index.js", - "types": "dist/index.d.ts" - }, "license": "MIT", + "description": "Shared web-platform-friendly code for atproto libraries", + "keywords": [ + "atproto" + ], + "homepage": "https://atproto.com", "repository": { "type": "git", - "url": "https://github.com/bluesky-social/atproto.git", + "url": "https://github.com/bluesky-social/atproto", "directory": "packages/common" }, + "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/index.d.ts" + }, "scripts": { "test": "jest", "build": "node ./build.js", diff --git a/packages/crypto/README.md b/packages/crypto/README.md index 2258451f7ca..0b610bf7f6f 100644 --- a/packages/crypto/README.md +++ b/packages/crypto/README.md @@ -1,3 +1,46 @@ -# ATP Crypto Library +# @atproto/crypto -ATP's common cryptographic operations. +TypeScript library providing basic cryptographic helpers as needed in [atproto](https://atproto.com). + +[![NPM](https://img.shields.io/npm/v/@atproto/crypto)](https://www.npmjs.com/package/@atproto/crypto) +[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml) + +This package implements the two currently supported cryptographic systems: + +- P-256 elliptic curve: aka "NIST P-256", aka secp256r1 (note the r), aka prime256v1 +- K-256 elliptic curve: aka "NIST K-256", aka secp256k1 (note the k) + +The details of cryptography in atproto are described in [the specification](https://atproto.com/specs/cryptography). This includes string encodings, validity of "low-S" signatures, byte representation "compression", hashing, and more. + +## Usage + +```typescript +import { verifySignature, Secp256k1Keypair, P256Keypair } from '@atproto/crypto' + +// generate a new random K-256 private key +const keypair = await Secp256k1Keypair.create({ exportable: true }) + +// sign binary data, resulting signature bytes. +// SHA-256 hash of data is what actually gets signed. +// signature output is often base64-encoded. +const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]) +const sig = await keypair.sign(data) + +// serialize the public key as a did:key string, which includes key type metadata +const pubDidKey = keypair.did() +console.log(pubDidKey) + +// output would look something like: 'did:key:zQ3shVRtgqTRHC7Lj4DYScoDgReNpsDp3HBnuKBKt1FSXKQ38' + +// verify signature using public key +const ok = verifySignature(pubDidKey, data, sig) +if (!ok) { + throw new Error('Uh oh, something is fishy') +} else { + console.log('Success') +} +``` + +## License + +MIT License diff --git a/packages/crypto/package.json b/packages/crypto/package.json index c1d7fa5b521..7b552ca6b43 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -1,17 +1,23 @@ { "name": "@atproto/crypto", "version": "0.2.2", - "main": "src/index.ts", - "publishConfig": { - "main": "dist/index.js", - "types": "dist/index.d.ts" - }, "license": "MIT", + "description": "Library for cryptographic keys and signing in atproto", + "keywords": [ + "atproto", + "cryptography" + ], + "homepage": "https://atproto.com", "repository": { "type": "git", - "url": "https://github.com/bluesky-social/atproto.git", + "url": "https://github.com/bluesky-social/atproto", "directory": "packages/crypto" }, + "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/index.d.ts" + }, "scripts": { "test": "jest ", "build": "node ./build.js", diff --git a/packages/dev-env/README.md b/packages/dev-env/README.md index c1a41e692fe..60befbe44ca 100644 --- a/packages/dev-env/README.md +++ b/packages/dev-env/README.md @@ -1,7 +1,10 @@ -# ATP Developer Environment +# @atproto/dev-env: Local Developer Environment A command-line application for developers to construct and manage development environments. +[![NPM](https://img.shields.io/npm/v/@atproto/dev-env)](https://www.npmjs.com/package/@atproto/dev-env) +[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml) + ## REPL API The following methods are available in the REPL. @@ -25,3 +28,7 @@ Create a new user. ### `user(handle: string): ServiceClient` Get the `ServiceClient` for the given user. + +## License + +MIT License diff --git a/packages/dev-env/package.json b/packages/dev-env/package.json index ad7b0f420b0..d36a4ab1324 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -1,18 +1,23 @@ { "name": "@atproto/dev-env", "version": "0.2.6", + "license": "MIT", + "description": "Local development environment helper for atproto development", + "keywords": [ + "atproto" + ], + "homepage": "https://atproto.com", + "repository": { + "type": "git", + "url": "https://github.com/bluesky-social/atproto", + "directory": "packages/dev-env" + }, "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", "types": "dist/index.d.ts" }, "bin": "dist/bin.js", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/bluesky-social/atproto.git", - "directory": "packages/dev-env" - }, "scripts": { "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", diff --git a/packages/identity/README.md b/packages/identity/README.md index c6d940f448e..874fe23570e 100644 --- a/packages/identity/README.md +++ b/packages/identity/README.md @@ -1,3 +1,40 @@ -# ATP DID Resolver +# @atproto/identity -A library for resolving ATP's Decentralized ID methods. +TypeScript library for decentralized identities in [atproto](https://atproto.com) using DIDs and handles + +[![NPM](https://img.shields.io/npm/v/@atproto/identity)](https://www.npmjs.com/package/@atproto/identity) +[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml) + +## Example + +Resolving a Handle and verifying against DID document: + +```typescript +const didres = new DidResolver({}) +const hdlres = new HandleResolver({}) + +const handle = 'atproto.com' +const did = await hdlres.resolve(handle) + +if (did == undefined) { + throw new Error('expected handle to resolve') +} +console.log(did) // did:plc:ewvi7nxzyoun6zhxrhs64oiz + +const doc = await didres.resolve(did) +console.log(doc) + +// additional resolutions of same DID will be cached for some time, unless forceRefresh flag is used +const doc2 = await didres.resolve(did, true) + +// helper methods use the same cache +const data = await didres.resolveAtprotoData(did) + +if (data.handle != handle) { + throw new Error('invalid handle (did not match DID document)') +} +``` + +## License + +MIT License diff --git a/packages/identity/package.json b/packages/identity/package.json index 1bd0e3cc3f4..e8b2abcf454 100644 --- a/packages/identity/package.json +++ b/packages/identity/package.json @@ -1,17 +1,24 @@ { "name": "@atproto/identity", "version": "0.2.0", - "main": "src/index.ts", - "publishConfig": { - "main": "dist/index.js", - "types": "dist/index.d.ts" - }, "license": "MIT", + "description": "Library for decentralized identities in atproto using DIDs and handles", + "keywords": [ + "atproto", + "did", + "identity" + ], + "homepage": "https://atproto.com", "repository": { "type": "git", - "url": "https://github.com/bluesky-social/atproto.git", + "url": "https://github.com/bluesky-social/atproto", "directory": "packages/identity" }, + "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/index.d.ts" + }, "scripts": { "test": "jest", "test:log": "cat test.log | pino-pretty", diff --git a/packages/lex-cli/package.json b/packages/lex-cli/package.json index 43ddf227fd3..db23ef2e1ce 100644 --- a/packages/lex-cli/package.json +++ b/packages/lex-cli/package.json @@ -1,6 +1,18 @@ { "name": "@atproto/lex-cli", "version": "0.2.1", + "license": "MIT", + "description": "TypeScript codegen tool for atproto Lexicon schemas", + "keywords": [ + "atproto", + "lexicon" + ], + "homepage": "https://atproto.com", + "repository": { + "type": "git", + "url": "https://github.com/bluesky-social/atproto", + "directory": "packages/lex-cli" + }, "bin": { "lex": "dist/index.js" }, @@ -14,12 +26,6 @@ "postbuild": "tsc --build tsconfig.build.json", "update-main-to-dist": "node ../../update-main-to-dist.js packages/lex-cli" }, - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/bluesky-social/atproto.git", - "directory": "packages/lex-cli" - }, "dependencies": { "@atproto/lexicon": "workspace:^", "@atproto/syntax": "workspace:^", diff --git a/packages/lexicon/README.md b/packages/lexicon/README.md index 7a7b2ed7b20..33fd777ce7f 100644 --- a/packages/lexicon/README.md +++ b/packages/lexicon/README.md @@ -1,10 +1,9 @@ -# Lexicon +# @atproto/lexicon: schema validation library -Lexicon is the semantic schemas & contracts system for ATP. This library provides definitions and APIs for ATP software. +TypeScript implementation of the Lexicon data and API schema description language, which is part of [atproto](https://atproto.com). -``` -npm install @atproto/lexicon -``` +[![NPM](https://img.shields.io/npm/v/@atproto/lexicon)](https://www.npmjs.com/package/@atproto/lexicon) +[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml) ## Usage @@ -29,3 +28,7 @@ lex.assertValidXrpcParams('com.example.query', {...}) lex.assertValidXrpcInput('com.example.procedure', {...}) lex.assertValidXrpcOutput('com.example.query', {...}) ``` + +## License + +MIT diff --git a/packages/lexicon/package.json b/packages/lexicon/package.json index 18f254b6d82..ebce21d48af 100644 --- a/packages/lexicon/package.json +++ b/packages/lexicon/package.json @@ -1,6 +1,18 @@ { "name": "@atproto/lexicon", "version": "0.2.1", + "license": "MIT", + "description": "atproto Lexicon schema language library", + "keywords": [ + "atproto", + "lexicon" + ], + "homepage": "https://atproto.com", + "repository": { + "type": "git", + "url": "https://github.com/bluesky-social/atproto", + "directory": "packages/lexicon" + }, "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", @@ -12,12 +24,6 @@ "postbuild": "tsc --build tsconfig.build.json", "update-main-to-dist": "node ../../update-main-to-dist.js packages/lexicon" }, - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/bluesky-social/atproto.git", - "directory": "packages/lexicon" - }, "dependencies": { "@atproto/common-web": "workspace:^", "@atproto/syntax": "workspace:^", diff --git a/packages/pds/README.md b/packages/pds/README.md index 547856de3d0..b70d99cb780 100644 --- a/packages/pds/README.md +++ b/packages/pds/README.md @@ -1,3 +1,12 @@ -# ATP Personal Data Server (PDS) +# @atproto/pds: Personal Data Server (PDS) -The Personal Data Server (PDS). This is ATP's main server-side implementation. +TypeScript reference implementation of an atproto PDS. + +[![NPM](https://img.shields.io/npm/v/@atproto/pds)](https://www.npmjs.com/package/@atproto/pds) +[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml) + +If you are interested in self-hosting a PDS, you probably want this repository instead, which has a thin service wrapper, documentation, a Dockerfile, etc: https://github.com/bluesky-social/pds + +## License + +MIT License diff --git a/packages/pds/package.json b/packages/pds/package.json index 469d1523d1d..384eee10d9d 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -2,9 +2,15 @@ "name": "@atproto/pds", "version": "0.1.15", "license": "MIT", + "description": "Reference implementation of atproto Personal Data Server (PDS)", + "keywords": [ + "atproto", + "pds" + ], + "homepage": "https://atproto.com", "repository": { "type": "git", - "url": "https://github.com/bluesky-social/atproto.git", + "url": "https://github.com/bluesky-social/atproto", "directory": "packages/pds" }, "main": "src/index.ts", diff --git a/packages/repo/README.md b/packages/repo/README.md index 53c83a3071c..e018cf76ff8 100644 --- a/packages/repo/README.md +++ b/packages/repo/README.md @@ -1,3 +1,12 @@ -# ATP Repo +# @atproto/repo: Repository and MST -The "ATP repository" core implementation (a Merkle Search Tree). +TypeScript library for atproto repositories, and in particular the Merkle Search Tree (MST) data structure. + +[![NPM](https://img.shields.io/npm/v/@atproto/repo)](https://www.npmjs.com/package/@atproto/repo) +[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml) + +Repositories in atproto are signed key/value stores containing CBOR-encoded data records. The structure and implementation details are described in [the specification](https://atproto.com/specs/repository). This includes MST node format, serialization, structural constraints, and more. + +## License + +MIT License diff --git a/packages/repo/package.json b/packages/repo/package.json index 7ea37ca4d03..bcb9d5eb452 100644 --- a/packages/repo/package.json +++ b/packages/repo/package.json @@ -1,17 +1,23 @@ { "name": "@atproto/repo", "version": "0.3.1", - "main": "src/index.ts", - "publishConfig": { - "main": "dist/index.js", - "types": "dist/index.d.ts" - }, "license": "MIT", + "description": "atproto repo and MST implementation", + "keywords": [ + "atproto", + "mst" + ], + "homepage": "https://atproto.com", "repository": { "type": "git", - "url": "https://github.com/bluesky-social/atproto.git", + "url": "https://github.com/bluesky-social/atproto", "directory": "packages/repo" }, + "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/index.d.ts" + }, "scripts": { "test": "jest", "test:profile": "node --inspect ../../node_modules/.bin/jest", diff --git a/packages/syntax/README.md b/packages/syntax/README.md index 5bc6c01b646..0658b64d59c 100644 --- a/packages/syntax/README.md +++ b/packages/syntax/README.md @@ -1,9 +1,16 @@ -# Syntax +# @atproto/syntax: validation helpers for identifier strings -Validation logic for AT identifiers - DIDs, Handles, NSIDs, and AT URIs. +Validation logic for [atproto](https://atproto.com) identifiers - DIDs, Handles, NSIDs, and AT URIs. + +[![NPM](https://img.shields.io/npm/v/@atproto/crypto)](https://www.npmjs.com/package/@atproto/syntax) +[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml) ## Usage +### Handles + +Syntax specification: + ```typescript import { isValidHandle, ensureValidHandle, isValidDid } from '@atproto/syntax' @@ -17,7 +24,9 @@ ensureValidDid('did:method:val') // returns void ensureValidDid(':did:method:val') // throws ``` -## NameSpaced IDs (NSID) +### NameSpaced IDs (NSID) + +Syntax specification: ```typescript import { NSID } from '@atproto/syntax' @@ -43,7 +52,9 @@ NSID.isValid('example.com/foo') // => false NSID.isValid('foo') // => false ``` -## AT URI +### AT URI + +Syntax specification: ```typescript import { AtUri } from '@atproto/syntax' diff --git a/packages/syntax/package.json b/packages/syntax/package.json index 0be28698de7..54488519d50 100644 --- a/packages/syntax/package.json +++ b/packages/syntax/package.json @@ -1,6 +1,20 @@ { "name": "@atproto/syntax", "version": "0.1.1", + "license": "MIT", + "description": "Validation for atproto identifiers and formats: DID, handle, NSID, AT URI, etc", + "keywords": [ + "atproto", + "did", + "nsid", + "at-uri" + ], + "homepage": "https://atproto.com", + "repository": { + "type": "git", + "url": "https://github.com/bluesky-social/atproto", + "directory": "packages/syntax" + }, "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", @@ -12,12 +26,6 @@ "postbuild": "tsc --build tsconfig.build.json", "update-main-to-dist": "node ../../update-main-to-dist.js packages/syntax" }, - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/bluesky-social/atproto.git", - "directory": "packages/syntax" - }, "dependencies": { "@atproto/common-web": "workspace:^" }, diff --git a/packages/xrpc-server/README.md b/packages/xrpc-server/README.md index 2c297043fdf..03314c342a6 100644 --- a/packages/xrpc-server/README.md +++ b/packages/xrpc-server/README.md @@ -1,4 +1,9 @@ -# XRPC Server API +# @atproto/xrpc-server: atproto HTTP API server library + +TypeScript library for implementing [atproto](https://atproto.com) HTTP API services, with Lexicon schema validation. + +[![NPM](https://img.shields.io/npm/v/@atproto/xrpc-server)](https://www.npmjs.com/package/@atproto/xrpc-server) +[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml) ## Usage diff --git a/packages/xrpc-server/package.json b/packages/xrpc-server/package.json index b6df825c8d8..2714be3951c 100644 --- a/packages/xrpc-server/package.json +++ b/packages/xrpc-server/package.json @@ -1,6 +1,18 @@ { "name": "@atproto/xrpc-server", "version": "0.3.1", + "license": "MIT", + "description": "atproto HTTP API (XRPC) server library", + "keywords": [ + "atproto", + "xrpc" + ], + "homepage": "https://atproto.com", + "repository": { + "type": "git", + "url": "https://github.com/bluesky-social/atproto", + "directory": "packages/xrpc-server" + }, "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", @@ -12,12 +24,6 @@ "postbuild": "tsc --build tsconfig.build.json", "update-main-to-dist": "node ../../update-main-to-dist.js packages/xrpc-server" }, - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/bluesky-social/atproto.git", - "directory": "packages/xrpc-server" - }, "dependencies": { "@atproto/common": "workspace:^", "@atproto/crypto": "workspace:^", diff --git a/packages/xrpc/README.md b/packages/xrpc/README.md index bfb0b8778c3..5789302658e 100644 --- a/packages/xrpc/README.md +++ b/packages/xrpc/README.md @@ -1,4 +1,9 @@ -# XRPC API +# @atproto/xrpc: atproto HTTP API Client + +TypeScript client library for talking to [atproto](https://atproto.com) services, with Lexicon schema validation. + +[![NPM](https://img.shields.io/npm/v/@atproto/xrpc)](https://www.npmjs.com/package/@atproto/xrpc) +[![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml) ## Usage diff --git a/packages/xrpc/package.json b/packages/xrpc/package.json index f6358602256..68ee51b35b8 100644 --- a/packages/xrpc/package.json +++ b/packages/xrpc/package.json @@ -1,6 +1,18 @@ { "name": "@atproto/xrpc", "version": "0.3.1", + "license": "MIT", + "description": "atproto HTTP API (XRPC) client library", + "keywords": [ + "atproto", + "xrpc" + ], + "homepage": "https://atproto.com", + "repository": { + "type": "git", + "url": "https://github.com/bluesky-social/atproto", + "directory": "packages/xrpc" + }, "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", @@ -11,12 +23,6 @@ "postbuild": "tsc --build tsconfig.build.json", "update-main-to-dist": "node ../../update-main-to-dist.js packages/xrpc" }, - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/bluesky-social/atproto.git", - "directory": "packages/xrpc" - }, "dependencies": { "@atproto/lexicon": "workspace:^", "zod": "^3.21.4" From 56e2cf8999f6d7522529a9be8652c47545f82242 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 21 Sep 2023 18:10:59 -0700 Subject: [PATCH 29/46] Improve the types of the thread and feed preferences APIs (#1653) * Improve the types of the thread and feed preferences APIs * Remove unused import * Add changeset --- .changeset/lazy-moons-relate.md | 5 +++++ packages/api/src/bsky-agent.ts | 16 ++++++++-------- packages/api/src/types.ts | 29 +++++++++++++++++++++++++---- 3 files changed, 38 insertions(+), 12 deletions(-) create mode 100644 .changeset/lazy-moons-relate.md diff --git a/.changeset/lazy-moons-relate.md b/.changeset/lazy-moons-relate.md new file mode 100644 index 00000000000..b314453a6f1 --- /dev/null +++ b/.changeset/lazy-moons-relate.md @@ -0,0 +1,5 @@ +--- +'@atproto/api': patch +--- + +Improve the types of the thread and feed preferences APIs diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index 2ba8d7bf48c..37b8d9c3620 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -6,7 +6,12 @@ import { AppBskyActorDefs, ComAtprotoRepoPutRecord, } from './client' -import { BskyPreferences, BskyLabelPreference } from './types' +import { + BskyPreferences, + BskyLabelPreference, + BskyFeedViewPreference, + BskyThreadViewPreference, +} from './types' const FEED_VIEW_PREF_DEFAULTS = { hideReplies: false, @@ -439,10 +444,7 @@ export class BskyAgent extends AtpAgent { }) } - async setFeedViewPrefs( - feed: string, - pref: Omit, - ) { + async setFeedViewPrefs(feed: string, pref: Partial) { await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => { const existing = prefs.findLast( (pref) => @@ -461,9 +463,7 @@ export class BskyAgent extends AtpAgent { }) } - async setThreadViewPrefs( - pref: Omit, - ) { + async setThreadViewPrefs(pref: Partial) { await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => { const existing = prefs.findLast( (pref) => diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index 985ceb44c31..10d0bbd90fe 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -1,5 +1,4 @@ import { LabelPreference } from './moderation/types' -import { AppBskyActorDefs } from './client' /** * Used by the PersistSessionHandler to indicate what change occurred @@ -81,15 +80,37 @@ export type BskyLabelPreference = LabelPreference | 'show' // TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf /** - * Bluesky preferences object + * Bluesky feed view preferences + */ + +export interface BskyFeedViewPreference { + hideReplies: boolean + hideRepliesByUnfollowed: boolean + hideRepliesByLikeCount: number + hideReposts: boolean + hideQuotePosts: boolean + [key: string]: any +} + +/** + * Bluesky thread view preferences + */ +export interface BskyThreadViewPreference { + sort: string + prioritizeFollowedUsers: boolean + [key: string]: any +} + +/** + * Bluesky preferences */ export interface BskyPreferences { feeds: { saved?: string[] pinned?: string[] } - feedViewPrefs: Record> - threadViewPrefs: Omit + feedViewPrefs: Record + threadViewPrefs: BskyThreadViewPreference adultContentEnabled: boolean contentLabels: Record birthDate: Date | undefined From 86bbf0860acd09055108f27b51b9f2bdd198db4a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 21 Sep 2023 18:19:28 -0700 Subject: [PATCH 30/46] Version packages (#1654) Co-authored-by: github-actions[bot] --- .changeset/lazy-moons-relate.md | 5 ----- packages/api/CHANGELOG.md | 6 ++++++ packages/api/package.json | 2 +- packages/bsky/CHANGELOG.md | 7 +++++++ packages/bsky/package.json | 2 +- packages/dev-env/CHANGELOG.md | 9 +++++++++ packages/dev-env/package.json | 2 +- packages/pds/CHANGELOG.md | 7 +++++++ packages/pds/package.json | 2 +- 9 files changed, 33 insertions(+), 9 deletions(-) delete mode 100644 .changeset/lazy-moons-relate.md diff --git a/.changeset/lazy-moons-relate.md b/.changeset/lazy-moons-relate.md deleted file mode 100644 index b314453a6f1..00000000000 --- a/.changeset/lazy-moons-relate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@atproto/api': patch ---- - -Improve the types of the thread and feed preferences APIs diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index ab2448d2bbb..79e6807686c 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -1,5 +1,11 @@ # @atproto/api +## 0.6.16 + +### Patch Changes + +- [#1653](https://github.com/bluesky-social/atproto/pull/1653) [`56e2cf89`](https://github.com/bluesky-social/atproto/commit/56e2cf8999f6d7522529a9be8652c47545f82242) Thanks [@pfrazee](https://github.com/pfrazee)! - Improve the types of the thread and feed preferences APIs + ## 0.6.15 ### Patch Changes diff --git a/packages/api/package.json b/packages/api/package.json index 6189e180aa8..76cf7f1b8e7 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.6.15", + "version": "0.6.16", "license": "MIT", "description": "Client library for atproto and Bluesky", "keywords": [ diff --git a/packages/bsky/CHANGELOG.md b/packages/bsky/CHANGELOG.md index df07140737f..8e449ba7cf0 100644 --- a/packages/bsky/CHANGELOG.md +++ b/packages/bsky/CHANGELOG.md @@ -1,5 +1,12 @@ # @atproto/bsky +## 0.0.7 + +### Patch Changes + +- Updated dependencies [[`56e2cf89`](https://github.com/bluesky-social/atproto/commit/56e2cf8999f6d7522529a9be8652c47545f82242)]: + - @atproto/api@0.6.16 + ## 0.0.6 ### Patch Changes diff --git a/packages/bsky/package.json b/packages/bsky/package.json index aac20db5209..8195d27b539 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/bsky", - "version": "0.0.6", + "version": "0.0.7", "license": "MIT", "description": "Reference implementation of app.bsky App View (Bluesky API)", "keywords": [ diff --git a/packages/dev-env/CHANGELOG.md b/packages/dev-env/CHANGELOG.md index d12edd580ef..38a56abaa99 100644 --- a/packages/dev-env/CHANGELOG.md +++ b/packages/dev-env/CHANGELOG.md @@ -1,5 +1,14 @@ # @atproto/dev-env +## 0.2.7 + +### Patch Changes + +- Updated dependencies [[`56e2cf89`](https://github.com/bluesky-social/atproto/commit/56e2cf8999f6d7522529a9be8652c47545f82242)]: + - @atproto/api@0.6.16 + - @atproto/bsky@0.0.7 + - @atproto/pds@0.1.16 + ## 0.2.6 ### Patch Changes diff --git a/packages/dev-env/package.json b/packages/dev-env/package.json index d36a4ab1324..e688319dcb1 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/dev-env", - "version": "0.2.6", + "version": "0.2.7", "license": "MIT", "description": "Local development environment helper for atproto development", "keywords": [ diff --git a/packages/pds/CHANGELOG.md b/packages/pds/CHANGELOG.md index 3aa0bc4d812..17f13cf1c85 100644 --- a/packages/pds/CHANGELOG.md +++ b/packages/pds/CHANGELOG.md @@ -1,5 +1,12 @@ # @atproto/pds +## 0.1.16 + +### Patch Changes + +- Updated dependencies [[`56e2cf89`](https://github.com/bluesky-social/atproto/commit/56e2cf8999f6d7522529a9be8652c47545f82242)]: + - @atproto/api@0.6.16 + ## 0.1.15 ### Patch Changes diff --git a/packages/pds/package.json b/packages/pds/package.json index 384eee10d9d..e227bd4c4c9 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/pds", - "version": "0.1.15", + "version": "0.1.16", "license": "MIT", "description": "Reference implementation of atproto Personal Data Server (PDS)", "keywords": [ From 558981ead14614346200369f61ec3dd267554d1a Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Fri, 22 Sep 2023 13:51:15 -0500 Subject: [PATCH 31/46] Disable pds appview routes (#1644) * wip * remove all canProxyReadc * finish cleanup * clean up tests * fix up tests * fix api tests * fix build * fix compression test * update image tests * fix dev envs * build branch * fix service file * re-enable getPopular * format * rm unused sharp code * dont build branch --- packages/api/tests/agent.test.ts | 19 +- packages/api/tests/bsky-agent.test.ts | 39 +- packages/dev-env/src/pds.ts | 13 +- packages/dev-env/src/types.ts | 1 - .../api/com/atproto/identity/resolveHandle.ts | 3 +- .../pds/src/api/com/atproto/repo/getRecord.ts | 34 +- .../app-view/api/app/bsky/actor/getProfile.ts | 55 +- .../api/app/bsky/actor/getProfiles.ts | 39 +- .../api/app/bsky/actor/getSuggestions.ts | 69 +- .../api/app/bsky/actor/searchActors.ts | 79 +- .../app/bsky/actor/searchActorsTypeahead.ts | 75 +- .../app/bsky/feed/describeFeedGenerator.ts | 21 - .../api/app/bsky/feed/getActorFeeds.ts | 59 +- .../api/app/bsky/feed/getActorLikes.ts | 75 +- .../api/app/bsky/feed/getAuthorFeed.ts | 125 +- .../src/app-view/api/app/bsky/feed/getFeed.ts | 208 +- .../api/app/bsky/feed/getFeedGenerator.ts | 68 +- .../api/app/bsky/feed/getFeedGenerators.ts | 31 +- .../app-view/api/app/bsky/feed/getLikes.ts | 73 +- .../api/app/bsky/feed/getPostThread.ts | 318 +- .../app-view/api/app/bsky/feed/getPosts.ts | 37 +- .../api/app/bsky/feed/getRepostedBy.ts | 63 +- .../app-view/api/app/bsky/feed/getTimeline.ts | 102 +- .../app-view/api/app/bsky/graph/getBlocks.ts | 62 +- .../api/app/bsky/graph/getFollowers.ts | 88 +- .../app-view/api/app/bsky/graph/getFollows.ts | 88 +- .../app-view/api/app/bsky/graph/getList.ts | 83 +- .../api/app/bsky/graph/getListMutes.ts | 50 +- .../app-view/api/app/bsky/graph/getLists.ts | 58 +- .../app-view/api/app/bsky/graph/getMutes.ts | 57 +- .../pds/src/app-view/api/app/bsky/index.ts | 2 - .../app/bsky/notification/getUnreadCount.ts | 49 +- .../bsky/notification/listNotifications.ts | 161 +- .../src/app-view/api/app/bsky/unspecced.ts | 184 +- .../src/app-view/api/app/bsky/util/feed.ts | 19 - .../pds/src/app-view/services/actor/index.ts | 73 - .../pds/src/app-view/services/actor/views.ts | 374 - .../pds/src/app-view/services/feed/index.ts | 685 -- .../pds/src/app-view/services/feed/types.ts | 107 - .../pds/src/app-view/services/feed/util.ts | 65 - .../pds/src/app-view/services/feed/views.ts | 344 - .../pds/src/app-view/services/graph/index.ts | 174 - .../services/indexing/plugins/post.ts | 10 +- packages/pds/src/config.ts | 62 +- packages/pds/src/context.ts | 36 +- packages/pds/src/feed-gen/best-of-follows.ts | 83 - packages/pds/src/feed-gen/bsky-team.ts | 46 - packages/pds/src/feed-gen/hot-classic.ts | 59 - packages/pds/src/feed-gen/index.ts | 20 - packages/pds/src/feed-gen/mutuals.ts | 60 - packages/pds/src/feed-gen/types.ts | 16 - packages/pds/src/feed-gen/whats-hot.ts | 111 - packages/pds/src/feed-gen/with-friends.ts | 62 - packages/pds/src/image/index.ts | 82 +- packages/pds/src/image/invalidator.ts | 26 - packages/pds/src/image/logger.ts | 5 - packages/pds/src/image/server.ts | 170 - packages/pds/src/image/sharp.ts | 93 - packages/pds/src/image/uri.ts | 202 - packages/pds/src/image/util.ts | 32 - packages/pds/src/index.ts | 59 +- packages/pds/src/services/index.ts | 22 +- packages/pds/src/services/moderation/index.ts | 28 +- .../feed-generation.test.ts.snap | 1382 ---- .../tests/__snapshots__/indexing.test.ts.snap | 141 - packages/pds/tests/_util.ts | 9 +- packages/pds/tests/account-deletion.test.ts | 23 - .../get-moderation-action.test.ts.snap | 0 .../get-moderation-actions.test.ts.snap | 0 .../get-moderation-report.test.ts.snap | 0 .../get-moderation-reports.test.ts.snap | 0 .../__snapshots__/get-record.test.ts.snap | 0 .../admin/__snapshots__/get-repo.test.ts.snap | 0 .../admin/get-moderation-action.test.ts | 8 +- .../admin/get-moderation-actions.test.ts | 8 +- .../admin/get-moderation-report.test.ts | 8 +- .../admin/get-moderation-reports.test.ts | 8 +- .../{views => }/admin/get-record.test.ts | 8 +- .../tests/{views => }/admin/get-repo.test.ts | 8 +- .../tests/{views => }/admin/invites.test.ts | 4 +- .../{views => }/admin/repo-search.test.ts | 8 +- packages/pds/tests/algos/hot-classic.test.ts | 93 - packages/pds/tests/algos/whats-hot.test.ts | 123 - packages/pds/tests/algos/with-friends.test.ts | 148 - packages/pds/tests/feed-generation.test.ts | 518 -- packages/pds/tests/file-uploads.test.ts | 20 - packages/pds/tests/handles.test.ts | 66 +- packages/pds/tests/image/server.test.ts | 127 - packages/pds/tests/image/sharp.test.ts | 185 - packages/pds/tests/image/uri.test.ts | 204 - packages/pds/tests/indexing.test.ts | 299 - .../pds/tests/labeler/apply-labels.test.ts | 37 +- .../pds/tests/migrations/blob-creator.test.ts | 141 - .../migrations/indexed-at-on-record.test.ts | 77 - .../migrations/repo-sync-data-pt2.test.ts | 87 - .../tests/migrations/repo-sync-data.test.ts | 153 - .../migrations/user-partitioned-cids.test.ts | 102 - .../migrations/user-table-did-pkey.test.ts | 133 - packages/pds/tests/moderation.test.ts | 21 +- .../timeline-skeleton.test.ts.snap | 2798 ------- .../tests/proxied/timeline-skeleton.test.ts | 99 - packages/pds/tests/server.test.ts | 25 +- .../__snapshots__/author-feed.test.ts.snap | 2495 ------ .../views/__snapshots__/blocks.test.ts.snap | 383 - .../views/__snapshots__/follows.test.ts.snap | 703 -- .../views/__snapshots__/likes.test.ts.snap | 120 - .../__snapshots__/mute-lists.test.ts.snap | 599 -- .../views/__snapshots__/mutes.test.ts.snap | 75 - .../__snapshots__/notifications.test.ts.snap | 1922 ----- .../views/__snapshots__/posts.test.ts.snap | 526 -- .../views/__snapshots__/profile.test.ts.snap | 286 - .../views/__snapshots__/reposts.test.ts.snap | 108 - .../views/__snapshots__/thread.test.ts.snap | 1883 ----- .../views/__snapshots__/timeline.test.ts.snap | 7343 ----------------- packages/pds/tests/views/actor-likes.test.ts | 115 - packages/pds/tests/views/actor-search.test.ts | 464 -- packages/pds/tests/views/author-feed.test.ts | 363 - packages/pds/tests/views/blocks.test.ts | 531 -- packages/pds/tests/views/follows.test.ts | 266 - packages/pds/tests/views/likes.test.ts | 97 - packages/pds/tests/views/mute-lists.test.ts | 355 - packages/pds/tests/views/mutes.test.ts | 107 - .../pds/tests/views/notifications.test.ts | 310 - packages/pds/tests/views/popular.test.ts | 110 - packages/pds/tests/views/posts.test.ts | 65 - packages/pds/tests/views/profile.test.ts | 285 - packages/pds/tests/views/reposts.test.ts | 77 - packages/pds/tests/views/suggestions.test.ts | 75 - packages/pds/tests/views/thread.test.ts | 456 - packages/pds/tests/views/timeline.test.ts | 310 - services/pds/index.js | 16 +- 131 files changed, 414 insertions(+), 32715 deletions(-) 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/util/feed.ts delete mode 100644 packages/pds/src/app-view/services/actor/index.ts delete mode 100644 packages/pds/src/app-view/services/actor/views.ts delete mode 100644 packages/pds/src/app-view/services/feed/index.ts delete mode 100644 packages/pds/src/app-view/services/feed/types.ts delete mode 100644 packages/pds/src/app-view/services/feed/util.ts delete mode 100644 packages/pds/src/app-view/services/feed/views.ts delete mode 100644 packages/pds/src/app-view/services/graph/index.ts delete mode 100644 packages/pds/src/feed-gen/best-of-follows.ts delete mode 100644 packages/pds/src/feed-gen/bsky-team.ts delete mode 100644 packages/pds/src/feed-gen/hot-classic.ts delete mode 100644 packages/pds/src/feed-gen/index.ts delete mode 100644 packages/pds/src/feed-gen/mutuals.ts delete mode 100644 packages/pds/src/feed-gen/types.ts delete mode 100644 packages/pds/src/feed-gen/whats-hot.ts delete mode 100644 packages/pds/src/feed-gen/with-friends.ts delete mode 100644 packages/pds/src/image/invalidator.ts delete mode 100644 packages/pds/src/image/logger.ts delete mode 100644 packages/pds/src/image/server.ts delete mode 100644 packages/pds/src/image/sharp.ts delete mode 100644 packages/pds/src/image/uri.ts delete mode 100644 packages/pds/src/image/util.ts delete mode 100644 packages/pds/tests/__snapshots__/feed-generation.test.ts.snap delete mode 100644 packages/pds/tests/__snapshots__/indexing.test.ts.snap rename packages/pds/tests/{views => }/admin/__snapshots__/get-moderation-action.test.ts.snap (100%) rename packages/pds/tests/{views => }/admin/__snapshots__/get-moderation-actions.test.ts.snap (100%) rename packages/pds/tests/{views => }/admin/__snapshots__/get-moderation-report.test.ts.snap (100%) rename packages/pds/tests/{views => }/admin/__snapshots__/get-moderation-reports.test.ts.snap (100%) rename packages/pds/tests/{views => }/admin/__snapshots__/get-record.test.ts.snap (100%) rename packages/pds/tests/{views => }/admin/__snapshots__/get-repo.test.ts.snap (100%) rename packages/pds/tests/{views => }/admin/get-moderation-action.test.ts (94%) rename packages/pds/tests/{views => }/admin/get-moderation-actions.test.ts (96%) rename packages/pds/tests/{views => }/admin/get-moderation-report.test.ts (94%) rename packages/pds/tests/{views => }/admin/get-moderation-reports.test.ts (98%) rename packages/pds/tests/{views => }/admin/get-record.test.ts (95%) rename packages/pds/tests/{views => }/admin/get-repo.test.ts (95%) rename packages/pds/tests/{views => }/admin/invites.test.ts (99%) rename packages/pds/tests/{views => }/admin/repo-search.test.ts (98%) delete mode 100644 packages/pds/tests/algos/hot-classic.test.ts delete mode 100644 packages/pds/tests/algos/whats-hot.test.ts delete mode 100644 packages/pds/tests/algos/with-friends.test.ts delete mode 100644 packages/pds/tests/feed-generation.test.ts delete mode 100644 packages/pds/tests/image/server.test.ts delete mode 100644 packages/pds/tests/image/sharp.test.ts delete mode 100644 packages/pds/tests/image/uri.test.ts delete mode 100644 packages/pds/tests/indexing.test.ts delete mode 100644 packages/pds/tests/migrations/blob-creator.test.ts delete mode 100644 packages/pds/tests/migrations/indexed-at-on-record.test.ts delete mode 100644 packages/pds/tests/migrations/repo-sync-data-pt2.test.ts delete mode 100644 packages/pds/tests/migrations/repo-sync-data.test.ts delete mode 100644 packages/pds/tests/migrations/user-partitioned-cids.test.ts delete mode 100644 packages/pds/tests/migrations/user-table-did-pkey.test.ts delete mode 100644 packages/pds/tests/proxied/__snapshots__/timeline-skeleton.test.ts.snap delete mode 100644 packages/pds/tests/proxied/timeline-skeleton.test.ts delete mode 100644 packages/pds/tests/views/__snapshots__/author-feed.test.ts.snap delete mode 100644 packages/pds/tests/views/__snapshots__/blocks.test.ts.snap delete mode 100644 packages/pds/tests/views/__snapshots__/follows.test.ts.snap delete mode 100644 packages/pds/tests/views/__snapshots__/likes.test.ts.snap delete mode 100644 packages/pds/tests/views/__snapshots__/mute-lists.test.ts.snap delete mode 100644 packages/pds/tests/views/__snapshots__/mutes.test.ts.snap delete mode 100644 packages/pds/tests/views/__snapshots__/notifications.test.ts.snap delete mode 100644 packages/pds/tests/views/__snapshots__/posts.test.ts.snap delete mode 100644 packages/pds/tests/views/__snapshots__/profile.test.ts.snap delete mode 100644 packages/pds/tests/views/__snapshots__/reposts.test.ts.snap delete mode 100644 packages/pds/tests/views/__snapshots__/thread.test.ts.snap delete mode 100644 packages/pds/tests/views/__snapshots__/timeline.test.ts.snap delete mode 100644 packages/pds/tests/views/actor-likes.test.ts delete mode 100644 packages/pds/tests/views/actor-search.test.ts delete mode 100644 packages/pds/tests/views/author-feed.test.ts delete mode 100644 packages/pds/tests/views/blocks.test.ts delete mode 100644 packages/pds/tests/views/follows.test.ts delete mode 100644 packages/pds/tests/views/likes.test.ts delete mode 100644 packages/pds/tests/views/mute-lists.test.ts delete mode 100644 packages/pds/tests/views/mutes.test.ts delete mode 100644 packages/pds/tests/views/notifications.test.ts delete mode 100644 packages/pds/tests/views/popular.test.ts delete mode 100644 packages/pds/tests/views/posts.test.ts delete mode 100644 packages/pds/tests/views/profile.test.ts delete mode 100644 packages/pds/tests/views/reposts.test.ts delete mode 100644 packages/pds/tests/views/suggestions.test.ts delete mode 100644 packages/pds/tests/views/thread.test.ts delete mode 100644 packages/pds/tests/views/timeline.test.ts diff --git a/packages/api/tests/agent.test.ts b/packages/api/tests/agent.test.ts index 2444e7e0c61..c0fb67b9902 100644 --- a/packages/api/tests/agent.test.ts +++ b/packages/api/tests/agent.test.ts @@ -197,7 +197,7 @@ describe('agent', () => { // put the agent through the auth flow AtpAgent.configure({ fetch: tokenExpiredFetchHandler }) - const res1 = await agent.api.app.bsky.feed.getTimeline() + const res1 = await createPost(agent) AtpAgent.configure({ fetch: defaultFetchHandler }) expect(res1.success).toEqual(true) @@ -267,9 +267,9 @@ describe('agent', () => { // put the agent through the auth flow AtpAgent.configure({ fetch: tokenExpiredFetchHandler }) const [res1, res2, res3] = await Promise.all([ - agent.api.app.bsky.feed.getTimeline(), - agent.api.app.bsky.feed.getTimeline(), - agent.api.app.bsky.feed.getTimeline(), + createPost(agent), + createPost(agent), + createPost(agent), ]) AtpAgent.configure({ fetch: defaultFetchHandler }) @@ -462,3 +462,14 @@ describe('agent', () => { }) }) }) + +const createPost = async (agent: AtpAgent) => { + return agent.api.com.atproto.repo.createRecord({ + repo: agent.session?.did ?? '', + collection: 'app.bsky.feed.post', + record: { + text: 'hello there', + createdAt: new Date().toISOString(), + }, + }) +} diff --git a/packages/api/tests/bsky-agent.test.ts b/packages/api/tests/bsky-agent.test.ts index 8ff041d3c14..8066bd61f3a 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/bsky-agent.test.ts @@ -20,6 +20,20 @@ describe('agent', () => { await close() }) + const getProfileDisplayName = async ( + agent: BskyAgent, + ): Promise => { + try { + const res = await agent.api.app.bsky.actor.profile.get({ + repo: agent.session?.did || '', + rkey: 'self', + }) + return res.value.displayName ?? '' + } catch (err) { + return undefined + } + } + it('upsertProfile correctly creates and updates profiles.', async () => { const agent = new BskyAgent({ service: server.url }) @@ -28,9 +42,8 @@ describe('agent', () => { email: 'user1@test.com', password: 'password', }) - - const profile1 = await agent.getProfile({ actor: agent.session?.did || '' }) - expect(profile1.data.displayName).toBeFalsy() + const displayName1 = await getProfileDisplayName(agent) + expect(displayName1).toBeFalsy() await agent.upsertProfile((existing) => { expect(existing).toBeFalsy() @@ -39,8 +52,8 @@ describe('agent', () => { } }) - const profile2 = await agent.getProfile({ actor: agent.session?.did || '' }) - expect(profile2.data.displayName).toBe('Bob') + const displayName2 = await getProfileDisplayName(agent) + expect(displayName2).toBe('Bob') await agent.upsertProfile((existing) => { expect(existing).toBeTruthy() @@ -49,8 +62,8 @@ describe('agent', () => { } }) - const profile3 = await agent.getProfile({ actor: agent.session?.did || '' }) - expect(profile3.data.displayName).toBe('BOB') + const displayName3 = await getProfileDisplayName(agent) + expect(displayName3).toBe('BOB') }) it('upsertProfile correctly handles CAS failures.', async () => { @@ -62,8 +75,8 @@ describe('agent', () => { password: 'password', }) - const profile1 = await agent.getProfile({ actor: agent.session?.did || '' }) - expect(profile1.data.displayName).toBeFalsy() + const displayName1 = await getProfileDisplayName(agent) + expect(displayName1).toBeFalsy() let hasConflicted = false let ranTwice = false @@ -88,8 +101,8 @@ describe('agent', () => { }) expect(ranTwice).toBe(true) - const profile2 = await agent.getProfile({ actor: agent.session?.did || '' }) - expect(profile2.data.displayName).toBe('Bob') + const displayName2 = await getProfileDisplayName(agent) + expect(displayName2).toBe('Bob') }) it('upsertProfile wont endlessly retry CAS failures.', async () => { @@ -101,8 +114,8 @@ describe('agent', () => { password: 'password', }) - const profile1 = await agent.getProfile({ actor: agent.session?.did || '' }) - expect(profile1.data.displayName).toBeFalsy() + const displayName1 = await getProfileDisplayName(agent) + expect(displayName1).toBeFalsy() const p = agent.upsertProfile(async (_existing) => { await agent.com.atproto.repo.putRecord({ diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index 97b8ceb7452..e94130f41ff 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -56,9 +56,6 @@ export class TestPds { appUrlPasswordReset: 'app://forgot-password', emailNoReplyAddress: 'noreply@blueskyweb.xyz', publicUrl: 'https://pds.public.url', - imgUriSalt: '9dd04221f5755bce5f55f47464c27e1e', - imgUriKey: - 'f23ecd142835025f42c3db2cf25dd813956c178392760256211f9d315f8ab4d8', dbPostgresUrl: cfg.dbPostgresUrl, maxSubscriptionBuffer: 200, repoBackfillLimitMs: 1000 * 60 * 60, // 1hr @@ -67,7 +64,8 @@ export class TestPds { labelerKeywords: { label_me: 'test-label', label_me_2: 'test-label-2' }, feedGenDid: 'did:example:feedGen', dbTxLockNonce: await randomStr(32, 'base32'), - bskyAppViewProxy: !!cfg.bskyAppViewEndpoint, + bskyAppViewEndpoint: cfg.bskyAppViewEndpoint ?? 'http://fake_address', + bskyAppViewDid: cfg.bskyAppViewDid ?? 'did:example:fake', bskyAppViewCdnUrlPattern: 'http://cdn.appview.com/%s/%s/%s', ...cfg, }) @@ -82,11 +80,7 @@ export class TestPds { : pds.Database.memory() await db.migrateToLatestOrThrow() - if ( - config.bskyAppViewEndpoint && - config.bskyAppViewProxy && - !cfg.enableInProcessAppView - ) { + if (cfg.bskyAppViewEndpoint && !cfg.enableInProcessAppView) { // Disable communication to app view within pds MessageDispatcher.prototype.send = async () => {} } @@ -97,7 +91,6 @@ export class TestPds { repoSigningKey, plcRotationKey, config, - algos: cfg.algos, }) await server.start() diff --git a/packages/dev-env/src/types.ts b/packages/dev-env/src/types.ts index 0aac4f3aa25..d87e78a679a 100644 --- a/packages/dev-env/src/types.ts +++ b/packages/dev-env/src/types.ts @@ -11,7 +11,6 @@ export type PdsConfig = Partial & { plcUrl: string migration?: string enableInProcessAppView?: boolean - algos?: pds.MountedAlgos enableLabelsCache?: boolean } diff --git a/packages/pds/src/api/com/atproto/identity/resolveHandle.ts b/packages/pds/src/api/com/atproto/identity/resolveHandle.ts index 494287dc096..4894103d1c1 100644 --- a/packages/pds/src/api/com/atproto/identity/resolveHandle.ts +++ b/packages/pds/src/api/com/atproto/identity/resolveHandle.ts @@ -33,8 +33,7 @@ export default function (server: Server, ctx: AppContext) { } // this is not someone on our server, but we help with resolving anyway - - if (!did && ctx.canProxyRead()) { + if (!did) { did = await tryResolveFromAppview(ctx.appviewAgent, handle) } diff --git a/packages/pds/src/api/com/atproto/repo/getRecord.ts b/packages/pds/src/api/com/atproto/repo/getRecord.ts index 2d73244b0d9..29194c94843 100644 --- a/packages/pds/src/api/com/atproto/repo/getRecord.ts +++ b/packages/pds/src/api/com/atproto/repo/getRecord.ts @@ -1,7 +1,7 @@ import { AtUri } from '@atproto/syntax' -import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { InvalidRequestError } from '@atproto/xrpc-server' export default function (server: Server, ctx: AppContext) { server.com.atproto.repo.getRecord(async ({ params }) => { @@ -14,29 +14,23 @@ export default function (server: Server, ctx: AppContext) { const record = await ctx.services .record(ctx.db) .getRecord(uri, cid || null) - if (record) { - return { - encoding: 'application/json', - body: { - uri: record.uri, - cid: record.cid, - value: record.value, - }, - } + if (!record) { + throw new InvalidRequestError(`Could not locate record: ${uri}`) } - } - - if (ctx.canProxyRead()) { - const res = await ctx.appviewAgent.api.com.atproto.repo.getRecord(params) return { encoding: 'application/json', - body: res.data, + body: { + uri: record.uri, + cid: record.cid, + value: record.value, + }, } - } else { - const uri = AtUri.make(did || repo, collection, rkey) - throw new InvalidRequestError( - `Could not locate record: ${uri.toString()}`, - ) + } + + const res = await ctx.appviewAgent.api.com.atproto.repo.getRecord(params) + return { + encoding: 'application/json', + body: res.data, } }) } 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 index 540a8cc3779..4d510bda720 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts @@ -1,6 +1,4 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../../lexicon' -import { softDeleted } from '../../../../../db/util' import AppContext from '../../../../../context' import { authPassthru } from '../../../../../api/com/atproto/admin/util' import { OutputSchema } from '../../../../../lexicon/types/app/bsky/actor/getProfile' @@ -13,57 +11,16 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ req, auth, params }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - if (ctx.canProxyRead()) { - const res = await ctx.appviewAgent.api.app.bsky.actor.getProfile( - params, - requester - ? await ctx.serviceAuthHeaders(requester) - : authPassthru(req), - ) - if (res.data.did === requester) { - return await handleReadAfterWrite( - ctx, - requester, - res, - getProfileMunge, - ) - } - return { - encoding: 'application/json', - body: res.data, - } - } - - // As long as user has triage permission, we know that they are a moderator user and can see taken down profiles - const canViewTakendownProfile = - auth.credentials.type === 'role' && auth.credentials.triage - 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 (!canViewTakendownProfile && softDeleted(actorRes)) { - throw new InvalidRequestError( - 'Account has been taken down', - 'AccountTakedown', - ) - } - const profile = await actorService.views.profileDetailed( - actorRes, - requester, - { includeSoftDeleted: canViewTakendownProfile }, + const res = await ctx.appviewAgent.api.app.bsky.actor.getProfile( + params, + requester ? await ctx.serviceAuthHeaders(requester) : authPassthru(req), ) - if (!profile) { - throw new InvalidRequestError('Profile not found') + if (res.data.did === requester) { + return await handleReadAfterWrite(ctx, requester, res, getProfileMunge) } - return { encoding: 'application/json', - body: profile, + body: res.data, } }, }) 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 index 0750a6109d7..08f30dfe690 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts @@ -9,40 +9,17 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ auth, params }) => { const requester = auth.credentials.did - if (ctx.canProxyRead()) { - const res = await ctx.appviewAgent.api.app.bsky.actor.getProfiles( - params, - await ctx.serviceAuthHeaders(requester), - ) - const hasSelf = res.data.profiles.some((prof) => prof.did === requester) - if (hasSelf) { - return await handleReadAfterWrite( - ctx, - requester, - res, - getProfilesMunge, - ) - } - return { - encoding: 'application/json', - body: res.data, - } + const res = await ctx.appviewAgent.api.app.bsky.actor.getProfiles( + params, + await ctx.serviceAuthHeaders(requester), + ) + const hasSelf = res.data.profiles.some((prof) => prof.did === requester) + if (hasSelf) { + return await handleReadAfterWrite(ctx, requester, res, getProfilesMunge) } - - 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.hydrateProfilesDetailed( - actorsRes, - requester, - ), - }, + body: res.data, } }, }) 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 index f80110dacb0..9047634dd30 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts @@ -1,5 +1,4 @@ import AppContext from '../../../../../context' -import { notSoftDeletedClause } from '../../../../../db/util' import { Server } from '../../../../../lexicon' export default function (server: Server, ctx: AppContext) { @@ -7,71 +6,13 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead()) { - 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() + const res = await ctx.appviewAgent.api.app.bsky.actor.getSuggestions( + params, + await ctx.serviceAuthHeaders(requester), + ) return { encoding: 'application/json', - body: { - cursor: suggestionsRes.at(-1)?.did, - actors: await services.appView - .actor(ctx.db) - .views.hydrateProfiles(suggestionsRes, requester), - }, + body: res.data, } }, }) 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 index 0c1e07c014a..ff88cd8d233 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts @@ -1,90 +1,19 @@ -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 ({ auth, params }) => { const requester = auth.credentials.did - if (ctx.canProxyRead()) { - 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.hydrateProfiles(results, requester) - - const filtered = actors.filter( - (actor) => !actor.viewer?.blocking && !actor.viewer?.blockedBy, + const res = await ctx.appviewAgent.api.app.bsky.actor.searchActors( + params, + await ctx.serviceAuthHeaders(requester), ) - return { encoding: 'application/json', - body: { - cursor: keyset.packFromResult(results), - actors: filtered, - }, + body: res.data, } }, }) } - -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 index bcc80d6acc3..5853cf3104e 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts @@ -1,83 +1,20 @@ import AppContext from '../../../../../context' -import Database from '../../../../../db' import { Server } from '../../../../../lexicon' -import * as Method from '../../../../../lexicon/types/app/bsky/actor/searchActorsTypeahead' -import { - cleanTerm, - getUserSearchQuerySimplePg, - 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 ({ params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead()) { - 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.hydrateProfilesBasic(results, requester) - - const filtered = actors.filter( - (actor) => !actor.viewer?.blocking && !actor.viewer?.blockedBy, - ) - + const res = + await ctx.appviewAgent.api.app.bsky.actor.searchActorsTypeahead( + params, + await ctx.serviceAuthHeaders(requester), + ) return { encoding: 'application/json', - body: { - actors: filtered, - }, + body: res.data, } }, }) } - -const getResultsPg: GetResultsFn = async (db, { term, limit }) => { - return await getUserSearchQuerySimplePg(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 index f6ded6d2c0e..57d7d72b6e4 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts @@ -1,69 +1,18 @@ 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 ({ auth, params }) => { const requester = auth.credentials.did - if (ctx.canProxyRead()) { - 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'), + const res = await ctx.appviewAgent.api.app.bsky.feed.getActorFeeds( + params, + await ctx.serviceAuthHeaders(requester), ) - feedsQb = paginate(feedsQb, { - limit, - cursor, - keyset, - }) - - const [feedsRes, creatorProfile] = await Promise.all([ - feedsQb.execute(), - actorService.views.profile(creatorRes, requester), - ]) - if (!creatorProfile) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } - - 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, - }, + body: res.data, } }, }) diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts b/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts index 5e67e9c6dcf..6e56bc81214 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts @@ -1,9 +1,5 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../../lexicon' -import { FeedKeyset } from '../util/feed' -import { paginate } from '../../../../../db/pagination' import AppContext from '../../../../../context' -import { FeedRow } from '../../../../services/feed' import { OutputSchema } from '../../../../../lexicon/types/app/bsky/feed/getAuthorFeed' import { handleReadAfterWrite } from '../util/read-after-write' import { authPassthru } from '../../../../../api/com/atproto/admin/util' @@ -16,75 +12,16 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - if (ctx.canProxyRead()) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getActorLikes( - params, - requester - ? await ctx.serviceAuthHeaders(requester) - : authPassthru(req), - ) - if (requester) { - return await handleReadAfterWrite(ctx, requester, res, getAuthorMunge) - } - return { - encoding: 'application/json', - body: res.data, - } - } - - const { actor, limit, cursor } = params - - const { ref } = ctx.db.db.dynamic - const actorService = ctx.services.appView.actor(ctx.db) - const feedService = ctx.services.appView.feed(ctx.db) - const graphService = ctx.services.appView.graph(ctx.db) - - // maybe resolve did first - const actorRes = await actorService.getActor(actor) - if (!actorRes) { - throw new InvalidRequestError('Profile not found') - } - const actorDid = actorRes.did - - if (!requester || requester !== actorDid) { - throw new InvalidRequestError('Profile not found') - } - - // defaults to posts, reposts, and replies - let feedItemsQb = feedService - .selectFeedItemQb() - .innerJoin('like', 'like.subject', 'feed_item.uri') - .where('like.creator', '=', actorDid) - - // for access-based auth, enforce blocks + const res = await ctx.appviewAgent.api.app.bsky.feed.getActorLikes( + params, + requester ? await ctx.serviceAuthHeaders(requester) : authPassthru(req), + ) if (requester) { - feedItemsQb = feedItemsQb.whereNotExists( - graphService.blockQb(requester, [ref('post.creator')]), - ) + return await handleReadAfterWrite(ctx, requester, res, getAuthorMunge) } - - 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, { - includeSoftDeleted: auth.credentials.type === 'role', // show takendown content to mods - }) - return { encoding: 'application/json', - body: { - feed, - cursor: keyset.packFromResult(feedItems), - }, + body: res.data, } }, }) 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 index 7850092dc28..735004f52e3 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts @@ -1,9 +1,5 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../../lexicon' -import { FeedKeyset } from '../util/feed' -import { paginate } from '../../../../../db/pagination' import AppContext from '../../../../../context' -import { FeedRow } from '../../../../services/feed' import { OutputSchema } from '../../../../../lexicon/types/app/bsky/feed/getAuthorFeed' import { handleReadAfterWrite } from '../util/read-after-write' import { authPassthru } from '../../../../../api/com/atproto/admin/util' @@ -16,130 +12,21 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ req, params, auth }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - if (ctx.canProxyRead()) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getAuthorFeed( - params, - requester - ? await ctx.serviceAuthHeaders(requester) - : authPassthru(req), - ) - if (requester) { - return await handleReadAfterWrite(ctx, requester, res, getAuthorMunge) - } - return { - encoding: 'application/json', - body: res.data, - } - } - - const { actor, limit, cursor } = params - - const { ref } = ctx.db.db.dynamic - const accountService = ctx.services.account(ctx.db) - const actorService = ctx.services.appView.actor(ctx.db) - const feedService = ctx.services.appView.feed(ctx.db) - const graphService = ctx.services.appView.graph(ctx.db) - - // maybe resolve did first - const actorRes = await actorService.getActor(actor) - if (!actorRes) { - throw new InvalidRequestError('Profile not found') - } - const actorDid = actorRes.did - - // defaults to posts, reposts, and replies - let feedItemsQb = feedService - .selectFeedItemQb() - .where('originatorDid', '=', actorDid) - - if (params.filter === 'posts_with_media') { - feedItemsQb = feedItemsQb - // and only your own posts/reposts - .where('post.creator', '=', actorDid) - // only posts with media - .whereExists((qb) => - qb - .selectFrom('post_embed_image') - .select('post_embed_image.postUri') - .whereRef('post_embed_image.postUri', '=', 'feed_item.postUri'), - ) - } else if (params.filter === 'posts_no_replies') { - feedItemsQb = feedItemsQb - // only posts, no replies - .where((qb) => - qb - .where('post.replyParent', 'is', null) - .orWhere('type', '=', 'repost'), - ) - } - - // for access-based auth, enforce blocks and mutes + const res = await ctx.appviewAgent.api.app.bsky.feed.getAuthorFeed( + params, + requester ? await ctx.serviceAuthHeaders(requester) : authPassthru(req), + ) if (requester) { - await assertNoBlocks(ctx, { requester, actor }) - feedItemsQb = feedItemsQb - .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')]), - ) + return await handleReadAfterWrite(ctx, requester, res, getAuthorMunge) } - - 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, { - includeSoftDeleted: auth.credentials.type === 'role', // show takendown content to mods - }) - return { encoding: 'application/json', - body: { - feed, - cursor: keyset.packFromResult(feedItems), - }, + body: res.data, } }, }) } -// throws when there's a block between the two users -async function assertNoBlocks( - ctx: AppContext, - opts: { requester: string; actor: string }, -) { - const { requester, actor } = opts - const graphService = ctx.services.appView.graph(ctx.db) - const blocks = await graphService.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 getAuthorMunge = async ( ctx: AppContext, original: OutputSchema, 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 index 5f9eb9d975c..a5cf4176d4e 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts @@ -1,217 +1,25 @@ -import { - InvalidRequestError, - UpstreamFailureError, - createServiceAuthHeaders, - ServerTimer, - serverTimingHeader, -} from '@atproto/xrpc-server' -import { ResponseType, XRPCError } from '@atproto/xrpc' -import { 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' -import { getDidDoc } from '../util/resolver' export default function (server: Server, ctx: AppContext) { - const isProxyableFeed = (feed: string): boolean => { - return feed in ctx.algos - } - server.app.bsky.feed.getFeed({ auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead()) { - 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, - } - } - let algoRes: AlgoResponse - const timerSkele = new ServerTimer('skele').start() - - if (ctx.cfg.bskyAppViewEndpoint && isProxyableFeed(params.feed)) { - // this is a temporary solution to smart proxy bsky feeds to the appview - const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedSkeleton( - params, + const { data: feed } = + await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( + { feed: params.feed }, await ctx.serviceAuthHeaders(requester), ) - algoRes = await filterMutesAndBlocks( - ctx, - res.data, - params.limit, - requester, - ) - } else { - algoRes = await skeletonFromFeedGen(ctx, params, requester) - } - - timerSkele.stop() - - const feedService = ctx.services.appView.feed(ctx.db) - const { feedItems, ...rest } = algoRes - - const timerHydr = new ServerTimer('hydr').start() - const hydrated = await feedService.hydrateFeed(feedItems, requester) - timerHydr.stop() - + const res = await ctx.appviewAgent.api.app.bsky.feed.getFeed( + params, + await ctx.serviceAuthHeaders(requester, feed.view.did), + ) return { encoding: 'application/json', - body: { - ...rest, - feed: hydrated, - }, - headers: { - 'server-timing': serverTimingHeader([timerSkele, timerHydr]), - }, + body: res.data, } }, }) } - -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 - - const doc = await getDidDoc(ctx, feedDid) - const fgEndpoint = getFeedGen(doc) - 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 - } - - return filterMutesAndBlocks(ctx, skeleton, params.limit, requester) -} - -export async function filterMutesAndBlocks( - ctx: AppContext, - skeleton: SkeletonOutput, - limit: number, - requester: string, -) { - const { feed: skeletonFeed, ...rest } = 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, limit) - 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[], - limit: number, -) { - 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 > limit) { - skeletonItems = skeletonItems.slice(0, 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 index 66e0cc8fdc5..210b1be54ef 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts @@ -1,9 +1,3 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { - DidDocument, - PoorlyFormattedDidDocumentError, - getFeedGen, -} from '@atproto/identity' import { Server } from '../../../../../lexicon' import AppContext from '../../../../../context' @@ -12,67 +6,13 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead()) { - 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.getFeedGeneratorInfos([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.getActorInfos( - [feedInfo.creator], - requester, - ) - const feedView = feedService.views.formatFeedGeneratorView( - feedInfo, - profiles, + const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( + params, + await ctx.serviceAuthHeaders(requester), ) - return { encoding: 'application/json', - body: { - view: feedView, - // @TODO temporarily hard-coding to true while external feedgens catch-up on describeFeedGenerator - isOnline: true, - isValid: true, - }, + body: res.data, } }, }) 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 index 3cf7f8eef11..36353f015b7 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts @@ -6,36 +6,13 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead()) { - 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 genInfos = await feedService.getFeedGeneratorInfos(feeds, requester) - const genList = Object.values(genInfos) - - const creators = genList.map((gen) => gen.creator) - const profiles = await feedService.getActorInfos(creators, requester) - - const feedViews = genList.map((gen) => - feedService.views.formatFeedGeneratorView(gen, profiles), + const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerators( + params, + await ctx.serviceAuthHeaders(requester), ) - return { encoding: 'application/json', - body: { - feeds: feedViews, - }, + body: res.data, } }, }) 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 index e6658863bed..a78469355d5 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts @@ -1,83 +1,18 @@ -import { mapDefined } from '@atproto/common' 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 ({ params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead()) { - 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.profiles(likesRes, requester) - - const likes = mapDefined(likesRes, (row) => - actors[row.did] - ? { - createdAt: row.createdAt, - indexedAt: row.indexedAt, - actor: actors[row.did], - } - : undefined, + const res = await ctx.appviewAgent.api.app.bsky.feed.getLikes( + params, + await ctx.serviceAuthHeaders(requester), ) - return { encoding: 'application/json', - body: { - uri, - cid, - cursor: keyset.packFromResult(likesRes), - likes, - }, + body: res.data, } }, }) 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 index fb5276f2452..be1e8004bac 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts @@ -1,27 +1,10 @@ import { AtUri } from '@atproto/syntax' import { AppBskyFeedGetPostThread } from '@atproto/api' import { Headers } from '@atproto/xrpc' -import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../../lexicon' import AppContext from '../../../../../context' import { - ActorInfoMap, - PostEmbedViews, - FeedRow, - FeedService, - PostInfoMap, - PostBlocksMap, -} from '../../../../services/feed' -import { - getAncestorsAndSelfQb, - getDescendentsQb, -} from '../../../../services/feed/util' -import { Labels } from '../../../../services/label' -import { - BlockedPost, - NotFoundPost, ThreadViewPost, - isNotFoundPost, isThreadViewPost, } from '../../../../../lexicon/types/app/bsky/feed/defs' import { Record as PostRecord } from '../../../../../lexicon/types/app/bsky/feed/post' @@ -40,294 +23,51 @@ import { handleReadAfterWrite, } from '../util/read-after-write' -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 ({ params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead()) { - try { - const res = await ctx.appviewAgent.api.app.bsky.feed.getPostThread( - params, - await ctx.serviceAuthHeaders(requester), - ) - return await handleReadAfterWrite( + try { + const res = await ctx.appviewAgent.api.app.bsky.feed.getPostThread( + params, + await ctx.serviceAuthHeaders(requester), + ) + return await handleReadAfterWrite( + ctx, + requester, + res, + getPostThreadMunge, + ) + } catch (err) { + if (err instanceof AppBskyFeedGetPostThread.NotFoundError) { + const local = await readAfterWriteNotFound( ctx, + params, requester, - res, - getPostThreadMunge, + err.headers, ) - } catch (err) { - if (err instanceof AppBskyFeedGetPostThread.NotFoundError) { - const local = await readAfterWriteNotFound( - ctx, - params, - requester, - err.headers, - ) - if (local === null) { - throw err - } else { - return { - encoding: 'application/json', - body: local.data, - headers: local.lag - ? { - 'Atproto-Upstream-Lag': local.lag.toString(10), - } - : undefined, - } - } - } else { + if (local === null) { throw err + } else { + return { + encoding: 'application/json', + body: local.data, + headers: local.lag + ? { + 'Atproto-Upstream-Lag': local.lag.toString(10), + } + : undefined, + } } + } else { + throw err } } - - 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(ctx, uri, depth, parentHeight) - if (!threadData) { - throw new InvalidRequestError(`Post not found: ${uri}`, 'NotFound') - } - const relevant = getRelevantIds(threadData) - const [actors, posts, labels] = await Promise.all([ - feedService.getActorInfos(Array.from(relevant.dids), requester, { - skipLabels: true, - }), - feedService.getPostInfos(Array.from(relevant.uris), requester), - labelService.getLabelsForSubjects([...relevant.uris, ...relevant.dids]), - ]) - const blocks = await feedService.blocksForPosts(posts) - const embeds = await feedService.embedsForPosts(posts, blocks, requester) - - const thread = composeThread( - threadData, - feedService, - posts, - actors, - embeds, - blocks, - 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: ActorInfoMap, - embeds: PostEmbedViews, - blocks: PostBlocksMap, - labels: Labels, -): ThreadViewPost | NotFoundPost | BlockedPost => { - const post = feedService.views.formatPostView( - threadData.post.postUri, - actors, - posts, - embeds, - labels, - ) - - if (!post || blocks[post.uri]?.reply) { - 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, - author: { - did: post.author.did, - viewer: post.author.viewer - ? { - blockedBy: post.author.viewer?.blockedBy, - blocking: post.author.viewer?.blocking, - } - : undefined, - }, - } - } - - 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, - blocks, - labels, - ) - } - } - - let replies: (ThreadViewPost | NotFoundPost | BlockedPost)[] | undefined - if (threadData.replies) { - replies = threadData.replies.flatMap((reply) => { - const thread = composeThread( - reply, - feedService, - posts, - actors, - embeds, - blocks, - labels, - ) - // e.g. don't bother including #postNotFound reply placeholders for takedowns. either way matches api contract. - const skip = [] - return isNotFoundPost(thread) ? skip : thread - }) - } - - 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 ( - ctx: AppContext, - uri: string, - depth: number, - parentHeight: number, -): Promise => { - const feedService = ctx.services.appView.feed(ctx.db) - const [parents, children] = await Promise.all([ - getAncestorsAndSelfQb(ctx.db.db, { uri, parentHeight }) - .selectFrom('ancestor') - .innerJoin( - feedService.selectPostQb().as('post'), - 'post.uri', - 'ancestor.uri', - ) - .selectAll('post') - .execute(), - getDescendentsQb(ctx.db.db, { uri, depth }) - .selectFrom('descendent') - .innerJoin( - feedService.selectPostQb().as('post'), - 'post.uri', - 'descendent.uri', - ) - .selectAll('post') - .orderBy('sortAt', '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}`) - } -} - // READ AFTER WRITE // ---------------- 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 index 25bc3d652e4..f394ae57a08 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts @@ -1,45 +1,18 @@ -import * as common from '@atproto/common' import { Server } from '../../../../../lexicon' import AppContext from '../../../../../context' -import { PostView } from '../../../../../lexicon/types/app/bsky/feed/defs' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getPosts({ auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead()) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getPosts( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const uris = common.dedupeStrs(params.uris) - - const postViews = await ctx.services.appView - .feed(ctx.db) - .getPostViews(uris, requester) - - const posts: PostView[] = [] - for (const uri of uris) { - const post = postViews[uri] - const isBlocked = - post?.author.viewer?.blockedBy === true || - typeof post?.author.viewer?.blocking === 'string' - - if (post && !isBlocked) { - posts.push(post) - } - } - + const res = await ctx.appviewAgent.api.app.bsky.feed.getPosts( + params, + await ctx.serviceAuthHeaders(requester), + ) return { encoding: 'application/json', - body: { posts }, + body: res.data, } }, }) 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 index f00f9a1a2b5..9704597ad27 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts @@ -1,73 +1,18 @@ 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 ({ params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead()) { - 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'), + const res = await ctx.appviewAgent.api.app.bsky.feed.getRepostedBy( + params, + await ctx.serviceAuthHeaders(requester), ) - builder = paginate(builder, { - limit, - cursor, - keyset, - }) - - const repostedByRes = await builder.execute() - const repostedBy = await services.appView - .actor(db) - .views.hydrateProfiles(repostedByRes, requester) - return { encoding: 'application/json', - body: { - uri, - cid, - repostedBy, - cursor: keyset.packFromResult(repostedByRes), - }, + body: res.data, } }, }) 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 index 5b1e29168a1..08676b3a943 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts @@ -1,10 +1,5 @@ -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' -import { filterMutesAndBlocks } from './getFeed' import { OutputSchema } from '../../../../../lexicon/types/app/bsky/feed/getTimeline' import { handleReadAfterWrite } from '../util/read-after-write' import { LocalRecords } from '../../../../../services/local' @@ -14,100 +9,11 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const { algorithm, limit, cursor } = params - if (algorithm && algorithm !== FeedAlgorithm.ReverseChronological) { - throw new InvalidRequestError(`Unsupported algorithm: ${algorithm}`) - } - - if (ctx.canProxyRead()) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getTimeline( - params, - await ctx.serviceAuthHeaders(requester), - ) - return await handleReadAfterWrite(ctx, requester, res, getTimelineMunge) - } - - if (ctx.cfg.bskyAppViewEndpoint) { - const res = - await ctx.appviewAgent.api.app.bsky.unspecced.getTimelineSkeleton( - { limit, cursor }, - await ctx.serviceAuthHeaders(requester), - ) - const filtered = await filterMutesAndBlocks( - ctx, - res.data, - limit, - requester, - ) - const hydrated = await ctx.services.appView - .feed(ctx.db) - .hydrateFeed(filtered.feedItems, requester) - return { - encoding: 'application/json', - body: { - cursor: filtered.cursor, - feed: hydrated, - }, - } - } - - 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 followingIdsSubquery = db - .selectFrom('follow') - .select('follow.subjectDid') - .where('follow.creator', '=', requester) - - const keyset = new FeedKeyset( - ref('feed_item.sortAt'), - ref('feed_item.cid'), + const res = await ctx.appviewAgent.api.app.bsky.feed.getTimeline( + params, + await ctx.serviceAuthHeaders(requester), ) - 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), - }, - } + return await handleReadAfterWrite(ctx, requester, res, getTimelineMunge) }, }) } 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 index 11acf6352a5..52f7b4d6f7f 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts @@ -1,72 +1,18 @@ 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 ({ params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead()) { - 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'), + const res = await ctx.appviewAgent.api.app.bsky.graph.getBlocks( + params, + await ctx.serviceAuthHeaders(requester), ) - blocksReq = paginate(blocksReq, { - limit, - cursor, - keyset, - }) - - const blocksRes = await blocksReq.execute() - - const actorService = services.appView.actor(db) - const blocks = await actorService.views.hydrateProfiles( - blocksRes, - requester, - ) - return { encoding: 'application/json', - body: { - blocks, - cursor: keyset.packFromResult(blocksRes), - }, + body: res.data, } }, }) 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 index 73a38e83df2..0c7f23869eb 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts @@ -1,8 +1,5 @@ -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' import { authPassthru } from '../../../../../api/com/atproto/admin/util' export default function (server: Server, ctx: AppContext) { @@ -11,90 +8,13 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ req, params, auth }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - if (ctx.canProxyRead()) { - const res = await ctx.appviewAgent.api.app.bsky.graph.getFollowers( - params, - requester - ? await ctx.serviceAuthHeaders(requester) - : authPassthru(req), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const canViewTakendownProfile = - auth.credentials.type === 'role' && auth.credentials.triage - 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, - canViewTakendownProfile, + const res = await ctx.appviewAgent.api.app.bsky.graph.getFollowers( + params, + requester ? await ctx.serviceAuthHeaders(requester) : authPassthru(req), ) - 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', - ) - .if(!canViewTakendownProfile, (qb) => - qb.where(notSoftDeletedClause(ref('creator_repo'))), - ) - .whereNotExists( - graphService.blockQb(requester, [ref('follow.creator')]), - ) - .whereNotExists( - graphService.blockRefQb( - ref('follow.subjectDid'), - ref('follow.creator'), - ), - ) - .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.hydrateProfiles(followersRes, requester, { - includeSoftDeleted: canViewTakendownProfile, - }), - actorService.views.profile(subjectRes, requester, { - includeSoftDeleted: canViewTakendownProfile, - }), - ]) - if (!subject) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } - return { encoding: 'application/json', - body: { - subject, - followers, - cursor: keyset.packFromResult(followersRes), - }, + body: res.data, } }, }) 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 index 3ebd47f6210..5871a213302 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts @@ -1,8 +1,5 @@ -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' import { authPassthru } from '../../../../../api/com/atproto/admin/util' export default function (server: Server, ctx: AppContext) { @@ -11,90 +8,13 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ req, params, auth }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - if (ctx.canProxyRead()) { - const res = await ctx.appviewAgent.api.app.bsky.graph.getFollows( - params, - requester - ? await ctx.serviceAuthHeaders(requester) - : authPassthru(req), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const canViewTakendownProfile = - auth.credentials.type === 'role' && auth.credentials.triage - 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, - canViewTakendownProfile, + const res = await ctx.appviewAgent.api.app.bsky.graph.getFollows( + params, + requester ? await ctx.serviceAuthHeaders(requester) : authPassthru(req), ) - 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', - ) - .if(!canViewTakendownProfile, (qb) => - qb.where(notSoftDeletedClause(ref('subject_repo'))), - ) - .whereNotExists( - graphService.blockQb(requester, [ref('follow.subjectDid')]), - ) - .whereNotExists( - graphService.blockRefQb( - ref('follow.subjectDid'), - 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.hydrateProfiles(followsRes, requester, { - includeSoftDeleted: canViewTakendownProfile, - }), - actorService.views.profile(creatorRes, requester, { - includeSoftDeleted: canViewTakendownProfile, - }), - ]) - if (!subject) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } - return { encoding: 'application/json', - body: { - subject, - follows, - cursor: keyset.packFromResult(followsRes), - }, + body: res.data, } }, }) 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 index a7a1556ec18..ba9b9f9346f 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getList.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getList.ts @@ -1,6 +1,4 @@ -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) { @@ -8,86 +6,13 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead()) { - 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'), + const res = await ctx.appviewAgent.api.app.bsky.graph.getList( + params, + await ctx.serviceAuthHeaders(requester), ) - itemsReq = paginate(itemsReq, { - limit, - cursor, - keyset, - }) - const itemsRes = await itemsReq.execute() - - const actorService = services.appView.actor(db) - const profiles = await actorService.views.hydrateProfiles( - itemsRes, - requester, - ) - - const items = profiles.map((subject) => ({ subject })) - - const creator = await actorService.views.profile(listRes, requester) - if (!creator) { - throw new InvalidRequestError(`Actor not found: ${listRes.handle}`) - } - - const subject = { - uri: listRes.uri, - cid: listRes.cid, - 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), - }, + body: res.data, } }, }) 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 index 673b2c61b38..adae96d58b0 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts @@ -1,5 +1,4 @@ import { Server } from '../../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../../db/pagination' import AppContext from '../../../../../context' export default function (server: Server, ctx: AppContext) { @@ -7,54 +6,13 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead()) { - 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.profiles(listsRes, requester) - - const lists = listsRes.map((row) => - graphService.formatListView(row, profiles), + const res = await ctx.appviewAgent.api.app.bsky.graph.getListMutes( + params, + await ctx.serviceAuthHeaders(requester), ) - return { encoding: 'application/json', - body: { - lists, - cursor: keyset.packFromResult(listsRes), - }, + body: res.data, } }, }) 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 index c6653726459..893b6d5a9ad 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts @@ -1,6 +1,4 @@ -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) { @@ -8,61 +6,13 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead()) { - 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), - ]) - if (!creator) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } - const profileMap = { - [creator.did]: creator, - } - - const lists = listsRes.map((row) => - graphService.formatListView(row, profileMap), + const res = await ctx.appviewAgent.api.app.bsky.graph.getLists( + params, + await ctx.serviceAuthHeaders(requester), ) - return { encoding: 'application/json', - body: { - lists, - cursor: keyset.packFromResult(listsRes), - }, + body: res.data, } }, }) 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 index a082666a968..596fef9dd15 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts @@ -1,68 +1,19 @@ 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 ({ auth, params }) => { const requester = auth.credentials.did - if (ctx.canProxyRead()) { - 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'), + const res = await ctx.appviewAgent.api.app.bsky.graph.getMutes( + params, + await ctx.serviceAuthHeaders(requester), ) - 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.hydrateProfiles(mutesRes, requester), - }, + body: res.data, } }, }) } - -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/index.ts b/packages/pds/src/app-view/api/app/bsky/index.ts index 96b0b5de06c..69cf8432224 100644 --- a/packages/pds/src/app-view/api/app/bsky/index.ts +++ b/packages/pds/src/app-view/api/app/bsky/index.ts @@ -6,7 +6,6 @@ import getSuggestedFeeds from './feed/getSuggestedFeeds' 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 getListFeed from './feed/getListFeed' @@ -45,7 +44,6 @@ export default function (server: Server, ctx: AppContext) { getAuthorFeed(server, ctx) getFeedGenerator(server, ctx) getFeedGenerators(server, ctx) - describeFeedGenerator(server, ctx) getFeed(server, ctx) getLikes(server, ctx) getListFeed(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 index 262cfa06fe0..61c2b48f1e3 100644 --- a/packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts +++ b/packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts @@ -1,6 +1,4 @@ -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) { @@ -8,51 +6,14 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ auth, params }) => { const requester = auth.credentials.did - if (ctx.canProxyRead()) { - 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')]), + const res = + await ctx.appviewAgent.api.app.bsky.notification.getUnreadCount( + params, + await ctx.serviceAuthHeaders(requester), ) - .whereRef('notif.indexedAt', '>', 'user_state.lastSeenNotifs') - .executeTakeFirst() - - const count = result?.count ?? 0 - return { encoding: 'application/json', - body: { count }, + body: res.data, } }, }) 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 index d3e44a90aa2..eefb3d29a48 100644 --- a/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts +++ b/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts @@ -1,171 +1,20 @@ -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' -import { getSelfLabels } from '../../../../services/label' export default function (server: Server, ctx: AppContext) { server.app.bsky.notification.listNotifications({ auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead()) { - 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')]), + const res = + await ctx.appviewAgent.api.app.bsky.notification.listNotifications( + params, + await ctx.serviceAuthHeaders(requester), ) - .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.profiles( - 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 = common.mapDefined(notifs, (notif) => { - const bytes = bytesByCid[notif.cid] - const author = authors[notif.authorDid] - if (!bytes || !author) return undefined - const record = common.cborBytesToRecord(bytes) - const recordLabels = labels[notif.uri] ?? [] - const recordSelfLabels = getSelfLabels({ - uri: notif.uri, - cid: notif.cid, - record, - }) - return { - uri: notif.uri, - cid: notif.cid, - author: authors[notif.authorDid], - reason: notif.reason, - reasonSubject: notif.reasonSubject || undefined, - record, - isRead: notif.indexedAt <= userState.lastSeenNotifs, - indexedAt: notif.indexedAt, - labels: [...recordLabels, ...recordSelfLabels], - } - }) - return { encoding: 'application/json', - body: { - notifications, - cursor: keyset.packFromResult(notifs), - }, + body: res.data, } }, }) } - -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/unspecced.ts b/packages/pds/src/app-view/api/app/bsky/unspecced.ts index 9dc3f77aba4..482adb3dbd8 100644 --- a/packages/pds/src/app-view/api/app/bsky/unspecced.ts +++ b/packages/pds/src/app-view/api/app/bsky/unspecced.ts @@ -1,129 +1,29 @@ -import { NotEmptyArray } from '@atproto/common' import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' -import { GenericKeyset, paginate } from '../../../../db/pagination' +import { GenericKeyset } from '../../../../db/pagination' import AppContext from '../../../../context' -import { FeedRow } from '../../../services/feed' -import { - isPostView, - GeneratorView, -} from '../../../../lexicon/types/app/bsky/feed/defs' -import { isViewRecord } from '../../../../lexicon/types/app/bsky/embed/record' -import { countAll, valuesList } from '../../../../db/util' -import { FeedKeyset } from './util/feed' -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 ({ params, auth }) => { + handler: async ({ auth, params }) => { const requester = auth.credentials.did - if (ctx.canProxyRead()) { - 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( - { feed: hotClassicUri, limit: params.limit, cursor: params.cursor }, - await ctx.serviceAuthHeaders(requester, feed.view.did), - ) - return { - encoding: 'application/json', - body: res.data, - } + const HOT_CLASSIC_URI = + 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/hot-classic' + const HOT_CLASSIC_DID = 'did:plc:5fllqkujj6kqp5izd5jg7gox' + const res = await ctx.appviewAgent.api.app.bsky.feed.getFeed( + { feed: HOT_CLASSIC_URI, limit: params.limit, cursor: params.cursor }, + await ctx.serviceAuthHeaders(requester, HOT_CLASSIC_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), - }, + body: { feed: [] }, } }, }) @@ -132,62 +32,14 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ auth, params }) => { const requester = auth.credentials.did - const db = ctx.db.db - const { limit, cursor, query } = params - const { ref } = db.dynamic - const feedService = ctx.services.appView.feed(ctx.db) - - let inner = ctx.db.db - .selectFrom('feed_generator') - .select([ - 'uri', - 'cid', - ctx.db.db - .selectFrom('like') - .whereRef('like.subject', '=', ref('feed_generator.uri')) - .select(countAll.as('count')) - .as('likeCount'), - ]) - - if (query) { - // like is case-insensitive is sqlite, and ilike is not supported - const operator = ctx.db.dialect === 'pg' ? 'ilike' : 'like' - inner = inner.where((qb) => - qb - .where('feed_generator.displayName', operator, `%${query}%`) - .orWhere('feed_generator.description', operator, `%${query}%`), + const res = + await ctx.appviewAgent.api.app.bsky.unspecced.getPopularFeedGenerators( + params, + await ctx.serviceAuthHeaders(requester), ) - } - - let builder = ctx.db.db.selectFrom(inner.as('feed_gens')).selectAll() - - const keyset = new LikeCountKeyset(ref('likeCount'), ref('cid')) - builder = paginate(builder, { limit, cursor, keyset, direction: 'desc' }) - - const res = await builder.execute() - - const genInfos = await feedService.getFeedGeneratorInfos( - res.map((feed) => feed.uri), - requester, - ) - - const creators = Object.values(genInfos).map((gen) => gen.creator) - const profiles = await feedService.getActorInfos(creators, requester) - - const genViews: GeneratorView[] = [] - for (const row of res) { - const gen = genInfos[row.uri] - if (!gen) continue - const view = feedService.views.formatFeedGeneratorView(gen, profiles) - genViews.push(view) - } - return { encoding: 'application/json', - body: { - cursor: keyset.packFromResult(res), - feeds: genViews, - }, + body: res.data, } }, }) 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 9d9d0323b95..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 = 1) => { - const timelineDateThreshold = from ? new Date(from) : new Date() - timelineDateThreshold.setDate(timelineDateThreshold.getDate() - days) - return timelineDateThreshold.toISOString() -} diff --git a/packages/pds/src/app-view/services/actor/index.ts b/packages/pds/src/app-view/services/actor/index.ts deleted file mode 100644 index e6c6fd6e756..00000000000 --- a/packages/pds/src/app-view/services/actor/index.ts +++ /dev/null @@ -1,73 +0,0 @@ -import Database from '../../../db' -import { DidHandle } from '../../../db/tables/did-handle' -import { notSoftDeletedClause } from '../../../db/util' -import { ActorViews } from './views' -import { ImageUriBuilder } from '../../../image/uri' -import { LabelCache } from '../../../label-cache' - -export class ActorService { - constructor( - public db: Database, - public imgUriBuilder: ImageUriBuilder, - public labelCache: LabelCache, - ) {} - - static creator(imgUriBuilder: ImageUriBuilder, labelCache: LabelCache) { - return (db: Database) => new ActorService(db, imgUriBuilder, labelCache) - } - - views = new ActorViews(this.db, this.imgUriBuilder, this.labelCache) - - async getActor( - handleOrDid: string, - includeSoftDeleted = false, - ): Promise { - const actors = await this.getActors([handleOrDid], includeSoftDeleted) - return actors[0] || null - } - - async getActors( - handleOrDids: string[], - includeSoftDeleted = false, - ): Promise { - const { ref } = this.db.db.dynamic - const dids: string[] = [] - const handles: string[] = [] - const order: Record = {} - handleOrDids.forEach((item, i) => { - if (item.startsWith('did:')) { - order[item] = i - dids.push(item) - } else { - order[item.toLowerCase()] = i - handles.push(item.toLowerCase()) - } - }) - const results = await this.db.db - .selectFrom('did_handle') - .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') - .if(!includeSoftDeleted, (qb) => - qb.where(notSoftDeletedClause(ref('repo_root'))), - ) - .where((qb) => { - if (dids.length) { - qb = qb.orWhere('did_handle.did', 'in', dids) - } - if (handles.length) { - qb = qb.orWhere('did_handle.handle', 'in', handles) - } - return qb - }) - .selectAll('did_handle') - .select('takedownId') - .execute() - - return results.sort((a, b) => { - const orderA = order[a.did] ?? order[a.handle.toLowerCase()] - const orderB = order[b.did] ?? order[b.handle.toLowerCase()] - return orderA - orderB - }) - } -} - -type ActorResult = DidHandle & { takedownId: number | null } diff --git a/packages/pds/src/app-view/services/actor/views.ts b/packages/pds/src/app-view/services/actor/views.ts deleted file mode 100644 index 3e4a1a91f7f..00000000000 --- a/packages/pds/src/app-view/services/actor/views.ts +++ /dev/null @@ -1,374 +0,0 @@ -import { mapDefined } from '@atproto/common' -import { cborToLexRecord } from '@atproto/repo' -import { - ProfileViewDetailed, - ProfileView, - ProfileViewBasic, -} from '../../../lexicon/types/app/bsky/actor/defs' -import { DidHandle } from '../../../db/tables/did-handle' -import Database from '../../../db' -import { ImageUriBuilder } from '../../../image/uri' -import { LabelService, getSelfLabels } from '../label' -import { GraphService } from '../graph' -import { LabelCache } from '../../../label-cache' -import { notSoftDeletedClause } from '../../../db/util' - -export class ActorViews { - constructor( - private db: Database, - private imgUriBuilder: ImageUriBuilder, - private labelCache: LabelCache, - ) {} - - services = { - label: LabelService.creator(this.labelCache)(this.db), - graph: GraphService.creator(this.imgUriBuilder)(this.db), - } - - async profilesDetailed( - results: ActorResult[], - viewer: string | null, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise> { - if (results.length === 0) return {} - - const { ref } = this.db.db.dynamic - const { skipLabels = false, includeSoftDeleted = false } = opts ?? {} - - const dids = results.map((r) => r.did) - - const profileInfosQb = this.db.db - .selectFrom('did_handle') - .where('did_handle.did', 'in', dids) - .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') - .leftJoin('profile', 'profile.creator', 'did_handle.did') - .leftJoin('profile_agg', 'profile_agg.did', 'did_handle.did') - .leftJoin('ipld_block', (join) => - join - .onRef('ipld_block.cid', '=', 'profile.cid') - .onRef('ipld_block.creator', '=', 'profile.creator'), - ) - .if(!includeSoftDeleted, (qb) => - qb.where(notSoftDeletedClause(ref('repo_root'))), - ) - .select([ - 'did_handle.did as did', - 'did_handle.handle as handle', - 'profile.uri as profileUri', - 'profile.cid as profileCid', - 'profile.displayName as displayName', - 'profile.description as description', - 'profile.avatarCid as avatarCid', - 'profile.bannerCid as bannerCid', - 'profile.indexedAt as indexedAt', - 'profile_agg.followsCount as followsCount', - 'profile_agg.followersCount as followersCount', - 'profile_agg.postsCount as postsCount', - 'ipld_block.content as profileBytes', - this.db.db - .selectFrom('follow') - .where('creator', '=', viewer ?? '') - .whereRef('subjectDid', '=', ref('did_handle.did')) - .select('uri') - .as('requesterFollowing'), - this.db.db - .selectFrom('follow') - .whereRef('creator', '=', ref('did_handle.did')) - .where('subjectDid', '=', viewer ?? '') - .select('uri') - .as('requesterFollowedBy'), - this.db.db - .selectFrom('actor_block') - .where('creator', '=', viewer ?? '') - .whereRef('subjectDid', '=', ref('did_handle.did')) - .select('uri') - .as('requesterBlocking'), - this.db.db - .selectFrom('actor_block') - .whereRef('creator', '=', ref('did_handle.did')) - .where('subjectDid', '=', viewer ?? '') - .select('uri') - .as('requesterBlockedBy'), - this.db.db - .selectFrom('mute') - .whereRef('did', '=', ref('did_handle.did')) - .where('mutedByDid', '=', viewer ?? '') - .select('did') - .as('requesterMuted'), - this.db.db - .selectFrom('list_item') - .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') - .where('list_mute.mutedByDid', '=', viewer ?? '') - .whereRef('list_item.subjectDid', '=', ref('did_handle.did')) - .select('list_item.listUri') - .limit(1) - .as('requesterMutedByList'), - ]) - - const [profileInfos, labels] = await Promise.all([ - profileInfosQb.execute(), - this.services.label.getLabelsForSubjects(skipLabels ? [] : dids), - ]) - - const listUris: string[] = profileInfos - .map((a) => a.requesterMutedByList) - .filter((list) => !!list) - const listViews = await this.services.graph.getListViews(listUris, viewer) - - return profileInfos.reduce((acc, cur) => { - const avatar = cur?.avatarCid - ? this.imgUriBuilder.getCommonSignedUri('avatar', cur.avatarCid) - : undefined - const banner = cur?.bannerCid - ? this.imgUriBuilder.getCommonSignedUri('banner', cur.bannerCid) - : undefined - const mutedByList = - cur.requesterMutedByList && listViews[cur.requesterMutedByList] - ? this.services.graph.formatListViewBasic( - listViews[cur.requesterMutedByList], - ) - : undefined - const actorLabels = labels[cur.did] ?? [] - const selfLabels = getSelfLabels({ - uri: cur.profileUri, - cid: cur.profileCid, - record: cur.profileBytes && cborToLexRecord(cur.profileBytes), - }) - const profile = { - did: cur.did, - handle: cur.handle, - displayName: truncateUtf8(cur?.displayName, 64) || undefined, - description: truncateUtf8(cur?.description, 256) || undefined, - avatar, - banner, - followsCount: cur?.followsCount || 0, - followersCount: cur?.followersCount || 0, - postsCount: cur?.postsCount || 0, - indexedAt: cur?.indexedAt || undefined, - viewer: { - muted: !!cur?.requesterMuted || !!cur?.requesterMutedByList, - mutedByList, - blockedBy: !!cur.requesterBlockedBy, - blocking: cur.requesterBlocking || undefined, - following: cur?.requesterFollowing || undefined, - followedBy: cur?.requesterFollowedBy || undefined, - }, - labels: skipLabels ? undefined : [...actorLabels, ...selfLabels], - } - acc[cur.did] = profile - return acc - }, {} as Record) - } - - async hydrateProfilesDetailed( - results: ActorResult[], - viewer: string, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise { - const profiles = await this.profilesDetailed(results, viewer, opts) - return mapDefined(results, (result) => profiles[result.did]) - } - - async profileDetailed( - result: ActorResult, - viewer: string | null, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise { - const profiles = await this.profilesDetailed([result], viewer, opts) - return profiles[result.did] ?? null - } - - async profiles( - results: ActorResult[], - viewer: string | null, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise> { - if (results.length === 0) return {} - - const { ref } = this.db.db.dynamic - const { skipLabels = false, includeSoftDeleted = false } = opts ?? {} - const dids = results.map((r) => r.did) - - const profileInfosQb = this.db.db - .selectFrom('did_handle') - .where('did_handle.did', 'in', dids) - .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') - .leftJoin('profile', 'profile.creator', 'did_handle.did') - .leftJoin('ipld_block', (join) => - join - .onRef('ipld_block.cid', '=', 'profile.cid') - .onRef('ipld_block.creator', '=', 'profile.creator'), - ) - .if(!includeSoftDeleted, (qb) => - qb.where(notSoftDeletedClause(ref('repo_root'))), - ) - .select([ - 'did_handle.did as did', - 'did_handle.handle as handle', - 'profile.uri as profileUri', - 'profile.cid as profileCid', - 'profile.displayName as displayName', - 'profile.description as description', - 'profile.avatarCid as avatarCid', - 'profile.indexedAt as indexedAt', - 'ipld_block.content as profileBytes', - this.db.db - .selectFrom('follow') - .where('creator', '=', viewer ?? '') - .whereRef('subjectDid', '=', ref('did_handle.did')) - .select('uri') - .as('requesterFollowing'), - this.db.db - .selectFrom('follow') - .whereRef('creator', '=', ref('did_handle.did')) - .where('subjectDid', '=', viewer ?? '') - .select('uri') - .as('requesterFollowedBy'), - this.db.db - .selectFrom('actor_block') - .where('creator', '=', viewer ?? '') - .whereRef('subjectDid', '=', ref('did_handle.did')) - .select('uri') - .as('requesterBlocking'), - this.db.db - .selectFrom('actor_block') - .whereRef('creator', '=', ref('did_handle.did')) - .where('subjectDid', '=', viewer ?? '') - .select('uri') - .as('requesterBlockedBy'), - this.db.db - .selectFrom('mute') - .whereRef('did', '=', ref('did_handle.did')) - .where('mutedByDid', '=', viewer ?? '') - .select('did') - .as('requesterMuted'), - this.db.db - .selectFrom('list_item') - .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') - .where('list_mute.mutedByDid', '=', viewer ?? '') - .whereRef('list_item.subjectDid', '=', ref('did_handle.did')) - .select('list_item.listUri') - .limit(1) - .as('requesterMutedByList'), - ]) - - const [profileInfos, labels] = await Promise.all([ - profileInfosQb.execute(), - this.services.label.getLabelsForSubjects(skipLabels ? [] : dids), - ]) - - const listUris: string[] = profileInfos - .map((a) => a.requesterMutedByList) - .filter((list) => !!list) - const listViews = await this.services.graph.getListViews(listUris, viewer) - - return profileInfos.reduce((acc, cur) => { - const avatar = cur.avatarCid - ? this.imgUriBuilder.getCommonSignedUri('avatar', cur.avatarCid) - : undefined - const mutedByList = - cur.requesterMutedByList && listViews[cur.requesterMutedByList] - ? this.services.graph.formatListViewBasic( - listViews[cur.requesterMutedByList], - ) - : undefined - const actorLabels = labels[cur.did] ?? [] - const selfLabels = getSelfLabels({ - uri: cur.profileUri, - cid: cur.profileCid, - record: cur.profileBytes && cborToLexRecord(cur.profileBytes), - }) - const profile = { - did: cur.did, - handle: cur.handle, - displayName: truncateUtf8(cur?.displayName, 64) || undefined, - description: truncateUtf8(cur?.description, 256) || undefined, - avatar, - indexedAt: cur?.indexedAt || undefined, - viewer: { - muted: !!cur?.requesterMuted || !!cur?.requesterMutedByList, - mutedByList, - blockedBy: !!cur.requesterBlockedBy, - blocking: cur.requesterBlocking || undefined, - following: cur?.requesterFollowing || undefined, - followedBy: cur?.requesterFollowedBy || undefined, - }, - labels: skipLabels ? undefined : [...actorLabels, ...selfLabels], - } - acc[cur.did] = profile - return acc - }, {} as Record) - } - - async hydrateProfiles( - results: ActorResult[], - viewer: string | null, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise { - const profiles = await this.profiles(results, viewer, opts) - return mapDefined(results, (result) => profiles[result.did]) - } - - async profile( - result: ActorResult, - viewer: string | null, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise { - const profiles = await this.profiles([result], viewer, opts) - return profiles[result.did] ?? null - } - - // @NOTE keep in sync with feedService.getActorViews() - async profilesBasic( - results: ActorResult[], - viewer: string, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise> { - if (results.length === 0) return {} - const profiles = await this.profiles(results, viewer, opts) - return Object.values(profiles).reduce((acc, cur) => { - const profile = { - did: cur.did, - handle: cur.handle, - displayName: truncateUtf8(cur.displayName, 64) || undefined, - avatar: cur.avatar, - viewer: cur.viewer, - labels: cur.labels, - } - acc[cur.did] = profile - return acc - }, {} as Record) - } - - async hydrateProfilesBasic( - results: ActorResult[], - viewer: string, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise { - const profiles = await this.profilesBasic(results, viewer, opts) - return mapDefined(results, (result) => profiles[result.did]) - } - - async profileBasic( - result: ActorResult, - viewer: string, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise { - const profiles = await this.profilesBasic([result], viewer, opts) - return profiles[result.did] ?? null - } -} - -type ActorResult = DidHandle - -function truncateUtf8(str: string | null | undefined, length: number) { - if (!str) return str - const encoder = new TextEncoder() - const utf8 = encoder.encode(str) - if (utf8.length > length) { - const decoder = new TextDecoder('utf-8', { fatal: false }) - const truncated = utf8.slice(0, length) - return decoder.decode(truncated).replace(/\uFFFD$/, '') - } - return str -} diff --git a/packages/pds/src/app-view/services/feed/index.ts b/packages/pds/src/app-view/services/feed/index.ts deleted file mode 100644 index a7f153b7a76..00000000000 --- a/packages/pds/src/app-view/services/feed/index.ts +++ /dev/null @@ -1,685 +0,0 @@ -import { sql } from 'kysely' -import { AtUri } from '@atproto/syntax' -import { dedupeStrs } from '@atproto/common' -import { cborToLexRecord } from '@atproto/repo' -import Database from '../../../db' -import { countAll, notSoftDeletedClause, valuesList } from '../../../db/util' -import { ImageUriBuilder } from '../../../image/uri' -import { ids } from '../../../lexicon/lexicons' -import { - Record as PostRecord, - isRecord as isPostRecord, -} from '../../../lexicon/types/app/bsky/feed/post' -import { isMain as isEmbedImages } from '../../../lexicon/types/app/bsky/embed/images' -import { isMain as isEmbedExternal } from '../../../lexicon/types/app/bsky/embed/external' -import { - isMain as isEmbedRecord, - isViewRecord, -} from '../../../lexicon/types/app/bsky/embed/record' -import { isMain as isEmbedRecordWithMedia } from '../../../lexicon/types/app/bsky/embed/recordWithMedia' -import { FeedViewPost } from '../../../lexicon/types/app/bsky/feed/defs' -import { - ActorInfoMap, - PostInfoMap, - FeedItemType, - FeedRow, - FeedGenInfoMap, - PostViews, - PostEmbedViews, - RecordEmbedViewRecordMap, - PostBlocksMap, - RecordEmbedViewRecord, - FeedHydrationOptions, - kSelfLabels, -} from './types' -import { LabelService, Labels, getSelfLabels } from '../label' -import { ActorService } from '../actor' -import { GraphService } from '../graph' -import { FeedViews } from './views' -import { LabelCache } from '../../../label-cache' - -export * from './types' - -export class FeedService { - constructor( - public db: Database, - public imgUriBuilder: ImageUriBuilder, - public labelCache: LabelCache, - ) {} - - static creator(imgUriBuilder: ImageUriBuilder, labelCache: LabelCache) { - return (db: Database) => new FeedService(db, imgUriBuilder, labelCache) - } - - views = new FeedViews(this.db, this.imgUriBuilder) - services = { - label: LabelService.creator(this.labelCache)(this.db), - actor: ActorService.creator(this.imgUriBuilder, this.labelCache)(this.db), - graph: GraphService.creator(this.imgUriBuilder)(this.db), - } - - selectPostQb() { - return this.db.db - .selectFrom('post') - .select([ - sql`${'post'}`.as('type'), - 'post.uri as uri', - 'post.cid as cid', - 'post.uri as postUri', - 'post.creator as originatorDid', - 'post.creator as postAuthorDid', - 'post.replyParent as replyParent', - 'post.replyRoot as replyRoot', - 'post.indexedAt as sortAt', - ]) - } - - selectFeedItemQb() { - return this.db.db - .selectFrom('feed_item') - .innerJoin('post', 'post.uri', 'feed_item.postUri') - .selectAll('feed_item') - .select([ - 'post.replyRoot', - 'post.replyParent', - 'post.creator as postAuthorDid', - ]) - } - - selectFeedGeneratorQb(requester: string | null) { - const { ref } = this.db.db.dynamic - return this.db.db - .selectFrom('feed_generator') - .innerJoin('did_handle', 'did_handle.did', 'feed_generator.creator') - .innerJoin( - 'repo_root as creator_repo', - 'creator_repo.did', - 'feed_generator.creator', - ) - .innerJoin('record', 'record.uri', 'feed_generator.uri') - .selectAll() - .where(notSoftDeletedClause(ref('creator_repo'))) - .where(notSoftDeletedClause(ref('record'))) - .select((qb) => - qb - .selectFrom('like') - .whereRef('like.subject', '=', 'feed_generator.uri') - .select(countAll.as('count')) - .as('likeCount'), - ) - .select((qb) => - qb - .selectFrom('like') - .where('like.creator', '=', requester ?? '') - .whereRef('like.subject', '=', 'feed_generator.uri') - .select('uri') - .as('viewerLike'), - ) - } - - // @NOTE keep in sync with actorService.views.profile() - async getActorInfos( - dids: string[], - requester: string | null, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, // @NOTE used by hydrateFeed() to batch label hydration - ): Promise { - if (dids.length < 1) return {} - const { ref } = this.db.db.dynamic - const { skipLabels = false, includeSoftDeleted = false } = opts ?? {} - const [actors, labels] = await Promise.all([ - this.db.db - .selectFrom('did_handle') - .where('did_handle.did', 'in', dids) - .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') - .leftJoin('profile', 'profile.creator', 'did_handle.did') - .leftJoin('ipld_block', (join) => - join - .onRef('ipld_block.cid', '=', 'profile.cid') - .onRef('ipld_block.creator', '=', 'profile.creator'), - ) - .selectAll('did_handle') - .if(!includeSoftDeleted, (qb) => - qb.where(notSoftDeletedClause(ref('repo_root'))), - ) - .select([ - 'profile.uri as profileUri', - 'profile.cid as profileCid', - 'profile.displayName as displayName', - 'profile.description as description', - 'profile.avatarCid as avatarCid', - 'profile.indexedAt as indexedAt', - 'ipld_block.content as profileBytes', - this.db.db - .selectFrom('follow') - .where('creator', '=', requester ?? '') - .whereRef('subjectDid', '=', ref('did_handle.did')) - .select('uri') - .as('requesterFollowing'), - this.db.db - .selectFrom('follow') - .whereRef('creator', '=', ref('did_handle.did')) - .where('subjectDid', '=', requester ?? '') - .select('uri') - .as('requesterFollowedBy'), - this.db.db - .selectFrom('actor_block') - .where('creator', '=', requester ?? '') - .whereRef('subjectDid', '=', ref('did_handle.did')) - .select('uri') - .as('requesterBlocking'), - this.db.db - .selectFrom('actor_block') - .whereRef('creator', '=', ref('did_handle.did')) - .where('subjectDid', '=', requester ?? '') - .select('uri') - .as('requesterBlockedBy'), - this.db.db - .selectFrom('mute') - .whereRef('did', '=', ref('did_handle.did')) - .where('mutedByDid', '=', requester ?? '') - .select('did') - .as('requesterMuted'), - this.db.db - .selectFrom('list_item') - .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') - .where('list_mute.mutedByDid', '=', requester ?? '') - .whereRef('list_item.subjectDid', '=', ref('did_handle.did')) - .select('list_item.listUri') - .limit(1) - .as('requesterMutedByList'), - ]) - .execute(), - this.services.label.getLabelsForSubjects(skipLabels ? [] : dids), - ]) - const listUris: string[] = actors - .map((a) => a.requesterMutedByList) - .filter((list) => !!list) - const listViews = await this.services.graph.getListViews( - listUris, - requester, - ) - return actors.reduce((acc, cur) => { - const avatar = cur.avatarCid - ? this.imgUriBuilder.getCommonSignedUri('avatar', cur.avatarCid) - : undefined - const mutedByList = - cur.requesterMutedByList && listViews[cur.requesterMutedByList] - ? this.services.graph.formatListViewBasic( - listViews[cur.requesterMutedByList], - ) - : undefined - const actorLabels = labels[cur.did] ?? [] - const selfLabels = getSelfLabels({ - uri: cur.profileUri, - cid: cur.profileCid, - record: cur.profileBytes && cborToLexRecord(cur.profileBytes), - }) - return { - ...acc, - [cur.did]: { - did: cur.did, - handle: cur.handle, - displayName: truncateUtf8(cur.displayName, 64) || undefined, - avatar, - viewer: { - muted: !!cur?.requesterMuted || !!cur?.requesterMutedByList, - mutedByList, - blockedBy: !!cur?.requesterBlockedBy, - blocking: cur?.requesterBlocking || undefined, - following: cur?.requesterFollowing || undefined, - followedBy: cur?.requesterFollowedBy || undefined, - }, - labels: skipLabels ? undefined : [...actorLabels, ...selfLabels], - [kSelfLabels]: selfLabels, - }, - } - }, {} as ActorInfoMap) - } - - async getPostInfos( - postUris: string[], - requester: string | null, - options?: Pick, - ): Promise { - if (postUris.length < 1) return {} - const db = this.db.db - const { ref } = db.dynamic - let postsQb = db - .selectFrom('post') - .where('post.uri', 'in', postUris) - .leftJoin('post_agg', 'post_agg.uri', 'post.uri') - .innerJoin('ipld_block', (join) => - join - .onRef('ipld_block.cid', '=', 'post.cid') - .onRef('ipld_block.creator', '=', 'post.creator'), - ) - .innerJoin('repo_root', 'repo_root.did', 'post.creator') - .innerJoin('record', 'record.uri', 'post.uri') - - if (!options?.includeSoftDeleted) { - postsQb = postsQb - .where(notSoftDeletedClause(ref('repo_root'))) // Ensures post reply parent/roots get omitted from views when taken down - .where(notSoftDeletedClause(ref('record'))) - } - - const posts = await postsQb - .select([ - 'post.uri as uri', - 'post.cid as cid', - 'post.creator as creator', - 'post.indexedAt as indexedAt', - 'ipld_block.content as recordBytes', - 'post_agg.likeCount as likeCount', - 'post_agg.repostCount as repostCount', - 'post_agg.replyCount as replyCount', - 'record.takedownId as takedownId', - db - .selectFrom('repost') - .where('creator', '=', requester ?? '') - .whereRef('subject', '=', ref('post.uri')) - .select('uri') - .as('requesterRepost'), - db - .selectFrom('like') - .where('creator', '=', requester ?? '') - .whereRef('subject', '=', ref('post.uri')) - .select('uri') - .as('requesterLike'), - ]) - .execute() - return posts.reduce((acc, cur) => { - const { recordBytes, ...post } = cur - return Object.assign(acc, { - [post.uri]: { - ...post, - record: cborToLexRecord(recordBytes), - }, - }) - }, {} as PostInfoMap) - } - - async getFeedGeneratorInfos( - generatorUris: string[], - requester: string | null, - ) { - if (generatorUris.length < 1) return {} - const feedGens = await this.selectFeedGeneratorQb(requester) - .where('feed_generator.uri', 'in', generatorUris) - .execute() - return feedGens.reduce( - (acc, cur) => ({ - ...acc, - [cur.uri]: cur, - }), - {} as FeedGenInfoMap, - ) - } - - async getPostViews( - postUris: string[], - requester: string, - precomputed?: { - actors?: ActorInfoMap - posts?: PostInfoMap - embeds?: PostEmbedViews - blocks?: PostBlocksMap - labels?: Labels - }, - ): Promise { - const uris = dedupeStrs(postUris) - const dids = dedupeStrs(postUris.map((uri) => new AtUri(uri).hostname)) - - const [actors, posts, labels] = await Promise.all([ - precomputed?.actors ?? - this.getActorInfos(dids, requester, { skipLabels: true }), - precomputed?.posts ?? this.getPostInfos(uris, requester), - precomputed?.labels ?? - this.services.label.getLabelsForSubjects([...uris, ...dids]), - ]) - const blocks = precomputed?.blocks ?? (await this.blocksForPosts(posts)) - const embeds = - precomputed?.embeds ?? - (await this.embedsForPosts(posts, blocks, requester)) - - return uris.reduce((acc, cur) => { - const view = this.views.formatPostView(cur, actors, posts, embeds, labels) - if (view) { - acc[cur] = view - } - return acc - }, {} as PostViews) - } - - async hydrateFeed( - items: FeedRow[], - requester: string | null, - options?: FeedHydrationOptions, - ): Promise { - const actorDids = new Set() - const postUris = new Set() - for (const item of items) { - actorDids.add(item.postAuthorDid) - postUris.add(item.postUri) - if (item.postAuthorDid !== item.originatorDid) { - actorDids.add(item.originatorDid) - } - if (item.replyParent) { - postUris.add(item.replyParent) - actorDids.add(new AtUri(item.replyParent).hostname) - } - if (item.replyRoot) { - postUris.add(item.replyRoot) - actorDids.add(new AtUri(item.replyRoot).hostname) - } - } - - const [actors, posts, labels] = await Promise.all([ - this.getActorInfos(Array.from(actorDids), requester, { - skipLabels: true, - includeSoftDeleted: options?.includeSoftDeleted, - }), - this.getPostInfos(Array.from(postUris), requester, options), - this.services.label.getLabelsForSubjects([...postUris, ...actorDids]), - ]) - const blocks = await this.blocksForPosts(posts) - const embeds = await this.embedsForPosts(posts, blocks, requester) - - return this.views.formatFeed( - items, - actors, - posts, - embeds, - labels, - blocks, - options, - ) - } - - // applies blocks for visibility to third-parties (i.e. based on post content) - async blocksForPosts(posts: PostInfoMap): Promise { - const relationships: RelationshipPair[] = [] - const byPost: Record = {} - const didFromUri = (uri) => new AtUri(uri).host - for (const post of Object.values(posts)) { - // skip posts that we can't process or appear to already have been processed - if (!isPostRecord(post.record)) continue - if (byPost[post.uri]) continue - byPost[post.uri] = {} - // 3p block for replies - const parentUri = post.record.reply?.parent.uri - const parentDid = parentUri ? didFromUri(parentUri) : null - // 3p block for record embeds - const embedUris = nestedRecordUris([post.record]) - // gather actor relationships among posts - if (parentDid) { - const pair: RelationshipPair = [post.creator, parentDid] - relationships.push(pair) - byPost[post.uri].reply = pair - } - for (const embedUri of embedUris) { - const pair: RelationshipPair = [post.creator, didFromUri(embedUri)] - relationships.push(pair) - byPost[post.uri].embed = pair - } - } - // compute block state from all actor relationships among posts - const blockSet = await this.getBlockSet(relationships) - if (blockSet.empty()) return {} - const result: PostBlocksMap = {} - Object.entries(byPost).forEach(([uri, block]) => { - if (block.embed && blockSet.has(block.embed)) { - result[uri] ??= {} - result[uri].embed = true - } - if (block.reply && blockSet.has(block.reply)) { - result[uri] ??= {} - result[uri].reply = true - } - }) - return result - } - - private async getBlockSet(relationships: RelationshipPair[]) { - const { ref } = this.db.db.dynamic - const blockSet = new RelationshipSet() - if (!relationships.length) return blockSet - const relationshipSet = new RelationshipSet() - relationships.forEach((pair) => relationshipSet.add(pair)) - // compute actual block set from all actor relationships - const blockRows = await this.db.db - .selectFrom('actor_block') - .select(['creator', 'subjectDid']) // index-only columns - .where( - sql`(${ref('creator')}, ${ref('subjectDid')})`, - 'in', - valuesList( - relationshipSet.listAllPairs().map(([a, b]) => sql`${a}, ${b}`), - ), - ) - .execute() - blockRows.forEach((r) => blockSet.add([r.creator, r.subjectDid])) - return blockSet - } - - async embedsForPosts( - postInfos: PostInfoMap, - blocks: PostBlocksMap, - requester: string | null, - depth = 0, - ) { - const postMap = postRecordsFromInfos(postInfos) - const posts = Object.values(postMap) - if (posts.length < 1) { - return {} - } - const recordEmbedViews = - depth > 1 ? {} : await this.nestedRecordViews(posts, requester, depth) - - const postEmbedViews: PostEmbedViews = {} - for (const [uri, post] of Object.entries(postMap)) { - if (!post.embed) continue - if (isEmbedImages(post.embed)) { - postEmbedViews[uri] = this.views.imagesEmbedView(post.embed) - } else if (isEmbedExternal(post.embed)) { - postEmbedViews[uri] = this.views.externalEmbedView(post.embed) - } else if (isEmbedRecord(post.embed)) { - if (!recordEmbedViews[post.embed.record.uri]) continue - postEmbedViews[uri] = { - $type: 'app.bsky.embed.record#view', - record: applyEmbedBlock( - uri, - blocks, - recordEmbedViews[post.embed.record.uri], - ), - } - } else if (isEmbedRecordWithMedia(post.embed)) { - const embedRecordView = recordEmbedViews[post.embed.record.record.uri] - if (!embedRecordView) continue - const formatted = this.views.getRecordWithMediaEmbedView( - post.embed, - applyEmbedBlock(uri, blocks, embedRecordView), - ) - if (formatted) { - postEmbedViews[uri] = formatted - } - } - } - return postEmbedViews - } - - async nestedRecordViews( - posts: PostRecord[], - requester: string | null, - depth: number, - ): Promise { - const nestedUris = nestedRecordUris(posts) - if (nestedUris.length < 1) return {} - const nestedPostUris: string[] = [] - const nestedFeedGenUris: string[] = [] - const nestedListUris: string[] = [] - const nestedDidsSet = new Set() - for (const uri of nestedUris) { - const parsed = new AtUri(uri) - nestedDidsSet.add(parsed.hostname) - if (parsed.collection === ids.AppBskyFeedPost) { - nestedPostUris.push(uri) - } else if (parsed.collection === ids.AppBskyFeedGenerator) { - nestedFeedGenUris.push(uri) - } else if (parsed.collection === ids.AppBskyGraphList) { - nestedListUris.push(uri) - } - } - const nestedDids = [...nestedDidsSet] - const [postInfos, actorInfos, labelViews, feedGenInfos, listViews] = - await Promise.all([ - this.getPostInfos(nestedPostUris, requester), - this.getActorInfos(nestedDids, requester, { skipLabels: true }), - this.services.label.getLabelsForSubjects([ - ...nestedPostUris, - ...nestedDids, - ]), - this.getFeedGeneratorInfos(nestedFeedGenUris, requester), - this.services.graph.getListViews(nestedListUris, requester), - ]) - const deepBlocks = await this.blocksForPosts(postInfos) - const deepEmbedViews = await this.embedsForPosts( - postInfos, - deepBlocks, - requester, - depth + 1, - ) - const recordEmbedViews: RecordEmbedViewRecordMap = {} - for (const uri of nestedUris) { - const collection = new AtUri(uri).collection - if (collection === ids.AppBskyFeedGenerator && feedGenInfos[uri]) { - recordEmbedViews[uri] = { - $type: 'app.bsky.feed.defs#generatorView', - ...this.views.formatFeedGeneratorView( - feedGenInfos[uri], - actorInfos, - labelViews, - ), - } - } else if (collection === ids.AppBskyGraphList && listViews[uri]) { - recordEmbedViews[uri] = { - $type: 'app.bsky.graph.defs#listView', - ...this.services.graph.formatListView(listViews[uri], actorInfos), - } - } else if (collection === ids.AppBskyFeedPost && postInfos[uri]) { - const formatted = this.views.formatPostView( - uri, - actorInfos, - postInfos, - deepEmbedViews, - labelViews, - ) - recordEmbedViews[uri] = this.views.getRecordEmbedView( - uri, - formatted, - depth > 0, - ) - } else { - recordEmbedViews[uri] = { - $type: 'app.bsky.embed.record#viewNotFound', - uri, - notFound: true, - } - } - } - return recordEmbedViews - } -} - -function truncateUtf8(str: string | null | undefined, length: number) { - if (!str) return str - const encoder = new TextEncoder() - const utf8 = encoder.encode(str) - if (utf8.length > length) { - const decoder = new TextDecoder('utf-8', { fatal: false }) - const truncated = utf8.slice(0, length) - return decoder.decode(truncated).replace(/\uFFFD$/, '') - } - return str -} - -const postRecordsFromInfos = ( - infos: PostInfoMap, -): { [uri: string]: PostRecord } => { - const records: { [uri: string]: PostRecord } = {} - for (const [uri, info] of Object.entries(infos)) { - if (isPostRecord(info.record)) { - records[uri] = info.record - } - } - return records -} - -const nestedRecordUris = (posts: PostRecord[]): string[] => { - const uris: string[] = [] - for (const post of posts) { - if (!post.embed) continue - if (isEmbedRecord(post.embed)) { - uris.push(post.embed.record.uri) - } else if (isEmbedRecordWithMedia(post.embed)) { - uris.push(post.embed.record.record.uri) - } else { - continue - } - } - return uris -} - -type PostRelationships = { reply?: RelationshipPair; embed?: RelationshipPair } - -type RelationshipPair = [didA: string, didB: string] - -class RelationshipSet { - index = new Map>() - add([didA, didB]: RelationshipPair) { - const didAIdx = this.index.get(didA) ?? new Set() - const didBIdx = this.index.get(didB) ?? new Set() - if (!this.index.has(didA)) this.index.set(didA, didAIdx) - if (!this.index.has(didB)) this.index.set(didB, didBIdx) - didAIdx.add(didB) - didBIdx.add(didA) - } - has([didA, didB]: RelationshipPair) { - return !!this.index.get(didA)?.has(didB) - } - listAllPairs() { - const pairs: RelationshipPair[] = [] - for (const [didA, didBIdx] of this.index.entries()) { - for (const didB of didBIdx) { - pairs.push([didA, didB]) - } - } - return pairs - } - empty() { - return this.index.size === 0 - } -} - -function applyEmbedBlock( - uri: string, - blocks: PostBlocksMap, - view: RecordEmbedViewRecord, -): RecordEmbedViewRecord { - if (isViewRecord(view) && blocks[uri]?.embed) { - return { - $type: 'app.bsky.embed.record#viewBlocked', - uri: view.uri, - blocked: true, - author: { - did: view.author.did, - viewer: view.author.viewer - ? { - blockedBy: view.author.viewer?.blockedBy, - blocking: view.author.viewer?.blocking, - } - : undefined, - }, - } - } - return view -} diff --git a/packages/pds/src/app-view/services/feed/types.ts b/packages/pds/src/app-view/services/feed/types.ts deleted file mode 100644 index 998caa6e32c..00000000000 --- a/packages/pds/src/app-view/services/feed/types.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { RepoRecord } from '@atproto/lexicon' -import { View as ImagesEmbedView } from '../../../lexicon/types/app/bsky/embed/images' -import { View as ExternalEmbedView } from '../../../lexicon/types/app/bsky/embed/external' -import { - ViewBlocked, - ViewNotFound, - ViewRecord, - View as RecordEmbedView, -} from '../../../lexicon/types/app/bsky/embed/record' -import { View as RecordWithMediaEmbedView } from '../../../lexicon/types/app/bsky/embed/recordWithMedia' -import { - BlockedPost, - GeneratorView, - NotFoundPost, - PostView, -} from '../../../lexicon/types/app/bsky/feed/defs' -import { Label } from '../../../lexicon/types/com/atproto/label/defs' -import { FeedGenerator } from '../../db/tables/feed-generator' -import { ListView } from '../../../lexicon/types/app/bsky/graph/defs' - -export type PostEmbedViews = { - [uri: string]: PostEmbedView -} - -export type PostEmbedView = - | ImagesEmbedView - | ExternalEmbedView - | RecordEmbedView - | RecordWithMediaEmbedView - -export type PostViews = { [uri: string]: PostView } - -export type PostInfo = { - uri: string - cid: string - creator: string - record: RepoRecord - indexedAt: string - likeCount: number | null - repostCount: number | null - replyCount: number | null - requesterRepost: string | null - requesterLike: string | null - takedownId: number | null -} - -export type PostInfoMap = { [uri: string]: PostInfo } - -export type PostBlocksMap = { - [uri: string]: { reply?: boolean; embed?: boolean } -} - -export const kSelfLabels = Symbol('selfLabels') - -export type ActorInfo = { - did: string - handle: string - displayName?: string - avatar?: string - viewer?: { - muted?: boolean - blockedBy?: boolean - blocking?: string - following?: string - followedBy?: string - } - labels?: Label[] - // allows threading self-labels through if they are going to be applied later, i.e. when using skipLabels option. - [kSelfLabels]?: Label[] -} -export type ActorInfoMap = { [did: string]: ActorInfo } - -export type FeedGenInfo = FeedGenerator & { - likeCount: number - viewerLike: string | null -} - -export type FeedGenInfoMap = { [uri: string]: FeedGenInfo } - -export type FeedItemType = 'post' | 'repost' - -export type FeedRow = { - type: FeedItemType - uri: string - cid: string - postUri: string - postAuthorDid: string - originatorDid: string - replyParent: string | null - replyRoot: string | null - sortAt: string -} -export type FeedHydrationOptions = { - includeSoftDeleted?: boolean - usePostViewUnion?: boolean -} - -export type MaybePostView = PostView | NotFoundPost | BlockedPost - -export type RecordEmbedViewRecord = - | ViewRecord - | ViewNotFound - | ViewBlocked - | GeneratorView - | ListView - -export type RecordEmbedViewRecordMap = { [uri: string]: RecordEmbedViewRecord } diff --git a/packages/pds/src/app-view/services/feed/util.ts b/packages/pds/src/app-view/services/feed/util.ts deleted file mode 100644 index cedc82df0e9..00000000000 --- a/packages/pds/src/app-view/services/feed/util.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { sql } from 'kysely' -import DatabaseSchema from '../../../db/database-schema' - -export const getDescendentsQb = ( - db: DatabaseSchema, - opts: { - uri: string - depth: number // required, protects against cycles - }, -) => { - const { uri, depth } = opts - const query = db.withRecursive('descendent(uri, depth)', (cte) => { - return cte - .selectFrom('post') - .select(['post.uri as uri', sql`1`.as('depth')]) - .where(sql`1`, '<=', depth) - .where('replyParent', '=', uri) - .unionAll( - cte - .selectFrom('post') - .innerJoin('descendent', 'descendent.uri', 'post.replyParent') - .where('descendent.depth', '<', depth) - .select([ - 'post.uri as uri', - sql`descendent.depth + 1`.as('depth'), - ]), - ) - }) - return query -} - -export const getAncestorsAndSelfQb = ( - db: DatabaseSchema, - opts: { - uri: string - parentHeight: number // required, protects against cycles - }, -) => { - const { uri, parentHeight } = opts - const query = db.withRecursive( - 'ancestor(uri, ancestorUri, height)', - (cte) => { - return cte - .selectFrom('post') - .select([ - 'post.uri as uri', - 'post.replyParent as ancestorUri', - sql`0`.as('height'), - ]) - .where('uri', '=', uri) - .unionAll( - cte - .selectFrom('post') - .innerJoin('ancestor', 'ancestor.ancestorUri', 'post.uri') - .where('ancestor.height', '<', parentHeight) - .select([ - 'post.uri as uri', - 'post.replyParent as ancestorUri', - sql`ancestor.height + 1`.as('height'), - ]), - ) - }, - ) - return query -} diff --git a/packages/pds/src/app-view/services/feed/views.ts b/packages/pds/src/app-view/services/feed/views.ts deleted file mode 100644 index cfb4d158e2b..00000000000 --- a/packages/pds/src/app-view/services/feed/views.ts +++ /dev/null @@ -1,344 +0,0 @@ -import Database from '../../../db' -import { - FeedViewPost, - GeneratorView, - PostView, -} from '../../../lexicon/types/app/bsky/feed/defs' -import { - Main as EmbedImages, - isMain as isEmbedImages, - View as EmbedImagesView, -} from '../../../lexicon/types/app/bsky/embed/images' -import { - Main as EmbedExternal, - isMain as isEmbedExternal, - View as EmbedExternalView, -} from '../../../lexicon/types/app/bsky/embed/external' -import { Main as EmbedRecordWithMedia } from '../../../lexicon/types/app/bsky/embed/recordWithMedia' -import { - ViewBlocked, - ViewNotFound, - ViewRecord, -} from '../../../lexicon/types/app/bsky/embed/record' -import { - ActorInfoMap, - PostEmbedViews, - FeedGenInfo, - FeedRow, - MaybePostView, - PostInfoMap, - RecordEmbedViewRecord, - PostBlocksMap, - FeedHydrationOptions, - kSelfLabels, -} from './types' -import { Labels, getSelfLabels } from '../label' -import { ImageUriBuilder } from '../../../image/uri' - -export * from './types' - -export class FeedViews { - constructor(public db: Database, public imgUriBuilder: ImageUriBuilder) {} - - static creator(imgUriBuilder: ImageUriBuilder) { - return (db: Database) => new FeedViews(db, imgUriBuilder) - } - - formatFeedGeneratorView( - info: FeedGenInfo, - profiles: ActorInfoMap, - labels?: Labels, - ): GeneratorView { - const profile = profiles[info.creator] - if (profile && !profile.labels) { - // If the creator labels are not hydrated yet, attempt to pull them - // from labels: e.g. compatible with embedsForPosts() batching label hydration. - const profileLabels = labels?.[info.creator] ?? [] - const profileSelfLabels = profile[kSelfLabels] ?? [] - profile.labels = [...profileLabels, ...profileSelfLabels] - } - return { - uri: info.uri, - cid: info.cid, - did: info.feedDid, - creator: profile, - displayName: info.displayName ?? undefined, - description: info.description ?? undefined, - descriptionFacets: info.descriptionFacets - ? JSON.parse(info.descriptionFacets) - : undefined, - avatar: info.avatarCid - ? this.imgUriBuilder.getCommonSignedUri('avatar', info.avatarCid) - : undefined, - likeCount: info.likeCount, - viewer: { - like: info.viewerLike ?? undefined, - }, - indexedAt: info.indexedAt, - } - } - - formatFeed( - items: FeedRow[], - actors: ActorInfoMap, - posts: PostInfoMap, - embeds: PostEmbedViews, - labels: Labels, - blocks: PostBlocksMap, - opts?: FeedHydrationOptions, - ): FeedViewPost[] { - const feed: FeedViewPost[] = [] - for (const item of items) { - const post = this.formatPostView( - item.postUri, - actors, - posts, - embeds, - labels, - opts, - ) - // skip over not found & blocked posts - if (!post || blocks[post.uri]?.reply) { - continue - } - const feedPost = { post } - if (item.type === 'repost') { - const originator = actors[item.originatorDid] - // skip over reposts where we don't have reposter profile - if (!originator) { - continue - } else { - const originatorLabels = labels[item.originatorDid] ?? [] - const originatorSelfLabels = originator[kSelfLabels] ?? [] - feedPost['reason'] = { - $type: 'app.bsky.feed.defs#reasonRepost', - by: { - ...originator, - labels: [...originatorLabels, ...originatorSelfLabels], - }, - indexedAt: item.sortAt, - } - } - } - if (item.replyParent && item.replyRoot) { - const replyParent = this.formatMaybePostView( - item.replyParent, - actors, - posts, - embeds, - labels, - blocks, - opts, - ) - const replyRoot = this.formatMaybePostView( - item.replyRoot, - actors, - posts, - embeds, - labels, - blocks, - opts, - ) - if (replyRoot && replyParent) { - feedPost['reply'] = { - root: replyRoot, - parent: replyParent, - } - } - } - feed.push(feedPost) - } - return feed - } - - formatPostView( - uri: string, - actors: ActorInfoMap, - posts: PostInfoMap, - embeds: PostEmbedViews, - labels: Labels, - opts?: Pick, - ): PostView | undefined { - const post = posts[uri] - const author = actors[post?.creator] - if (!post || !author) return undefined - // If the author labels are not hydrated yet, attempt to pull them - // from labels: e.g. compatible with hydrateFeed() batching label hydration. - const authorLabels = labels[author.did] ?? [] - const authorSelfLabels = author[kSelfLabels] ?? [] - author.labels ??= [...authorLabels, ...authorSelfLabels] - const postLabels = labels[uri] ?? [] - const postSelfLabels = getSelfLabels({ - uri: post.uri, - cid: post.cid, - record: post.record, - }) - return { - uri: post.uri, - cid: post.cid, - author: author, - takedownId: opts?.includeSoftDeleted - ? post.takedownId ?? null - : undefined, - record: post.record, - embed: embeds[uri], - replyCount: post.replyCount ?? 0, - repostCount: post.repostCount ?? 0, - likeCount: post.likeCount ?? 0, - indexedAt: post.indexedAt, - viewer: { - repost: post.requesterRepost ?? undefined, - like: post.requesterLike ?? undefined, - }, - labels: [...postLabels, ...postSelfLabels], - } - } - - formatMaybePostView( - uri: string, - actors: ActorInfoMap, - posts: PostInfoMap, - embeds: PostEmbedViews, - labels: Labels, - blocks: PostBlocksMap, - opts?: FeedHydrationOptions, - ): MaybePostView | undefined { - const post = this.formatPostView(uri, actors, posts, embeds, labels, opts) - if (!post) { - if (!opts?.usePostViewUnion) return - return this.notFoundPost(uri) - } - if ( - post.author.viewer?.blockedBy || - post.author.viewer?.blocking || - blocks[uri]?.reply - ) { - if (!opts?.usePostViewUnion) return - return this.blockedPost(post) - } - return { - $type: 'app.bsky.feed.defs#postView', - ...post, - } - } - - blockedPost(post: PostView) { - return { - $type: 'app.bsky.feed.defs#blockedPost', - uri: post.uri, - blocked: true as const, - author: { - did: post.author.did, - viewer: post.author.viewer - ? { - blockedBy: post.author.viewer?.blockedBy, - blocking: post.author.viewer?.blocking, - } - : undefined, - }, - } - } - - notFoundPost(uri: string) { - return { - $type: 'app.bsky.feed.defs#notFoundPost', - uri: uri, - notFound: true as const, - } - } - - imagesEmbedView(embed: EmbedImages) { - const imgViews = embed.images.map((img) => ({ - thumb: this.imgUriBuilder.getCommonSignedUri( - 'feed_thumbnail', - img.image.ref, - ), - fullsize: this.imgUriBuilder.getCommonSignedUri( - 'feed_fullsize', - img.image.ref, - ), - alt: img.alt, - aspectRatio: img.aspectRatio, - })) - return { - $type: 'app.bsky.embed.images#view', - images: imgViews, - } - } - - externalEmbedView(embed: EmbedExternal) { - const { uri, title, description, thumb } = embed.external - return { - $type: 'app.bsky.embed.external#view', - external: { - uri, - title, - description, - thumb: thumb - ? this.imgUriBuilder.getCommonSignedUri('feed_thumbnail', thumb.ref) - : undefined, - }, - } - } - - getRecordEmbedView( - uri: string, - post?: PostView, - omitEmbeds = false, - ): (ViewRecord | ViewNotFound | ViewBlocked) & { $type: string } { - if (!post) { - return { - $type: 'app.bsky.embed.record#viewNotFound', - uri, - notFound: true, - } - } - if (post.author.viewer?.blocking || post.author.viewer?.blockedBy) { - return { - $type: 'app.bsky.embed.record#viewBlocked', - uri, - blocked: true, - author: { - did: post.author.did, - viewer: post.author.viewer - ? { - blockedBy: post.author.viewer?.blockedBy, - blocking: post.author.viewer?.blocking, - } - : undefined, - }, - } - } - return { - $type: 'app.bsky.embed.record#viewRecord', - uri: post.uri, - cid: post.cid, - author: post.author, - value: post.record, - labels: post.labels, - indexedAt: post.indexedAt, - embeds: omitEmbeds ? undefined : post.embed ? [post.embed] : [], - } - } - - getRecordWithMediaEmbedView( - embed: EmbedRecordWithMedia, - embedRecordView: RecordEmbedViewRecord, - ) { - let mediaEmbed: EmbedImagesView | EmbedExternalView - if (isEmbedImages(embed.media)) { - mediaEmbed = this.imagesEmbedView(embed.media) - } else if (isEmbedExternal(embed.media)) { - mediaEmbed = this.externalEmbedView(embed.media) - } else { - return - } - return { - $type: 'app.bsky.embed.recordWithMedia#view', - record: { - record: embedRecordView, - }, - media: mediaEmbed, - } - } -} diff --git a/packages/pds/src/app-view/services/graph/index.ts b/packages/pds/src/app-view/services/graph/index.ts deleted file mode 100644 index 6df2d0f10d4..00000000000 --- a/packages/pds/src/app-view/services/graph/index.ts +++ /dev/null @@ -1,174 +0,0 @@ -import Database from '../../../db' -import { DbRef } from '../../../db/util' -import { NotEmptyArray } from '@atproto/common' -import { sql } from 'kysely' -import { ImageUriBuilder } from '../../../image/uri' -import { ProfileView } from '../../../lexicon/types/app/bsky/actor/defs' -import { List } from '../../db/tables/list' - -export class GraphService { - constructor(public db: Database, public imgUriBuilder: ImageUriBuilder) {} - - static creator(imgUriBuilder: ImageUriBuilder) { - return (db: Database) => new GraphService(db, imgUriBuilder) - } - - getListsQb(requester: string | null) { - const { ref } = this.db.db.dynamic - return this.db.db - .selectFrom('list') - .innerJoin('did_handle', 'did_handle.did', 'list.creator') - .selectAll('list') - .selectAll('did_handle') - .select( - this.db.db - .selectFrom('list_mute') - .where('list_mute.mutedByDid', '=', requester ?? '') - .whereRef('list_mute.listUri', '=', ref('list.uri')) - .select('list_mute.listUri') - .as('viewerMuted'), - ) - } - - getListItemsQb() { - return this.db.db - .selectFrom('list_item') - .innerJoin('did_handle as subject', 'subject.did', 'list_item.subjectDid') - .selectAll('subject') - .select(['list_item.cid as cid', 'list_item.createdAt as createdAt']) - } - - blockQb(requester: string | null, refs: NotEmptyArray) { - const subjectRefs = sql.join(refs) - return this.db.db - .selectFrom('actor_block') - .where((outer) => - outer - .where((qb) => - qb - .where('actor_block.creator', '=', requester ?? '') - .whereRef('actor_block.subjectDid', 'in', sql`(${subjectRefs})`), - ) - .orWhere((qb) => - qb - .where('actor_block.subjectDid', '=', requester ?? '') - .whereRef('actor_block.creator', 'in', sql`(${subjectRefs})`), - ), - ) - .select(['creator', 'subjectDid']) - } - - blockRefQb(first: DbRef, second: DbRef) { - return this.db.db - .selectFrom('actor_block') - .where((outer) => - outer - .where((qb) => - qb - .whereRef('actor_block.creator', '=', first) - .whereRef('actor_block.subjectDid', '=', second), - ) - .orWhere((qb) => - qb - .whereRef('actor_block.subjectDid', '=', first) - .whereRef('actor_block.creator', '=', second), - ), - ) - .select(['creator', 'subjectDid']) - } - - async getBlocks( - requester: string, - subjectHandleOrDid: string, - ): Promise<{ blocking: boolean; blockedBy: boolean }> { - let subjectDid - if (subjectHandleOrDid.startsWith('did:')) { - subjectDid = subjectHandleOrDid - } else { - const res = await this.db.db - .selectFrom('did_handle') - .where('handle', '=', subjectHandleOrDid) - .select('did') - .executeTakeFirst() - if (!res) { - return { blocking: false, blockedBy: false } - } - subjectDid = res.did - } - - const accnts = [requester, subjectDid] - const blockRes = await this.db.db - .selectFrom('actor_block') - .where('creator', 'in', accnts) - .where('subjectDid', 'in', accnts) - .selectAll() - .execute() - - const blocking = blockRes.some( - (row) => row.creator === requester && row.subjectDid === subjectDid, - ) - const blockedBy = blockRes.some( - (row) => row.creator === subjectDid && row.subjectDid === requester, - ) - - return { - blocking, - blockedBy, - } - } - - async getListViews(listUris: string[], requester: string | null) { - if (listUris.length < 1) return {} - const lists = await this.getListsQb(requester) - .where('list.uri', 'in', listUris) - .execute() - return lists.reduce( - (acc, cur) => ({ - ...acc, - [cur.uri]: cur, - }), - {}, - ) - } - - formatListView(list: ListInfo, profiles: Record) { - return { - uri: list.uri, - cid: list.cid, - creator: profiles[list.creator], - name: list.name, - purpose: list.purpose, - description: list.description ?? undefined, - descriptionFacets: list.descriptionFacets - ? JSON.parse(list.descriptionFacets) - : undefined, - avatar: list.avatarCid - ? this.imgUriBuilder.getCommonSignedUri('avatar', list.avatarCid) - : undefined, - indexedAt: list.indexedAt, - viewer: { - muted: !!list.viewerMuted, - }, - } - } - - formatListViewBasic(list: ListInfo) { - return { - uri: list.uri, - cid: list.cid, - name: list.name, - purpose: list.purpose, - avatar: list.avatarCid - ? this.imgUriBuilder.getCommonSignedUri('avatar', list.avatarCid) - : undefined, - indexedAt: list.indexedAt, - viewer: { - muted: !!list.viewerMuted, - }, - } - } -} - -type ListInfo = List & { - viewerMuted: string | null -} diff --git a/packages/pds/src/app-view/services/indexing/plugins/post.ts b/packages/pds/src/app-view/services/indexing/plugins/post.ts index 3583f087cf1..efc1f2532c7 100644 --- a/packages/pds/src/app-view/services/indexing/plugins/post.ts +++ b/packages/pds/src/app-view/services/indexing/plugins/post.ts @@ -20,7 +20,6 @@ import RecordProcessor from '../processor' import { UserNotification } from '../../../../db/tables/user-notification' import { countAll, excluded } from '../../../../db/util' import { toSimplifiedISOSafe } from '../util' -import { getAncestorsAndSelfQb } from '../../feed/util' type Post = DatabaseSchemaType['post'] type PostEmbedImage = DatabaseSchemaType['post_embed_image'] @@ -138,14 +137,7 @@ const insertFn = async ( await db.insertInto('post_embed_record').values(recordEmbed).execute() } } - const ancestors = await getAncestorsAndSelfQb(db, { - uri: post.uri, - parentHeight: REPLY_NOTIF_DEPTH, - }) - .selectFrom('ancestor') - .selectAll() - .execute() - return { post: insertedPost, facets, embeds, ancestors } + return { post: insertedPost, facets, embeds } } const findDuplicate = async (): Promise => { diff --git a/packages/pds/src/config.ts b/packages/pds/src/config.ts index acca18b580a..816aa511873 100644 --- a/packages/pds/src/config.ts +++ b/packages/pds/src/config.ts @@ -38,11 +38,6 @@ export interface ServerConfigValues { availableUserDomains: string[] handleResolveNameservers?: string[] - imgUriSalt: string - imgUriKey: string - imgUriEndpoint?: string - blobCacheLocation?: string - rateLimitsEnabled: boolean rateLimitBypassKey?: string rateLimitBypassIps?: string[] @@ -69,10 +64,9 @@ export interface ServerConfigValues { // this is really only used in test environments dbTxLockNonce?: string - bskyAppViewEndpoint?: string + bskyAppViewEndpoint: string + bskyAppViewDid: string bskyAppViewModeration?: boolean - bskyAppViewDid?: string - bskyAppViewProxy: boolean bskyAppViewCdnUrlPattern?: string crawlersToNotify?: string[] @@ -154,14 +148,6 @@ export class ServerConfig { ? process.env.HANDLE_RESOLVE_NAMESERVERS.split(',') : [] - const imgUriSalt = - process.env.IMG_URI_SALT || '9dd04221f5755bce5f55f47464c27e1e' - const imgUriKey = - process.env.IMG_URI_KEY || - 'f23ecd142835025f42c3db2cf25dd813956c178392760256211f9d315f8ab4d8' - const imgUriEndpoint = process.env.IMG_URI_ENDPOINT - const blobCacheLocation = process.env.BLOB_CACHE_LOC - const rateLimitsEnabled = process.env.RATE_LIMITS_ENABLED === 'true' const rateLimitBypassKey = nonemptyString(process.env.RATE_LIMIT_BYPASS_KEY) const rateLimitBypassIpsStr = nonemptyString( @@ -228,12 +214,17 @@ export class ServerConfig { const bskyAppViewEndpoint = nonemptyString( process.env.BSKY_APP_VIEW_ENDPOINT, ) + if (typeof bskyAppViewEndpoint !== 'string') { + throw new Error( + 'No value provided for process.env.BSKY_APP_VIEW_ENDPOINT', + ) + } + const bskyAppViewDid = nonemptyString(process.env.BSKY_APP_VIEW_DID) + if (typeof bskyAppViewDid !== 'string') { + throw new Error('No value provided for process.env.BSKY_APP_VIEW_DID') + } const bskyAppViewModeration = process.env.BSKY_APP_VIEW_MODERATION === 'true' ? true : false - const bskyAppViewDid = nonemptyString(process.env.BSKY_APP_VIEW_DID) - const bskyAppViewProxy = - process.env.BSKY_APP_VIEW_PROXY === 'true' ? true : false - const bskyAppViewCdnUrlPattern = nonemptyString( process.env.BSKY_APP_VIEW_CDN_URL_PATTERN, ) @@ -270,10 +261,6 @@ export class ServerConfig { databaseLocation, availableUserDomains, handleResolveNameservers, - imgUriSalt, - imgUriKey, - imgUriEndpoint, - blobCacheLocation, rateLimitsEnabled, rateLimitBypassKey, rateLimitBypassIps, @@ -294,9 +281,8 @@ export class ServerConfig { sequencerLeaderEnabled, dbTxLockNonce, bskyAppViewEndpoint, - bskyAppViewModeration, bskyAppViewDid, - bskyAppViewProxy, + bskyAppViewModeration, bskyAppViewCdnUrlPattern, crawlersToNotify, ...overrides, @@ -441,22 +427,6 @@ export class ServerConfig { return this.cfg.handleResolveNameservers } - get imgUriSalt() { - return this.cfg.imgUriSalt - } - - get imgUriKey() { - return this.cfg.imgUriKey - } - - get imgUriEndpoint() { - return this.cfg.imgUriEndpoint - } - - get blobCacheLocation() { - return this.cfg.blobCacheLocation - } - get rateLimitsEnabled() { return this.cfg.rateLimitsEnabled } @@ -537,16 +507,12 @@ export class ServerConfig { return this.cfg.bskyAppViewEndpoint } - get bskyAppViewModeration() { - return this.cfg.bskyAppViewModeration - } - get bskyAppViewDid() { return this.cfg.bskyAppViewDid } - get bskyAppViewProxy() { - return this.cfg.bskyAppViewProxy + get bskyAppViewModeration() { + return this.cfg.bskyAppViewModeration } get bskyAppViewCdnUrlPattern() { diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index 1f414845ea1..ea0990955e2 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -1,4 +1,3 @@ -import express from 'express' import { Redis } from 'ioredis' import * as plc from '@did-plc/lib' import * as crypto from '@atproto/crypto' @@ -11,14 +10,12 @@ import * as auth from './auth' import { ServerMailer } from './mailer' import { ModerationMailer } from './mailer/moderation' import { BlobStore } from '@atproto/repo' -import { ImageUriBuilder } from './image/uri' import { Services } from './services' import { MessageDispatcher } from './event-stream/message-queue' import { Sequencer, SequencerLeader } from './sequencer' import { Labeler } from './labeler' import { BackgroundQueue } from './event-stream/background-queue' import DidSqlCache from './did-cache' -import { MountedAlgos } from './feed-gen/types' import { Crawlers } from './crawlers' import { LabelCache } from './label-cache' import { RuntimeFlags } from './runtime-flags' @@ -34,7 +31,6 @@ export class AppContext { idResolver: IdResolver didCache: DidSqlCache auth: auth.ServerAuth - imgUriBuilder: ImageUriBuilder cfg: ServerConfig mailer: ServerMailer moderationMailer: ModerationMailer @@ -46,9 +42,8 @@ export class AppContext { labelCache: LabelCache runtimeFlags: RuntimeFlags backgroundQueue: BackgroundQueue - appviewAgent?: AtpAgent + appviewAgent: AtpAgent crawlers: Crawlers - algos: MountedAlgos }, ) {} @@ -104,10 +99,6 @@ export class AppContext { return auth.optionalAccessOrRoleVerifier(this.auth) } - get imgUriBuilder(): ImageUriBuilder { - return this.opts.imgUriBuilder - } - get cfg(): ServerConfig { return this.opts.cfg } @@ -168,8 +159,8 @@ export class AppContext { return this.opts.didCache } - get algos(): MountedAlgos { - return this.opts.algos + get appviewAgent(): AtpAgent { + return this.opts.appviewAgent } async serviceAuthHeaders(did: string, audience?: string) { @@ -184,27 +175,6 @@ export class AppContext { }) } - get appviewAgent(): AtpAgent { - if (!this.opts.appviewAgent) { - throw new Error('Could not find bsky appview endpoint') - } - return this.opts.appviewAgent - } - - canProxyRead(): boolean { - if (!this.cfg.bskyAppViewProxy || !this.cfg.bskyAppViewEndpoint) { - return false - } - return true - } - - canProxyFeedConstruction(req: express.Request): boolean { - return ( - this.cfg.bskyAppViewEndpoint !== undefined && - req.get('x-appview-proxy') !== undefined - ) - } - shouldProxyModeration(): boolean { return ( this.cfg.bskyAppViewEndpoint !== undefined && diff --git a/packages/pds/src/feed-gen/best-of-follows.ts b/packages/pds/src/feed-gen/best-of-follows.ts deleted file mode 100644 index a88e24fdb8f..00000000000 --- a/packages/pds/src/feed-gen/best-of-follows.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { QueryParams as SkeletonParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton' -import { AlgoHandler, AlgoResponse } from './types' -import { GenericKeyset, paginate } from '../db/pagination' -import AppContext from '../context' - -const handler: AlgoHandler = async ( - ctx: AppContext, - params: SkeletonParams, - requester: string, -): Promise => { - if (ctx.db.dialect === 'sqlite') { - throw new Error('best-of-follows algo not available in sqlite') - } - - const { limit, cursor } = params - const accountService = ctx.services.account(ctx.db) - const feedService = ctx.services.appView.feed(ctx.db) - const graphService = ctx.services.appView.graph(ctx.db) - - const { ref } = ctx.db.db.dynamic - - // candidates are ranked within a materialized view by like count, depreciated over time. - - let builder = feedService - .selectPostQb() - .innerJoin('algo_whats_hot_view as candidate', 'candidate.uri', 'post.uri') - .where((qb) => - qb - .where('post.creator', '=', requester) - .orWhereExists((inner) => - inner - .selectFrom('follow') - .where('follow.creator', '=', requester) - .whereRef('follow.subjectDid', '=', 'post.creator'), - ), - ) - .where((qb) => - accountService.whereNotMuted(qb, requester, [ref('post.creator')]), - ) - .whereNotExists(graphService.blockQb(requester, [ref('post.creator')])) - .select('candidate.score') - .select('candidate.cid') - - const keyset = new ScoreKeyset(ref('candidate.score'), ref('candidate.cid')) - builder = paginate(builder, { limit, cursor, keyset }) - - const feedItems = await builder.execute() - - return { - feedItems, - cursor: keyset.packFromResult(feedItems), - } -} - -export default handler - -type Result = { score: number; cid: string } -type LabeledResult = { primary: number; secondary: string } -export class ScoreKeyset extends GenericKeyset { - labelResult(result: Result) { - return { - primary: result.score, - secondary: result.cid, - } - } - labeledResultToCursor(labeled: LabeledResult) { - return { - primary: Math.round(labeled.primary).toString(), - secondary: labeled.secondary, - } - } - cursorToLabeledResult(cursor: { primary: string; secondary: string }) { - const score = parseInt(cursor.primary, 10) - if (isNaN(score)) { - throw new InvalidRequestError('Malformed cursor') - } - return { - primary: score, - secondary: cursor.secondary, - } - } -} diff --git a/packages/pds/src/feed-gen/bsky-team.ts b/packages/pds/src/feed-gen/bsky-team.ts deleted file mode 100644 index ba4196e79d9..00000000000 --- a/packages/pds/src/feed-gen/bsky-team.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { NotEmptyArray } from '@atproto/common' -import { QueryParams as SkeletonParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton' -import AppContext from '../context' -import { paginate } from '../db/pagination' -import { AlgoHandler, AlgoResponse } from './types' -import { FeedKeyset } from '../app-view/api/app/bsky/util/feed' - -const BSKY_TEAM: NotEmptyArray = [ - 'did:plc:z72i7hdynmk6r22z27h6tvur', // @bsky.app - 'did:plc:ewvi7nxzyoun6zhxrhs64oiz', // @atproto.com - 'did:plc:eon2iu7v3x2ukgxkqaf7e5np', // @safety.bsky.app -] - -const handler: AlgoHandler = async ( - ctx: AppContext, - params: SkeletonParams, - requester: string, -): Promise => { - const { limit = 50, cursor } = params - const accountService = ctx.services.account(ctx.db) - const feedService = ctx.services.appView.feed(ctx.db) - const graphService = ctx.services.appView.graph(ctx.db) - - const { ref } = ctx.db.db.dynamic - - const postsQb = feedService - .selectPostQb() - .where('post.creator', 'in', BSKY_TEAM) - .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 = await feedQb.execute() - return { - feedItems, - cursor: keyset.packFromResult(feedItems), - } -} - -export default handler diff --git a/packages/pds/src/feed-gen/hot-classic.ts b/packages/pds/src/feed-gen/hot-classic.ts deleted file mode 100644 index 5284632a7e9..00000000000 --- a/packages/pds/src/feed-gen/hot-classic.ts +++ /dev/null @@ -1,59 +0,0 @@ -import AppContext from '../context' -import { NotEmptyArray } from '@atproto/common' -import { QueryParams as SkeletonParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton' -import { paginate } from '../db/pagination' -import { AlgoHandler, AlgoResponse } from './types' -import { FeedKeyset } from '../app-view/api/app/bsky/util/feed' -import { valuesList } from '../db/util' - -const NO_WHATS_HOT_LABELS: NotEmptyArray = ['!no-promote'] - -const handler: AlgoHandler = async ( - ctx: AppContext, - params: SkeletonParams, - requester: string, -): Promise => { - const { limit = 50, cursor } = params - const accountService = ctx.services.account(ctx.db) - const feedService = ctx.services.appView.feed(ctx.db) - const graphService = ctx.services.appView.graph(ctx.db) - - const { ref } = ctx.db.db.dynamic - - const postsQb = feedService - .selectPostQb() - .leftJoin('post_agg', 'post_agg.uri', 'post.uri') - .leftJoin('post_embed_record', 'post_embed_record.postUri', 'post.uri') - .where('post_agg.likeCount', '>=', 12) - .where('post.replyParent', 'is', null) - .whereNotExists((qb) => - qb - .selectFrom('label') - .selectAll() - .whereRef('val', 'in', valuesList(NO_WHATS_HOT_LABELS)) - .where('neg', '=', 0) - .where((clause) => - clause - .whereRef('label.uri', '=', ref('post.creator')) - .orWhereRef('label.uri', '=', ref('post.uri')) - .orWhereRef('label.uri', '=', ref('post_embed_record.embedUri')), - ), - ) - .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 = await feedQb.execute() - return { - feedItems, - cursor: keyset.packFromResult(feedItems), - } -} - -export default handler diff --git a/packages/pds/src/feed-gen/index.ts b/packages/pds/src/feed-gen/index.ts deleted file mode 100644 index 46657e87fae..00000000000 --- a/packages/pds/src/feed-gen/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { AtUri } from '@atproto/syntax' -import withFriends from './with-friends' -import bskyTeam from './bsky-team' -import hotClassic from './hot-classic' -import bestOfFollows from './best-of-follows' -import mutuals from './mutuals' -import { ids } from '../lexicon/lexicons' -import { MountedAlgos } from './types' - -const coll = ids.AppBskyFeedGenerator - -// These are custom algorithms that will be mounted directly onto an AppView -// Feel free to remove, update to your own, or serve the following logic at a record that you control -export const makeAlgos = (did: string): MountedAlgos => ({ - [AtUri.make(did, coll, 'with-friends').toString()]: withFriends, - [AtUri.make(did, coll, 'bsky-team').toString()]: bskyTeam, - [AtUri.make(did, coll, 'hot-classic').toString()]: hotClassic, - [AtUri.make(did, coll, 'best-of-follows').toString()]: bestOfFollows, - [AtUri.make(did, coll, 'mutuals').toString()]: mutuals, -}) diff --git a/packages/pds/src/feed-gen/mutuals.ts b/packages/pds/src/feed-gen/mutuals.ts deleted file mode 100644 index 94aba5bb104..00000000000 --- a/packages/pds/src/feed-gen/mutuals.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { QueryParams as SkeletonParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton' -import AppContext from '../context' -import { paginate } from '../db/pagination' -import { AlgoHandler, AlgoResponse } from './types' -import { - FeedKeyset, - getFeedDateThreshold, -} from '../app-view/api/app/bsky/util/feed' - -const handler: AlgoHandler = async ( - ctx: AppContext, - params: SkeletonParams, - requester: string, -): Promise => { - const { limit = 50, cursor } = params - const accountService = ctx.services.account(ctx.db) - const feedService = ctx.services.appView.feed(ctx.db) - const graphService = ctx.services.appView.graph(ctx.db) - - const { ref } = ctx.db.db.dynamic - - const mutualsSubquery = ctx.db.db - .selectFrom('follow') - .where('follow.creator', '=', requester) - .whereExists((qb) => - qb - .selectFrom('follow as follow_inner') - .whereRef('follow_inner.creator', '=', 'follow.subjectDid') - .where('follow_inner.subjectDid', '=', requester) - .selectAll(), - ) - .select('follow.subjectDid') - - const keyset = new FeedKeyset(ref('feed_item.sortAt'), ref('feed_item.cid')) - const sortFrom = keyset.unpack(cursor)?.primary - - let feedQb = feedService - .selectFeedItemQb() - .where('feed_item.type', '=', 'post') // ensures originatorDid is post.creator - .where((qb) => - qb - .where('originatorDid', '=', requester) - .orWhere('originatorDid', 'in', mutualsSubquery), - ) - .where((qb) => - accountService.whereNotMuted(qb, requester, [ref('originatorDid')]), - ) - .whereNotExists(graphService.blockQb(requester, [ref('originatorDid')])) - .where('feed_item.sortAt', '>', getFeedDateThreshold(sortFrom)) - - feedQb = paginate(feedQb, { limit, cursor, keyset }) - - const feedItems = await feedQb.execute() - return { - feedItems, - cursor: keyset.packFromResult(feedItems), - } -} - -export default handler diff --git a/packages/pds/src/feed-gen/types.ts b/packages/pds/src/feed-gen/types.ts deleted file mode 100644 index 7682a0e23bc..00000000000 --- a/packages/pds/src/feed-gen/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -import AppContext from '../context' -import { QueryParams as SkeletonParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton' -import { FeedRow } from '../app-view/services/feed' - -export type AlgoResponse = { - feedItems: FeedRow[] - cursor?: string -} - -export type AlgoHandler = ( - ctx: AppContext, - params: SkeletonParams, - requester: string, -) => Promise - -export type MountedAlgos = Record diff --git a/packages/pds/src/feed-gen/whats-hot.ts b/packages/pds/src/feed-gen/whats-hot.ts deleted file mode 100644 index d48c049e95c..00000000000 --- a/packages/pds/src/feed-gen/whats-hot.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { NotEmptyArray } from '@atproto/common' -import { InvalidRequestError } from '@atproto/xrpc-server' -import { QueryParams as SkeletonParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton' -import { AlgoHandler, AlgoResponse } from './types' -import { GenericKeyset, paginate } from '../db/pagination' -import AppContext from '../context' -import { valuesList } from '../db/util' -import { sql } from 'kysely' -import { FeedItemType } from '../app-view/services/feed' - -const NO_WHATS_HOT_LABELS: NotEmptyArray = [ - '!no-promote', - 'corpse', - 'self-harm', - 'porn', - 'sexual', - 'nudity', - 'underwear', -] - -const handler: AlgoHandler = async ( - ctx: AppContext, - params: SkeletonParams, - requester: string, -): Promise => { - if (ctx.db.dialect === 'sqlite') { - throw new Error('what-hot algo not available in sqlite') - } - - const { limit, cursor } = params - const accountService = ctx.services.account(ctx.db) - const graphService = ctx.services.appView.graph(ctx.db) - - const { ref } = ctx.db.db.dynamic - - // candidates are ranked within a materialized view by like count, depreciated over time. - - let builder = ctx.db.db - .selectFrom('algo_whats_hot_view as candidate') - .innerJoin('post', 'post.uri', 'candidate.uri') - .leftJoin('post_embed_record', 'post_embed_record.postUri', 'candidate.uri') - .whereNotExists((qb) => - qb - .selectFrom('label') - .selectAll() - .whereRef('val', 'in', valuesList(NO_WHATS_HOT_LABELS)) - .where('neg', '=', 0) - .where((clause) => - clause - .whereRef('label.uri', '=', ref('post.creator')) - .orWhereRef('label.uri', '=', ref('post.uri')) - .orWhereRef('label.uri', '=', ref('post_embed_record.embedUri')), - ), - ) - .where((qb) => - accountService.whereNotMuted(qb, requester, [ref('post.creator')]), - ) - .whereNotExists(graphService.blockQb(requester, [ref('post.creator')])) - .select([ - sql`${'post'}`.as('type'), - 'post.uri as uri', - 'post.cid as cid', - 'post.uri as postUri', - 'post.creator as originatorDid', - 'post.creator as postAuthorDid', - 'post.replyParent as replyParent', - 'post.replyRoot as replyRoot', - 'post.indexedAt as sortAt', - 'candidate.score', - 'candidate.cid', - ]) - - const keyset = new ScoreKeyset(ref('candidate.score'), ref('candidate.cid')) - builder = paginate(builder, { limit, cursor, keyset }) - - const feedItems = await builder.execute() - - return { - feedItems, - cursor: keyset.packFromResult(feedItems), - } -} - -export default handler - -type Result = { score: number; cid: string } -type LabeledResult = { primary: number; secondary: string } -export class ScoreKeyset extends GenericKeyset { - labelResult(result: Result) { - return { - primary: result.score, - secondary: result.cid, - } - } - labeledResultToCursor(labeled: LabeledResult) { - return { - primary: Math.round(labeled.primary).toString(), - secondary: labeled.secondary, - } - } - cursorToLabeledResult(cursor: { primary: string; secondary: string }) { - const score = parseInt(cursor.primary, 10) - if (isNaN(score)) { - throw new InvalidRequestError('Malformed cursor') - } - return { - primary: score, - secondary: cursor.secondary, - } - } -} diff --git a/packages/pds/src/feed-gen/with-friends.ts b/packages/pds/src/feed-gen/with-friends.ts deleted file mode 100644 index a36f435286c..00000000000 --- a/packages/pds/src/feed-gen/with-friends.ts +++ /dev/null @@ -1,62 +0,0 @@ -import AppContext from '../context' -import { QueryParams as SkeletonParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton' -import { AlgoHandler, AlgoResponse } from './types' - -const handler: AlgoHandler = async ( - _ctx: AppContext, - _params: SkeletonParams, - _requester: string, -): Promise => { - // Temporary change to only return a post notifying users that the feed is down - return { - feedItems: [ - { - type: 'post', - uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jzinucnmbi2c', - cid: 'bafyreifmtn55tubbv7tefrq277nzfy4zu7ioithky276aho5ehb6w3nu6q', - postUri: - 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jzinucnmbi2c', - postAuthorDid: 'did:plc:z72i7hdynmk6r22z27h6tvur', - originatorDid: 'did:plc:z72i7hdynmk6r22z27h6tvur', - replyParent: null, - replyRoot: null, - sortAt: '2023-07-01T23:04:27.853Z', - }, - ], - } - // const { cursor, limit = 50 } = params - // const accountService = ctx.services.account(ctx.db) - // const feedService = ctx.services.appView.feed(ctx.db) - // const graphService = ctx.services.appView.graph(ctx.db) - - // const { ref } = ctx.db.db.dynamic - - // const keyset = new FeedKeyset(ref('post.indexedAt'), ref('post.cid')) - // const sortFrom = keyset.unpack(cursor)?.primary - - // let postsQb = feedService - // .selectPostQb() - // // .innerJoin('post_agg', 'post_agg.uri', 'post.uri') - // // .where('post_agg.likeCount', '>=', 6) - // .whereExists((qb) => - // qb - // .selectFrom('follow') - // .where('follow.creator', '=', requester) - // .whereRef('follow.subjectDid', '=', 'post.creator'), - // ) - // .where((qb) => - // accountService.whereNotMuted(qb, requester, [ref('post.creator')]), - // ) - // .whereNotExists(graphService.blockQb(requester, [ref('post.creator')])) - // .where('post.indexedAt', '>', getFeedDateThreshold(sortFrom)) - - // postsQb = paginate(postsQb, { limit, cursor, keyset, tryIndex: true }) - - // const feedItems = await postsQb.execute() - // return { - // feedItems, - // cursor: keyset.packFromResult(feedItems), - // } -} - -export default handler diff --git a/packages/pds/src/image/index.ts b/packages/pds/src/image/index.ts index 3197b5aeb5c..ebf47088f6b 100644 --- a/packages/pds/src/image/index.ts +++ b/packages/pds/src/image/index.ts @@ -1,2 +1,80 @@ -export * from './sharp' -export type { Options, ImageInfo } from './util' +import { Readable } from 'stream' +import { pipeline } from 'stream/promises' +import sharp from 'sharp' +import { errHasMsg } from '@atproto/common' + +export async function maybeGetInfo( + stream: Readable, +): Promise { + let metadata: sharp.Metadata + try { + const processor = sharp() + const [result] = await Promise.all([ + processor.metadata(), + pipeline(stream, processor), // Handles error propagation + ]) + metadata = result + } catch (err) { + if (errHasMsg(err, 'Input buffer contains unsupported image format')) { + return null + } + throw err + } + const { size, height, width, format } = metadata + if ( + size === undefined || + height === undefined || + width === undefined || + format === undefined + ) { + return null + } + + return { + height, + width, + size, + mime: formatsToMimes[format] ?? ('unknown' as const), + } +} + +export async function getInfo(stream: Readable): Promise { + const maybeInfo = await maybeGetInfo(stream) + if (!maybeInfo) { + throw new Error('could not obtain all image metadata') + } + return maybeInfo +} + +export type Options = Dimensions & { + format: 'jpeg' | 'png' + // When 'cover' (default), scale to fill given dimensions, cropping if necessary. + // When 'inside', scale to fit within given dimensions. + fit?: 'cover' | 'inside' + // When false (default), do not scale up. + // When true, scale up to hit dimensions given in options. + // Otherwise, scale up to hit specified min dimensions. + min?: Dimensions | boolean + // A number 1-100 + quality?: number +} + +export type ImageInfo = Dimensions & { + size: number + mime: `image/${string}` | 'unknown' +} + +export type Dimensions = { height: number; width: number } + +export const formatsToMimes: { + [s in keyof sharp.FormatEnum]?: `image/${string}` +} = { + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + png: 'image/png', + gif: 'image/gif', + svg: 'image/svg+xml', + tif: 'image/tiff', + tiff: 'image/tiff', + webp: 'image/webp', +} diff --git a/packages/pds/src/image/invalidator.ts b/packages/pds/src/image/invalidator.ts deleted file mode 100644 index d1319951500..00000000000 --- a/packages/pds/src/image/invalidator.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { BlobCache } from './server' - -// Invalidation is a general interface for propagating an image blob -// takedown through any caches where a representation of it may be stored. -// @NOTE this does not remove the blob from storage: just invalidates it from caches. -// @NOTE keep in sync with same interface in aws/src/cloudfront.ts -export interface ImageInvalidator { - invalidate(subject: string, paths: string[]): Promise -} - -export class ImageProcessingServerInvalidator implements ImageInvalidator { - constructor(private cache: BlobCache) {} - async invalidate(_subject: string, paths: string[]) { - const results = await Promise.allSettled( - paths.map(async (path) => { - const [, signature] = path.split('/') - if (!signature) throw new Error('Missing signature') - await this.cache.clear(signature) - }), - ) - const rejection = results.find( - (result): result is PromiseRejectedResult => result.status === 'rejected', - ) - if (rejection) throw rejection.reason - } -} diff --git a/packages/pds/src/image/logger.ts b/packages/pds/src/image/logger.ts deleted file mode 100644 index f4bcb5c9a66..00000000000 --- a/packages/pds/src/image/logger.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { subsystemLogger } from '@atproto/common' - -export const logger = subsystemLogger('pds:image') - -export default logger diff --git a/packages/pds/src/image/server.ts b/packages/pds/src/image/server.ts deleted file mode 100644 index 8e52d5d1fdf..00000000000 --- a/packages/pds/src/image/server.ts +++ /dev/null @@ -1,170 +0,0 @@ -import fs from 'fs/promises' -import fsSync from 'fs' -import os from 'os' -import path from 'path' -import { Readable } from 'stream' -import express, { ErrorRequestHandler, NextFunction } from 'express' -import createError, { isHttpError } from 'http-errors' -import { BlobNotFoundError, BlobStore } from '@atproto/repo' -import { - cloneStream, - forwardStreamErrors, - isErrnoException, -} from '@atproto/common' -import { BadPathError, ImageUriBuilder } from './uri' -import log from './logger' -import { resize } from './sharp' -import { formatsToMimes, Options } from './util' - -export class ImageProcessingServer { - app = express() - uriBuilder: ImageUriBuilder - - constructor( - protected salt: string | Uint8Array, - protected key: string | Uint8Array, - protected storage: BlobStore, - public cache: BlobCache, - ) { - this.uriBuilder = new ImageUriBuilder('', salt, key) - this.app.get('*', this.handler.bind(this)) - this.app.use(errorMiddleware) - } - - async handler( - req: express.Request, - res: express.Response, - next: NextFunction, - ) { - try { - const path = req.path - const options = this.uriBuilder.getVerifiedOptions(path) - - // Cached flow - - try { - const cachedImage = await this.cache.get(options.signature) - res.statusCode = 200 - res.setHeader('x-cache', 'hit') - res.setHeader('content-type', getMime(options.format)) - res.setHeader('cache-control', `public, max-age=31536000`) // 1 year - res.setHeader('content-length', cachedImage.size) - forwardStreamErrors(cachedImage, res) - return cachedImage.pipe(res) - } catch (err) { - // Ignore BlobNotFoundError and move on to non-cached flow - if (!(err instanceof BlobNotFoundError)) throw err - } - - // Non-cached flow - - const imageStream = await this.storage.getStream(options.cid) - const processedImage = await resize(imageStream, options) - - // Cache in the background - this.cache - .put(options.signature, cloneStream(processedImage)) - .catch((err) => log.error(err, 'failed to cache image')) - // Respond - res.statusCode = 200 - res.setHeader('x-cache', 'miss') - res.setHeader('content-type', getMime(options.format)) - res.setHeader('cache-control', `public, max-age=31536000`) // 1 year - forwardStreamErrors(processedImage, res) - return ( - processedImage - // @NOTE sharp does emit this in time to be set as a header - .once('info', (info) => res.setHeader('content-length', info.size)) - .pipe(res) - ) - } catch (err: unknown) { - if (err instanceof BadPathError) { - return next(createError(400, err)) - } - if (err instanceof BlobNotFoundError) { - return next(createError(404, 'Image not found')) - } - return next(err) - } - } -} - -const errorMiddleware: ErrorRequestHandler = function (err, _req, res, next) { - if (isHttpError(err)) { - log.error(err, `error: ${err.message}`) - } else { - log.error(err, 'unhandled exception') - } - if (res.headersSent) { - return next(err) - } - const httpError = createError(err) - return res.status(httpError.status).json({ - message: httpError.expose ? httpError.message : 'Internal Server Error', - }) -} - -function getMime(format: Options['format']) { - const mime = formatsToMimes[format] - if (!mime) throw new Error('Unknown format') - return mime -} - -export interface BlobCache { - get(fileId: string): Promise - put(fileId: string, stream: Readable): Promise - clear(fileId: string): Promise - clearAll(): Promise -} - -export class BlobDiskCache implements BlobCache { - tempDir: string - constructor(basePath?: string) { - this.tempDir = basePath || path.join(os.tmpdir(), 'pds--processed-images') - if (!path.isAbsolute(this.tempDir)) { - throw new Error('Must provide an absolute path') - } - try { - fsSync.mkdirSync(this.tempDir, { recursive: true }) - } catch (err) { - // All good if cache dir already exists - if (isErrnoException(err) && err.code === 'EEXIST') return - } - } - - async get(fileId: string) { - const filePath = path.join(this.tempDir, fileId) - try { - const { size } = await fs.stat(filePath) - if (size === 0) { - throw new BlobNotFoundError() - } - return Object.assign(fsSync.createReadStream(filePath), { size }) - } catch (err) { - if (isErrnoException(err) && err.code === 'ENOENT') { - throw new BlobNotFoundError() - } - throw err - } - } - - async put(fileId: string, stream: Readable) { - const filename = path.join(this.tempDir, fileId) - try { - await fs.writeFile(filename, stream, { flag: 'wx' }) - } catch (err) { - // Do not overwrite existing file, just ignore the error - if (isErrnoException(err) && err.code === 'EEXIST') return - throw err - } - } - - async clear(fileId: string) { - const filename = path.join(this.tempDir, fileId) - await fs.rm(filename, { force: true }) - } - - async clearAll() { - await fs.rm(this.tempDir, { recursive: true, force: true }) - } -} diff --git a/packages/pds/src/image/sharp.ts b/packages/pds/src/image/sharp.ts deleted file mode 100644 index 1edc7a58835..00000000000 --- a/packages/pds/src/image/sharp.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Readable } from 'stream' -import { pipeline } from 'stream/promises' -import sharp from 'sharp' -import { errHasMsg, forwardStreamErrors } from '@atproto/common' -import { formatsToMimes, ImageInfo, Options } from './util' - -export type { Options } - -export async function resize( - stream: Readable, - options: Options, -): Promise { - const { height, width, min = false, fit = 'cover', format, quality } = options - - let processor = sharp() - - // Scale up to hit any specified minimum size - if (typeof min !== 'boolean') { - const upsizeProcessor = sharp().resize({ - fit: 'outside', - width: min.width, - height: min.height, - withoutReduction: true, - withoutEnlargement: false, - }) - forwardStreamErrors(stream, upsizeProcessor) - stream = stream.pipe(upsizeProcessor) - } - - // Scale down (or possibly up if min is true) to desired size - processor = processor.resize({ - fit, - width, - height, - withoutEnlargement: min !== true, - }) - - // Output to specified format - if (format === 'jpeg') { - processor = processor.jpeg({ quality: quality ?? 100 }) - } else if (format === 'png') { - processor = processor.png({ quality: quality ?? 100 }) - } else { - const exhaustiveCheck: never = format - throw new Error(`Unhandled case: ${exhaustiveCheck}`) - } - - forwardStreamErrors(stream, processor) - return stream.pipe(processor) -} - -export async function maybeGetInfo( - stream: Readable, -): Promise { - let metadata: sharp.Metadata - try { - const processor = sharp() - const [result] = await Promise.all([ - processor.metadata(), - pipeline(stream, processor), // Handles error propagation - ]) - metadata = result - } catch (err) { - if (errHasMsg(err, 'Input buffer contains unsupported image format')) { - return null - } - throw err - } - const { size, height, width, format } = metadata - if ( - size === undefined || - height === undefined || - width === undefined || - format === undefined - ) { - return null - } - - return { - height, - width, - size, - mime: formatsToMimes[format] ?? ('unknown' as const), - } -} - -export async function getInfo(stream: Readable): Promise { - const maybeInfo = await maybeGetInfo(stream) - if (!maybeInfo) { - throw new Error('could not obtain all image metadata') - } - return maybeInfo -} diff --git a/packages/pds/src/image/uri.ts b/packages/pds/src/image/uri.ts deleted file mode 100644 index 227a4ebb94b..00000000000 --- a/packages/pds/src/image/uri.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { createHmac } from 'crypto' -import * as uint8arrays from 'uint8arrays' -import { CID } from 'multiformats/cid' -import { Options } from './util' - -// @NOTE if there are any additions here, ensure to include them on ImageUriBuilder.commonSignedUris -type CommonSignedUris = 'avatar' | 'banner' | 'feed_thumbnail' | 'feed_fullsize' - -export class ImageUriBuilder { - public endpoint: string - private salt: Uint8Array - private key: Uint8Array - - constructor( - endpoint: string, - salt: Uint8Array | string, - key: Uint8Array | string, - ) { - this.endpoint = endpoint - this.salt = - typeof salt === 'string' ? uint8arrays.fromString(salt, 'hex') : salt - this.key = - typeof key === 'string' ? uint8arrays.fromString(key, 'hex') : key - } - - getSignedPath(opts: Options & { cid: CID }): string { - const path = ImageUriBuilder.getPath(opts) - const saltedPath = uint8arrays.concat([ - this.salt, - uint8arrays.fromString(path), - ]) - const sig = hmac(this.key, saltedPath).toString('base64url') - return `/${sig}${path}` - } - - getSignedUri(opts: Options & { cid: CID }): string { - const path = this.getSignedPath(opts) - return this.endpoint + path - } - - static commonSignedUris: CommonSignedUris[] = [ - 'avatar', - 'banner', - 'feed_thumbnail', - 'feed_fullsize', - ] - - getCommonSignedUri(id: CommonSignedUris, cid: string | CID): string { - if (id === 'avatar') { - return this.getSignedUri({ - cid: typeof cid === 'string' ? CID.parse(cid) : cid, - format: 'jpeg', - fit: 'cover', - height: 1000, - width: 1000, - min: true, - }) - } else if (id === 'banner') { - return this.getSignedUri({ - cid: typeof cid === 'string' ? CID.parse(cid) : cid, - format: 'jpeg', - fit: 'cover', - height: 1000, - width: 3000, - min: true, - }) - } else if (id === 'feed_fullsize') { - return this.getSignedUri({ - cid: typeof cid === 'string' ? CID.parse(cid) : cid, - format: 'jpeg', - fit: 'inside', - height: 2000, - width: 2000, - min: true, - }) - } else if (id === 'feed_thumbnail') { - return this.getSignedUri({ - cid: typeof cid === 'string' ? CID.parse(cid) : cid, - format: 'jpeg', - fit: 'inside', - height: 1000, - width: 1000, - min: true, - }) - } else { - const exhaustiveCheck: never = id - throw new Error( - `Unrecognized requested common uri type: ${exhaustiveCheck}`, - ) - } - } - - getVerifiedOptions(path: string): Options & { cid: CID; signature: string } { - if (path.at(0) !== '/') { - throw new BadPathError('Invalid path: does not start with a slash') - } - const pathParts = path.split('/') // ['', sig, 'rs:fill:...', ...] - const [sig] = pathParts.splice(1, 1) // ['', 'rs:fill:...', ...] - const unsignedPath = pathParts.join('/') - if (!sig || sig.includes(':')) { - throw new BadPathError('Invalid path: missing signature') - } - const saltedPath = uint8arrays.concat([ - this.salt, - uint8arrays.fromString(unsignedPath), - ]) - const validSig = hmac(this.key, saltedPath).toString('base64url') - if (sig !== validSig) { - throw new BadPathError('Invalid path: bad signature') - } - const options = ImageUriBuilder.getOptions(unsignedPath) - return { - signature: validSig, - ...options, - } - } - - static getPath(opts: Options & { cid: CID }) { - const fit = opts.fit === 'inside' ? 'fit' : 'fill' // fit default is 'cover' - const enlarge = opts.min === true ? 1 : 0 // min default is false - const resize = `rs:${fit}:${opts.width}:${opts.height}:${enlarge}:0` // final ':0' is for interop with imgproxy - const minWidth = - opts.min && typeof opts.min === 'object' ? `mw:${opts.min.width}` : null - const minHeight = - opts.min && typeof opts.min === 'object' ? `mh:${opts.min.height}` : null - const quality = opts.quality ? `q:${opts.quality}` : null - return ( - `/` + - [resize, minWidth, minHeight, quality].filter(Boolean).join('/') + - `/plain/${opts.cid.toString()}@${opts.format}` - ) - } - - static getOptions(path: string): Options & { cid: CID } { - if (path.at(0) !== '/') { - throw new BadPathError('Invalid path: does not start with a slash') - } - const parts = path.split('/') - if (parts.at(-2) !== 'plain') { - throw new BadPathError('Invalid path') - } - const cidAndFormat = parts.at(-1) - const [cid, format, ...others] = cidAndFormat?.split('@') ?? [] - if (!cid || (format !== 'png' && format !== 'jpeg') || others.length) { - throw new BadPathError('Invalid path: bad cid/format part') - } - const resizePart = parts.find((part) => part.startsWith('rs:')) - const qualityPart = parts.find((part) => part.startsWith('q:')) - const minWidthPart = parts.find((part) => part.startsWith('mw:')) - const minHeightPart = parts.find((part) => part.startsWith('mh:')) - const [, fit, width, height, enlarge] = resizePart?.split(':') ?? [] - const [, quality] = qualityPart?.split(':') ?? [] - const [, minWidth] = minWidthPart?.split(':') ?? [] - const [, minHeight] = minHeightPart?.split(':') ?? [] - if (fit !== 'fill' && fit !== 'fit') { - throw new BadPathError('Invalid path: bad resize fit param') - } - if (isNaN(toInt(width)) || isNaN(toInt(height))) { - throw new BadPathError('Invalid path: bad resize height/width param') - } - if (enlarge !== '0' && enlarge !== '1') { - throw new BadPathError('Invalid path: bad resize enlarge param') - } - if (quality && isNaN(toInt(quality))) { - throw new BadPathError('Invalid path: bad quality param') - } - if ( - (!minWidth && minHeight) || - (minWidth && !minHeight) || - (minWidth && isNaN(toInt(minWidth))) || - (minHeight && isNaN(toInt(minHeight))) || - (enlarge === '1' && (minHeight || minHeight)) - ) { - throw new BadPathError('Invalid path: bad min width/height param') - } - return { - cid: CID.parse(cid), - format, - height: toInt(height), - width: toInt(width), - fit: fit === 'fill' ? 'cover' : 'inside', - quality: quality ? toInt(quality) : undefined, - min: - minWidth && minHeight - ? { width: toInt(minWidth), height: toInt(minHeight) } - : enlarge === '1', - } - } -} - -export class BadPathError extends Error {} - -function toInt(str: string) { - if (!/^\d+$/.test(str)) { - return NaN // String must be all numeric - } - return parseInt(str, 10) -} - -function hmac(key: Uint8Array, message: Uint8Array) { - return createHmac('sha256', key).update(message).digest() -} diff --git a/packages/pds/src/image/util.ts b/packages/pds/src/image/util.ts deleted file mode 100644 index ce18ba343d5..00000000000 --- a/packages/pds/src/image/util.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { FormatEnum } from 'sharp' - -export type Options = Dimensions & { - format: 'jpeg' | 'png' - // When 'cover' (default), scale to fill given dimensions, cropping if necessary. - // When 'inside', scale to fit within given dimensions. - fit?: 'cover' | 'inside' - // When false (default), do not scale up. - // When true, scale up to hit dimensions given in options. - // Otherwise, scale up to hit specified min dimensions. - min?: Dimensions | boolean - // A number 1-100 - quality?: number -} - -export type ImageInfo = Dimensions & { - size: number - mime: `image/${string}` | 'unknown' -} - -export type Dimensions = { height: number; width: number } - -export const formatsToMimes: { [s in keyof FormatEnum]?: `image/${string}` } = { - jpg: 'image/jpeg', - jpeg: 'image/jpeg', - png: 'image/png', - gif: 'image/gif', - svg: 'image/svg+xml', - tif: 'image/tiff', - tiff: 'image/tiff', - webp: 'image/webp', -} diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index a156d8f7b8e..36f15adae8e 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -36,26 +36,18 @@ import { ServerMailer } from './mailer' import { ModerationMailer } from './mailer/moderation' import { createServer } from './lexicon' import { MessageDispatcher } from './event-stream/message-queue' -import { ImageUriBuilder } from './image/uri' -import { BlobDiskCache, ImageProcessingServer } from './image/server' import { createServices } from './services' import { createHttpTerminator, HttpTerminator } from 'http-terminator' import AppContext from './context' import { Sequencer, SequencerLeader } from './sequencer' -import { - ImageInvalidator, - ImageProcessingServerInvalidator, -} from './image/invalidator' import { Labeler, HiveLabeler, KeywordLabeler } from './labeler' import { BackgroundQueue } from './event-stream/background-queue' import DidSqlCache from './did-cache' -import { MountedAlgos } from './feed-gen/types' import { Crawlers } from './crawlers' import { LabelCache } from './label-cache' import { getRedisClient } from './redis' import { RuntimeFlags } from './runtime-flags' -export type { MountedAlgos } from './feed-gen/types' export type { ServerConfigValues } from './config' export { ServerConfig } from './config' export { Database } from './db' @@ -63,7 +55,6 @@ export { ViewMaintainer } from './db/views' export { PeriodicModerationActionReversal } from './db/periodic-moderation-action-reversal' export { DiskBlobStore, MemoryBlobStore } from './storage' export { AppContext } from './context' -export { makeAlgos } from './feed-gen' export class PDS { public ctx: AppContext @@ -81,21 +72,11 @@ export class PDS { static create(opts: { db: Database blobstore: BlobStore - imgInvalidator?: ImageInvalidator repoSigningKey: crypto.Keypair plcRotationKey: crypto.Keypair - algos?: MountedAlgos config: ServerConfig }): PDS { - const { - db, - blobstore, - repoSigningKey, - plcRotationKey, - algos = {}, - config, - } = opts - let maybeImgInvalidator = opts.imgInvalidator + const { db, blobstore, repoSigningKey, plcRotationKey, config } = opts const auth = new ServerAuth({ jwtSecret: config.jwtSecret, adminPass: config.adminPassword, @@ -142,35 +123,6 @@ export class PDS { app.use(loggerMiddleware) app.use(compression()) - let imgUriEndpoint = config.imgUriEndpoint - if (!imgUriEndpoint) { - const imgProcessingCache = new BlobDiskCache(config.blobCacheLocation) - const imgProcessingServer = new ImageProcessingServer( - config.imgUriSalt, - config.imgUriKey, - blobstore, - imgProcessingCache, - ) - maybeImgInvalidator ??= new ImageProcessingServerInvalidator( - imgProcessingCache, - ) - app.use('/image', imgProcessingServer.app) - imgUriEndpoint = `${config.publicUrl}/image` - } - - let imgInvalidator: ImageInvalidator - if (maybeImgInvalidator) { - imgInvalidator = maybeImgInvalidator - } else { - throw new Error('Missing PDS image invalidator') - } - - const imgUriBuilder = new ImageUriBuilder( - imgUriEndpoint, - config.imgUriSalt, - config.imgUriKey, - ) - const backgroundQueue = new BackgroundQueue(db) const crawlers = new Crawlers( config.hostname, @@ -198,17 +150,12 @@ export class PDS { } const labelCache = new LabelCache(db) - - const appviewAgent = config.bskyAppViewEndpoint - ? new AtpAgent({ service: config.bskyAppViewEndpoint }) - : undefined + const appviewAgent = new AtpAgent({ service: config.bskyAppViewEndpoint }) const services = createServices({ repoSigningKey, messageDispatcher, blobstore, - imgUriBuilder, - imgInvalidator, labeler, labelCache, appviewAgent, @@ -247,11 +194,9 @@ export class PDS { services, mailer, moderationMailer, - imgUriBuilder, backgroundQueue, appviewAgent, crawlers, - algos, }) const xrpcOpts: XrpcServerOptions = { diff --git a/packages/pds/src/services/index.ts b/packages/pds/src/services/index.ts index b49693cc8cf..a60451785ec 100644 --- a/packages/pds/src/services/index.ts +++ b/packages/pds/src/services/index.ts @@ -3,16 +3,11 @@ import * as crypto from '@atproto/crypto' import { BlobStore } from '@atproto/repo' import Database from '../db' import { MessageDispatcher } from '../event-stream/message-queue' -import { ImageUriBuilder } from '../image/uri' -import { ImageInvalidator } from '../image/invalidator' import { AccountService } from './account' import { AuthService } from './auth' import { RecordService } from './record' import { RepoService } from './repo' import { ModerationService } from './moderation' -import { ActorService } from '../app-view/services/actor' -import { GraphService } from '../app-view/services/graph' -import { FeedService } from '../app-view/services/feed' import { IndexingService } from '../app-view/services/indexing' import { Labeler } from '../labeler' import { LabelService } from '../app-view/services/label' @@ -25,8 +20,6 @@ export function createServices(resources: { repoSigningKey: crypto.Keypair messageDispatcher: MessageDispatcher blobstore: BlobStore - imgUriBuilder: ImageUriBuilder - imgInvalidator: ImageInvalidator labeler: Labeler labelCache: LabelCache appviewAgent?: AtpAgent @@ -39,8 +32,6 @@ export function createServices(resources: { repoSigningKey, messageDispatcher, blobstore, - imgUriBuilder, - imgInvalidator, labeler, labelCache, appviewAgent, @@ -67,16 +58,8 @@ export function createServices(resources: { appviewDid, appviewCdnUrlPattern, ), - moderation: ModerationService.creator( - messageDispatcher, - blobstore, - imgUriBuilder, - imgInvalidator, - ), + moderation: ModerationService.creator(messageDispatcher, blobstore), appView: { - actor: ActorService.creator(imgUriBuilder, labelCache), - graph: GraphService.creator(imgUriBuilder), - feed: FeedService.creator(imgUriBuilder, labelCache), indexing: IndexingService.creator(backgroundQueue), label: LabelService.creator(labelCache), }, @@ -91,10 +74,7 @@ export type Services = { local: FromDb moderation: FromDb appView: { - feed: FromDb indexing: FromDb - actor: FromDb - graph: FromDb label: FromDb } } diff --git a/packages/pds/src/services/moderation/index.ts b/packages/pds/src/services/moderation/index.ts index 0a5ba6d02b0..6104ffe728b 100644 --- a/packages/pds/src/services/moderation/index.ts +++ b/packages/pds/src/services/moderation/index.ts @@ -9,8 +9,6 @@ import { ModerationAction, ModerationReport } from '../../db/tables/moderation' import { RecordService } from '../record' import { ModerationViews } from './views' import SqlRepoStorage from '../../sql-repo-storage' -import { ImageInvalidator } from '../../image/invalidator' -import { ImageUriBuilder } from '../../image/uri' import { TAKEDOWN } from '../../lexicon/types/com/atproto/admin/defs' import { addHoursToDate } from '../../util/date' @@ -19,24 +17,11 @@ export class ModerationService { public db: Database, public messageDispatcher: MessageQueue, public blobstore: BlobStore, - public imgUriBuilder: ImageUriBuilder, - public imgInvalidator: ImageInvalidator, ) {} - static creator( - messageDispatcher: MessageQueue, - blobstore: BlobStore, - imgUriBuilder: ImageUriBuilder, - imgInvalidator: ImageInvalidator, - ) { + static creator(messageDispatcher: MessageQueue, blobstore: BlobStore) { return (db: Database) => - new ModerationService( - db, - messageDispatcher, - blobstore, - imgUriBuilder, - imgInvalidator, - ) + new ModerationService(db, messageDispatcher, blobstore) } views = new ModerationViews(this.db, this.messageDispatcher) @@ -497,14 +482,7 @@ export class ModerationService { .where('takedownId', 'is', null) .executeTakeFirst() await Promise.all( - info.blobCids.map(async (cid) => { - await this.blobstore.quarantine(cid) - const paths = ImageUriBuilder.commonSignedUris.map((id) => { - const uri = this.imgUriBuilder.getCommonSignedUri(id, cid) - return uri.replace(this.imgUriBuilder.endpoint, '') - }) - await this.imgInvalidator.invalidate(cid.toString(), paths) - }), + info.blobCids.map((cid) => this.blobstore.quarantine(cid)), ) } } diff --git a/packages/pds/tests/__snapshots__/feed-generation.test.ts.snap b/packages/pds/tests/__snapshots__/feed-generation.test.ts.snap deleted file mode 100644 index b1c5319fd7d..00000000000 --- a/packages/pds/tests/__snapshots__/feed-generation.test.ts.snap +++ /dev/null @@ -1,1382 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`feed generation does not embed taken-down feed generator records in posts 1`] = ` -Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewNotFound", - "notFound": true, - "uri": "record(2)", - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "weird feed", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(0)", - "viewer": Object {}, -} -`; - -exports[`feed generation embeds feed generator records in posts 1`] = ` -Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.feed.defs#generatorView", - "cid": "cids(2)", - "creator": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(5)", - "val": "self-label-a", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(5)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", - "muted": false, - }, - }, - "description": "Provides all feed candidates", - "did": "user(1)", - "displayName": "All", - "indexedAt": "1970-01-01T00:00:00.000Z", - "likeCount": 2, - "uri": "record(2)", - "viewer": Object { - "like": "record(6)", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "cool feed!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(0)", - "viewer": Object {}, -} -`; - -exports[`feed generation getActorFeeds fetches feed generators by actor. 1`] = ` -Array [ - Object { - "cid": "cids(0)", - "creator": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "its me!", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "description": "Provides odd-indexed feed candidates", - "did": "user(0)", - "displayName": "Odd", - "indexedAt": "1970-01-01T00:00:00.000Z", - "likeCount": 0, - "uri": "record(0)", - "viewer": Object {}, - }, - Object { - "cid": "cids(2)", - "creator": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "its me!", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "description": "Provides all feed candidates, blindly ignoring pagination limit", - "did": "user(0)", - "displayName": "Bad Pagination", - "indexedAt": "1970-01-01T00:00:00.000Z", - "likeCount": 0, - "uri": "record(4)", - "viewer": Object {}, - }, - Object { - "cid": "cids(3)", - "creator": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "its me!", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "description": "Provides even-indexed feed candidates", - "did": "user(0)", - "displayName": "Even", - "indexedAt": "1970-01-01T00:00:00.000Z", - "likeCount": 0, - "uri": "record(5)", - "viewer": Object {}, - }, - Object { - "cid": "cids(4)", - "creator": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "its me!", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "description": "Provides all feed candidates", - "did": "user(0)", - "displayName": "All", - "indexedAt": "1970-01-01T00:00:00.000Z", - "likeCount": 2, - "uri": "record(6)", - "viewer": Object { - "like": "record(7)", - }, - }, -] -`; - -exports[`feed generation getFeed paginates, handling replies and reposts. 1`] = ` -Array [ - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(0)", - "val": "self-label", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label", - }, - ], - }, - "text": "hey there", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(0)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(5)", - "val": "self-label-a", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(5)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(4)", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(5)", - "val": "self-label-a", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(5)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", - "muted": false, - }, - }, - "cid": "cids(2)", - "embeds": Array [], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(2)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(6)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - }, - "text": "hi im carol", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(6)", - "viewer": Object { - "like": "record(9)", - }, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(7)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(8)", - "uri": "record(11)", - }, - "root": Object { - "cid": "cids(8)", - "uri": "record(11)", - }, - }, - "text": "of course", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(10)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(8)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(11)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(8)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(11)", - "viewer": Object {}, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(3)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(3)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(13)", - "muted": false, - }, - }, - "cid": "cids(9)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(4)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(5)", - "val": "self-label-a", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(5)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(2)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(6)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(6)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(4)", - "uri": "record(6)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(0)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(12)", - "viewer": Object {}, - }, - "reason": Object { - "$type": "app.bsky.feed.defs#reasonRepost", - "by": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - }, -] -`; - -exports[`feed generation getFeed resolves basic feed contents. 1`] = ` -Array [ - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(0)", - "val": "self-label", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label", - }, - ], - }, - "text": "hey there", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(0)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", - "muted": false, - }, - }, - "cid": "cids(2)", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(6)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(8)", - "val": "self-label-a", - }, - Object { - "cid": "cids(6)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(8)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(5)", - "embeds": Array [], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(5)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(3)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(4)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(5)", - "uri": "record(5)", - }, - }, - }, - "text": "hi im carol", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(2)", - "viewer": Object { - "like": "record(9)", - }, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", - "muted": false, - }, - }, - "cid": "cids(7)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(8)", - "uri": "record(11)", - }, - "root": Object { - "cid": "cids(8)", - "uri": "record(11)", - }, - }, - "text": "of course", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(10)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(8)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(11)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(8)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(11)", - "viewer": Object {}, - }, - }, - }, -] -`; - -exports[`feed generation getFeedGenerator describes a feed gen & returns online status 1`] = ` -Object { - "isOnline": true, - "isValid": true, - "view": Object { - "cid": "cids(0)", - "creator": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "description": "Provides all feed candidates", - "did": "user(0)", - "displayName": "All", - "indexedAt": "1970-01-01T00:00:00.000Z", - "likeCount": 2, - "uri": "record(0)", - "viewer": Object { - "like": "record(4)", - }, - }, -} -`; - -exports[`feed generation getFeedGenerators describes multiple feed gens 1`] = ` -Object { - "feeds": Array [ - Object { - "cid": "cids(0)", - "creator": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "description": "Provides all feed candidates", - "did": "user(0)", - "displayName": "All", - "indexedAt": "1970-01-01T00:00:00.000Z", - "likeCount": 2, - "uri": "record(0)", - "viewer": Object { - "like": "record(4)", - }, - }, - Object { - "cid": "cids(2)", - "creator": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "description": "Provides even-indexed feed candidates", - "did": "user(0)", - "displayName": "Even", - "indexedAt": "1970-01-01T00:00:00.000Z", - "likeCount": 0, - "uri": "record(5)", - "viewer": Object {}, - }, - ], -} -`; diff --git a/packages/pds/tests/__snapshots__/indexing.test.ts.snap b/packages/pds/tests/__snapshots__/indexing.test.ts.snap deleted file mode 100644 index c4bc3f83cf0..00000000000 --- a/packages/pds/tests/__snapshots__/indexing.test.ts.snap +++ /dev/null @@ -1,141 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`indexing indexes posts. 1`] = ` -Object { - "thread": Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(1)", - }, - ], - "index": Object { - "byteEnd": 9, - "byteStart": 0, - }, - }, - ], - "text": "@bob.test how are you?", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(0)", - "viewer": Object {}, - }, - "replies": Array [], - }, -} -`; - -exports[`indexing indexes posts. 2`] = ` -Object { - "createNotifications": Array [ - Object { - "author": "user(1)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "reason": "mention", - "reasonSubject": null, - "recordCid": "cids(0)", - "recordUri": "record(0)", - "userDid": "user(0)", - }, - ], - "deleteNotifications": Array [], -} -`; - -exports[`indexing indexes profiles. 1`] = ` -Object { - "did": "user(0)", - "displayName": "dan", - "followersCount": 0, - "followsCount": 0, - "handle": "dan.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "postsCount": 0, - "viewer": Object { - "blockedBy": false, - "muted": false, - }, -} -`; - -exports[`indexing indexes profiles. 2`] = ` -Object { - "did": "user(0)", - "displayName": "danny", - "followersCount": 0, - "followsCount": 0, - "handle": "dan.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "postsCount": 0, - "viewer": Object { - "blockedBy": false, - "muted": false, - }, -} -`; - -exports[`indexing indexes profiles. 3`] = ` -Object { - "did": "user(0)", - "followersCount": 0, - "followsCount": 0, - "handle": "dan.test", - "labels": Array [], - "postsCount": 0, - "viewer": Object { - "blockedBy": false, - "muted": false, - }, -} -`; - -exports[`indexing indexes profiles. 4`] = ` -Object { - "createNotifications": Array [], - "deleteNotifications": Array [], - "updateNotifications": Array [], -} -`; diff --git a/packages/pds/tests/_util.ts b/packages/pds/tests/_util.ts index 0b94f09148b..16061b8af51 100644 --- a/packages/pds/tests/_util.ts +++ b/packages/pds/tests/_util.ts @@ -15,7 +15,6 @@ import DiskBlobStore from '../src/storage/disk-blobstore' import AppContext from '../src/context' import { DAY, HOUR } from '@atproto/common' import { lexToJson } from '@atproto/lexicon' -import { MountedAlgos } from '../src/feed-gen/types' const ADMIN_PASSWORD = 'admin-pass' const MODERATOR_PASSWORD = 'moderator-pass' @@ -31,7 +30,6 @@ export type TestServerInfo = { export type TestServerOpts = { migration?: string - algos?: MountedAlgos } export const runTestServer = async ( @@ -97,9 +95,6 @@ export const runTestServer = async ( appUrlPasswordReset: 'app://forgot-password', emailNoReplyAddress: 'noreply@blueskyweb.xyz', publicUrl: 'https://pds.public.url', - imgUriSalt: '9dd04221f5755bce5f55f47464c27e1e', - imgUriKey: - 'f23ecd142835025f42c3db2cf25dd813956c178392760256211f9d315f8ab4d8', dbPostgresUrl: process.env.DB_POSTGRES_URL, blobstoreLocation: `${blobstoreLoc}/blobs`, blobstoreTmp: `${blobstoreLoc}/tmp`, @@ -109,8 +104,9 @@ export const runTestServer = async ( maxSubscriptionBuffer: 200, repoBackfillLimitMs: HOUR, sequencerLeaderLockId: uniqueLockId(), + bskyAppViewEndpoint: 'http://fake_address.invalid', + bskyAppViewDid: 'did:example:fake', dbTxLockNonce: await randomStr(32, 'base32'), - bskyAppViewProxy: false, ...params, }) @@ -153,7 +149,6 @@ export const runTestServer = async ( repoSigningKey, plcRotationKey, config: cfg, - algos: opts.algos, }) const pdsServer = await pds.start() const pdsPort = (pdsServer.address() as AddressInfo).port diff --git a/packages/pds/tests/account-deletion.test.ts b/packages/pds/tests/account-deletion.test.ts index 31ffcd1af5b..b99807ec6d5 100644 --- a/packages/pds/tests/account-deletion.test.ts +++ b/packages/pds/tests/account-deletion.test.ts @@ -237,29 +237,6 @@ describe('account deletion', () => { ) }) - it('no longer displays the users posts in feeds', async () => { - const feed = await agent.api.app.bsky.feed.getTimeline(undefined, { - headers: sc.getHeaders(sc.dids.alice), - }) - const found = feed.data.feed.filter( - (item) => item.post.author.did === carol.did, - ) - expect(found.length).toBe(0) - }) - - it('removes notifications from the user', async () => { - const notifs = await agent.api.app.bsky.notification.listNotifications( - undefined, - { - headers: sc.getHeaders(sc.dids.alice), - }, - ) - const found = notifs.data.notifications.filter( - (item) => item.author.did === sc.dids.carol, - ) - expect(found.length).toBe(0) - }) - it('can delete an empty user', async () => { const eve = await sc.createAccount('eve', { handle: 'eve.test', diff --git a/packages/pds/tests/views/admin/__snapshots__/get-moderation-action.test.ts.snap b/packages/pds/tests/admin/__snapshots__/get-moderation-action.test.ts.snap similarity index 100% rename from packages/pds/tests/views/admin/__snapshots__/get-moderation-action.test.ts.snap rename to packages/pds/tests/admin/__snapshots__/get-moderation-action.test.ts.snap diff --git a/packages/pds/tests/views/admin/__snapshots__/get-moderation-actions.test.ts.snap b/packages/pds/tests/admin/__snapshots__/get-moderation-actions.test.ts.snap similarity index 100% rename from packages/pds/tests/views/admin/__snapshots__/get-moderation-actions.test.ts.snap rename to packages/pds/tests/admin/__snapshots__/get-moderation-actions.test.ts.snap diff --git a/packages/pds/tests/views/admin/__snapshots__/get-moderation-report.test.ts.snap b/packages/pds/tests/admin/__snapshots__/get-moderation-report.test.ts.snap similarity index 100% rename from packages/pds/tests/views/admin/__snapshots__/get-moderation-report.test.ts.snap rename to packages/pds/tests/admin/__snapshots__/get-moderation-report.test.ts.snap diff --git a/packages/pds/tests/views/admin/__snapshots__/get-moderation-reports.test.ts.snap b/packages/pds/tests/admin/__snapshots__/get-moderation-reports.test.ts.snap similarity index 100% rename from packages/pds/tests/views/admin/__snapshots__/get-moderation-reports.test.ts.snap rename to packages/pds/tests/admin/__snapshots__/get-moderation-reports.test.ts.snap diff --git a/packages/pds/tests/views/admin/__snapshots__/get-record.test.ts.snap b/packages/pds/tests/admin/__snapshots__/get-record.test.ts.snap similarity index 100% rename from packages/pds/tests/views/admin/__snapshots__/get-record.test.ts.snap rename to packages/pds/tests/admin/__snapshots__/get-record.test.ts.snap diff --git a/packages/pds/tests/views/admin/__snapshots__/get-repo.test.ts.snap b/packages/pds/tests/admin/__snapshots__/get-repo.test.ts.snap similarity index 100% rename from packages/pds/tests/views/admin/__snapshots__/get-repo.test.ts.snap rename to packages/pds/tests/admin/__snapshots__/get-repo.test.ts.snap diff --git a/packages/pds/tests/views/admin/get-moderation-action.test.ts b/packages/pds/tests/admin/get-moderation-action.test.ts similarity index 94% rename from packages/pds/tests/views/admin/get-moderation-action.test.ts rename to packages/pds/tests/admin/get-moderation-action.test.ts index 66562a73ec2..54b86984416 100644 --- a/packages/pds/tests/views/admin/get-moderation-action.test.ts +++ b/packages/pds/tests/admin/get-moderation-action.test.ts @@ -6,10 +6,10 @@ import { import { REASONOTHER, REASONSPAM, -} from '../../../src/lexicon/types/com/atproto/moderation/defs' -import { runTestServer, forSnapshot, CloseFn, adminAuth } from '../../_util' -import { SeedClient } from '../../seeds/client' -import basicSeed from '../../seeds/basic' +} from '../../src/lexicon/types/com/atproto/moderation/defs' +import { runTestServer, forSnapshot, CloseFn, adminAuth } from '../_util' +import { SeedClient } from '../seeds/client' +import basicSeed from '../seeds/basic' describe('pds admin get moderation action view', () => { let agent: AtpAgent diff --git a/packages/pds/tests/views/admin/get-moderation-actions.test.ts b/packages/pds/tests/admin/get-moderation-actions.test.ts similarity index 96% rename from packages/pds/tests/views/admin/get-moderation-actions.test.ts rename to packages/pds/tests/admin/get-moderation-actions.test.ts index 31ee1686c3c..1ad5e066c60 100644 --- a/packages/pds/tests/views/admin/get-moderation-actions.test.ts +++ b/packages/pds/tests/admin/get-moderation-actions.test.ts @@ -7,16 +7,16 @@ import { import { REASONOTHER, REASONSPAM, -} from '../../../src/lexicon/types/com/atproto/moderation/defs' +} from '../../src/lexicon/types/com/atproto/moderation/defs' import { runTestServer, forSnapshot, CloseFn, adminAuth, paginateAll, -} from '../../_util' -import { SeedClient } from '../../seeds/client' -import basicSeed from '../../seeds/basic' +} from '../_util' +import { SeedClient } from '../seeds/client' +import basicSeed from '../seeds/basic' describe('pds admin get moderation actions view', () => { let agent: AtpAgent diff --git a/packages/pds/tests/views/admin/get-moderation-report.test.ts b/packages/pds/tests/admin/get-moderation-report.test.ts similarity index 94% rename from packages/pds/tests/views/admin/get-moderation-report.test.ts rename to packages/pds/tests/admin/get-moderation-report.test.ts index c1363a0c49f..7d433539b37 100644 --- a/packages/pds/tests/views/admin/get-moderation-report.test.ts +++ b/packages/pds/tests/admin/get-moderation-report.test.ts @@ -6,10 +6,10 @@ import { import { REASONOTHER, REASONSPAM, -} from '../../../src/lexicon/types/com/atproto/moderation/defs' -import { runTestServer, forSnapshot, CloseFn, adminAuth } from '../../_util' -import { SeedClient } from '../../seeds/client' -import basicSeed from '../../seeds/basic' +} from '../../src/lexicon/types/com/atproto/moderation/defs' +import { runTestServer, forSnapshot, CloseFn, adminAuth } from '../_util' +import { SeedClient } from '../seeds/client' +import basicSeed from '../seeds/basic' describe('pds admin get moderation action view', () => { let agent: AtpAgent diff --git a/packages/pds/tests/views/admin/get-moderation-reports.test.ts b/packages/pds/tests/admin/get-moderation-reports.test.ts similarity index 98% rename from packages/pds/tests/views/admin/get-moderation-reports.test.ts rename to packages/pds/tests/admin/get-moderation-reports.test.ts index 14be4ce821a..20f1c97f781 100644 --- a/packages/pds/tests/views/admin/get-moderation-reports.test.ts +++ b/packages/pds/tests/admin/get-moderation-reports.test.ts @@ -7,16 +7,16 @@ import { import { REASONOTHER, REASONSPAM, -} from '../../../src/lexicon/types/com/atproto/moderation/defs' +} from '../../src/lexicon/types/com/atproto/moderation/defs' import { runTestServer, forSnapshot, CloseFn, adminAuth, paginateAll, -} from '../../_util' -import { SeedClient } from '../../seeds/client' -import basicSeed from '../../seeds/basic' +} from '../_util' +import { SeedClient } from '../seeds/client' +import basicSeed from '../seeds/basic' describe('pds admin get moderation reports view', () => { let agent: AtpAgent diff --git a/packages/pds/tests/views/admin/get-record.test.ts b/packages/pds/tests/admin/get-record.test.ts similarity index 95% rename from packages/pds/tests/views/admin/get-record.test.ts rename to packages/pds/tests/admin/get-record.test.ts index 797a4b726f2..d70707b2b70 100644 --- a/packages/pds/tests/views/admin/get-record.test.ts +++ b/packages/pds/tests/admin/get-record.test.ts @@ -7,16 +7,16 @@ import { import { REASONOTHER, REASONSPAM, -} from '../../../src/lexicon/types/com/atproto/moderation/defs' +} from '../../src/lexicon/types/com/atproto/moderation/defs' import { runTestServer, forSnapshot, CloseFn, adminAuth, TestServerInfo, -} from '../../_util' -import { SeedClient } from '../../seeds/client' -import basicSeed from '../../seeds/basic' +} from '../_util' +import { SeedClient } from '../seeds/client' +import basicSeed from '../seeds/basic' describe('pds admin get record view', () => { let server: TestServerInfo diff --git a/packages/pds/tests/views/admin/get-repo.test.ts b/packages/pds/tests/admin/get-repo.test.ts similarity index 95% rename from packages/pds/tests/views/admin/get-repo.test.ts rename to packages/pds/tests/admin/get-repo.test.ts index c0f8d500c90..3cb997f6ff2 100644 --- a/packages/pds/tests/views/admin/get-repo.test.ts +++ b/packages/pds/tests/admin/get-repo.test.ts @@ -6,7 +6,7 @@ import { import { REASONOTHER, REASONSPAM, -} from '../../../src/lexicon/types/com/atproto/moderation/defs' +} from '../../src/lexicon/types/com/atproto/moderation/defs' import { runTestServer, forSnapshot, @@ -15,9 +15,9 @@ import { TestServerInfo, moderatorAuth, triageAuth, -} from '../../_util' -import { SeedClient } from '../../seeds/client' -import basicSeed from '../../seeds/basic' +} from '../_util' +import { SeedClient } from '../seeds/client' +import basicSeed from '../seeds/basic' describe('pds admin get repo view', () => { let server: TestServerInfo diff --git a/packages/pds/tests/views/admin/invites.test.ts b/packages/pds/tests/admin/invites.test.ts similarity index 99% rename from packages/pds/tests/views/admin/invites.test.ts rename to packages/pds/tests/admin/invites.test.ts index d315b7dbdd7..6cdfdbaba93 100644 --- a/packages/pds/tests/views/admin/invites.test.ts +++ b/packages/pds/tests/admin/invites.test.ts @@ -4,9 +4,9 @@ import { adminAuth, moderatorAuth, TestServerInfo, -} from '../../_util' +} from '../_util' import { randomStr } from '@atproto/crypto' -import { SeedClient } from '../../seeds/client' +import { SeedClient } from '../seeds/client' describe('pds admin invite views', () => { let agent: AtpAgent diff --git a/packages/pds/tests/views/admin/repo-search.test.ts b/packages/pds/tests/admin/repo-search.test.ts similarity index 98% rename from packages/pds/tests/views/admin/repo-search.test.ts rename to packages/pds/tests/admin/repo-search.test.ts index e468a58a663..3f25c893901 100644 --- a/packages/pds/tests/views/admin/repo-search.test.ts +++ b/packages/pds/tests/admin/repo-search.test.ts @@ -6,10 +6,10 @@ import { CloseFn, paginateAll, adminAuth, -} from '../../_util' -import { SeedClient } from '../../seeds/client' -import usersBulkSeed from '../../seeds/users-bulk' -import { Database } from '../../../src' +} from '../_util' +import { SeedClient } from '../seeds/client' +import usersBulkSeed from '../seeds/users-bulk' +import { Database } from '../../src' describe('pds admin repo search view', () => { let agent: AtpAgent diff --git a/packages/pds/tests/algos/hot-classic.test.ts b/packages/pds/tests/algos/hot-classic.test.ts deleted file mode 100644 index 82a65f5c852..00000000000 --- a/packages/pds/tests/algos/hot-classic.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import AtpAgent, { AtUri } from '@atproto/api' -import { runTestServer, TestServerInfo } from '../_util' -import { SeedClient } from '../seeds/client' -import basicSeed from '../seeds/basic' -import { makeAlgos } from '../../src' - -describe.skip('algo hot-classic', () => { - let server: TestServerInfo - let agent: AtpAgent - let sc: SeedClient - - // account dids, for convenience - let alice: string - let bob: string - - const feedPublisherDid = 'did:example:feed-publisher' - const feedUri = AtUri.make( - feedPublisherDid, - 'app.bsky.feed.generator', - 'hot-classic', - ).toString() - - beforeAll(async () => { - server = await runTestServer( - { - dbPostgresSchema: 'algo_hot_classic', - }, - { - algos: makeAlgos(feedPublisherDid), - }, - ) - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await basicSeed(sc) - - alice = sc.dids.alice - bob = sc.dids.bob - await server.processAll() - }) - - afterAll(async () => { - await server.close() - }) - - it('returns well liked posts', async () => { - const img = await sc.uploadFile( - alice, - 'tests/image/fixtures/key-landscape-small.jpg', - 'image/jpeg', - ) - const one = await sc.post(alice, 'first post', undefined, [img]) - const two = await sc.post(bob, 'bobby boi') - const three = await sc.reply(bob, one.ref, one.ref, 'reply') - - for (let i = 0; i < 12; i++) { - const name = `user${i}` - await sc.createAccount(name, { - handle: `user${i}.test`, - email: `user${i}@test.com`, - password: 'password', - }) - await sc.like(sc.dids[name], one.ref) - await sc.like(sc.dids[name], two.ref) - await sc.like(sc.dids[name], three.ref) - } - await server.processAll() - - const res = await agent.api.app.bsky.feed.getFeed( - { feed: feedUri }, - { headers: sc.getHeaders(alice) }, - ) - const feedUris = res.data.feed.map((i) => i.post.uri).sort() - const expected = [one.ref.uriStr, two.ref.uriStr].sort() - expect(feedUris).toEqual(expected) - }) - - it('paginates', async () => { - const res = await agent.api.app.bsky.feed.getFeed( - { feed: feedUri }, - { headers: sc.getHeaders(alice) }, - ) - const first = await agent.api.app.bsky.feed.getFeed( - { feed: feedUri, limit: 1 }, - { headers: sc.getHeaders(alice) }, - ) - const second = await agent.api.app.bsky.feed.getFeed( - { feed: feedUri, cursor: first.data.cursor }, - { headers: sc.getHeaders(alice) }, - ) - - expect([...first.data.feed, ...second.data.feed]).toEqual(res.data.feed) - }) -}) diff --git a/packages/pds/tests/algos/whats-hot.test.ts b/packages/pds/tests/algos/whats-hot.test.ts deleted file mode 100644 index 9ad33621af4..00000000000 --- a/packages/pds/tests/algos/whats-hot.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { HOUR } from '@atproto/common' -import AtpAgent, { AtUri } from '@atproto/api' -import { runTestServer, TestServerInfo } from '../_util' -import { SeedClient } from '../seeds/client' -import basicSeed from '../seeds/basic' -import { makeAlgos } from '../../src' - -describe.skip('algo whats-hot', () => { - let server: TestServerInfo - let agent: AtpAgent - let sc: SeedClient - - // account dids, for convenience - let alice: string - let bob: string - let carol: string - - const feedPublisherDid = 'did:example:feed-publisher' - const feedUri = AtUri.make( - feedPublisherDid, - 'app.bsky.feed.generator', - 'whats-hot', - ).toString() - - beforeAll(async () => { - server = await runTestServer( - { - dbPostgresSchema: 'algo_whats_hot', - }, - { - algos: makeAlgos(feedPublisherDid), - }, - ) - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await basicSeed(sc) - - alice = sc.dids.alice - bob = sc.dids.bob - carol = sc.dids.carol - await server.processAll() - }) - - afterAll(async () => { - await server.close() - }) - - it('returns well liked posts', async () => { - if (server.ctx.db.dialect === 'sqlite') return - - const img = await sc.uploadFile( - alice, - 'tests/image/fixtures/key-landscape-small.jpg', - 'image/jpeg', - ) - const one = await sc.post(carol, 'carol is in the chat') - const two = await sc.post(carol, "it's me, carol") - const three = await sc.post(alice, 'first post', undefined, [img]) - const four = await sc.post(bob, 'bobby boi') - const five = await sc.post(bob, 'another one') - - for (let i = 0; i < 20; i++) { - const name = `user${i}` - await sc.createAccount(name, { - handle: `user${i}.test`, - email: `user${i}@test.com`, - password: 'password', - }) - await sc.like(sc.dids[name], three.ref) // will be down-regulated by time - if (i > 3) { - await sc.like(sc.dids[name], one.ref) - } - if (i > 5) { - await sc.like(sc.dids[name], two.ref) - } - if (i > 7) { - await sc.like(sc.dids[name], four.ref) - await sc.like(sc.dids[name], five.ref) - } - } - await server.processAll() - - // move the 3rd post 5 hours into the past to check gravity - await server.ctx.db.db - .updateTable('post') - .where('uri', '=', three.ref.uriStr) - .set({ indexedAt: new Date(Date.now() - 5 * HOUR).toISOString() }) - .execute() - - await server.ctx.db.refreshMaterializedView('algo_whats_hot_view') - - const res = await agent.api.app.bsky.feed.getFeed( - { feed: feedUri }, - { headers: sc.getHeaders(alice) }, - ) - expect(res.data.feed[0].post.uri).toBe(one.ref.uriStr) - expect(res.data.feed[1].post.uri).toBe(two.ref.uriStr) - const indexOfThird = res.data.feed.findIndex( - (item) => item.post.uri === three.ref.uriStr, - ) - // doesn't quite matter where this cam in but it should be down-regulated pretty severely from gravity - expect(indexOfThird).toBeGreaterThan(3) - }) - - it('paginates', async () => { - if (server.ctx.db.dialect === 'sqlite') return - - const res = await agent.api.app.bsky.feed.getFeed( - { feed: feedUri }, - { headers: sc.getHeaders(alice) }, - ) - const first = await agent.api.app.bsky.feed.getFeed( - { feed: feedUri, limit: 3 }, - { headers: sc.getHeaders(alice) }, - ) - const second = await agent.api.app.bsky.feed.getFeed( - { feed: feedUri, cursor: first.data.cursor }, - { headers: sc.getHeaders(alice) }, - ) - - expect([...first.data.feed, ...second.data.feed]).toEqual(res.data.feed) - }) -}) diff --git a/packages/pds/tests/algos/with-friends.test.ts b/packages/pds/tests/algos/with-friends.test.ts deleted file mode 100644 index 7e6e79357d9..00000000000 --- a/packages/pds/tests/algos/with-friends.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -import AtpAgent, { AtUri } from '@atproto/api' -import { runTestServer, TestServerInfo } from '../_util' -import { RecordRef, SeedClient } from '../seeds/client' -import userSeed from '../seeds/users' -import { makeAlgos } from '../../src' - -describe.skip('algo with friends', () => { - let server: TestServerInfo - let agent: AtpAgent - let sc: SeedClient - - // account dids, for convenience - let alice: string - let bob: string - let carol: string - let dan: string - - const feedPublisherDid = 'did:example:feed-publisher' - const feedUri = AtUri.make( - feedPublisherDid, - 'app.bsky.feed.generator', - 'with-friends', - ).toString() - - beforeAll(async () => { - server = await runTestServer( - { - dbPostgresSchema: 'algo_with_friends', - }, - { - algos: makeAlgos(feedPublisherDid), - }, - ) - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await userSeed(sc) - - alice = sc.dids.alice - bob = sc.dids.bob - carol = sc.dids.carol - dan = sc.dids.dan - - await server.processAll() - }) - - afterAll(async () => { - await server.close() - }) - - let expectedFeed: string[] - - it('setup', async () => { - for (let i = 0; i < 10; i++) { - const name = `user${i}` - await sc.createAccount(name, { - handle: `user${i}.test`, - email: `user${i}@test.com`, - password: 'password', - }) - } - - const hitLikeThreshold = async (ref: RecordRef) => { - for (let i = 0; i < 10; i++) { - const name = `user${i}` - await sc.like(sc.dids[name], ref) - } - } - - // bob and dan are mutuals of alice, all userN are out-of-network. - await sc.follow(alice, bob) - await sc.follow(alice, carol) - await sc.follow(alice, dan) - await sc.follow(bob, alice) - await sc.follow(dan, alice) - const one = await sc.post(bob, 'one') - const two = await sc.post(bob, 'two') - const three = await sc.post(carol, 'three') - const four = await sc.post(carol, 'four') - const five = await sc.post(dan, 'five') - const six = await sc.post(dan, 'six') - const seven = await sc.post(sc.dids.user0, 'seven') - const eight = await sc.post(sc.dids.user0, 'eight') - const nine = await sc.post(sc.dids.user1, 'nine') - const ten = await sc.post(sc.dids.user1, 'ten') - - // 1, 2, 3, 4, 6, 8, 10 hit like threshold - await hitLikeThreshold(one.ref) - await hitLikeThreshold(two.ref) - await hitLikeThreshold(three.ref) - await hitLikeThreshold(four.ref) - await hitLikeThreshold(six.ref) - await hitLikeThreshold(eight.ref) - await hitLikeThreshold(ten.ref) - - // 1, 4, 7, 8, 10 liked by mutual - await sc.like(bob, one.ref) - await sc.like(dan, four.ref) - await sc.like(bob, seven.ref) - await sc.like(dan, eight.ref) - await sc.like(bob, nine.ref) - await sc.like(dan, ten.ref) - - // all liked by non-mutual - await sc.like(carol, one.ref) - await sc.like(carol, two.ref) - await sc.like(carol, three.ref) - await sc.like(carol, four.ref) - await sc.like(carol, five.ref) - await sc.like(carol, six.ref) - await sc.like(carol, seven.ref) - await sc.like(carol, eight.ref) - await sc.like(carol, nine.ref) - await sc.like(carol, ten.ref) - - expectedFeed = [ - ten.ref.uriStr, - eight.ref.uriStr, - four.ref.uriStr, - one.ref.uriStr, - ] - }) - - it('returns popular in & out of network posts', async () => { - const res = await agent.api.app.bsky.feed.getFeed( - { feed: feedUri }, - { headers: sc.getHeaders(alice) }, - ) - const feedUris = res.data.feed.map((i) => i.post.uri) - expect(feedUris).toEqual(expectedFeed) - }) - - it('paginates', async () => { - const res = await agent.api.app.bsky.feed.getFeed( - { feed: feedUri }, - { headers: sc.getHeaders(alice) }, - ) - const first = await agent.api.app.bsky.feed.getFeed( - { feed: feedUri, limit: 2 }, - { headers: sc.getHeaders(alice) }, - ) - const second = await agent.api.app.bsky.feed.getFeed( - { feed: feedUri, cursor: first.data.cursor }, - { headers: sc.getHeaders(alice) }, - ) - - expect([...first.data.feed, ...second.data.feed]).toEqual(res.data.feed) - }) -}) diff --git a/packages/pds/tests/feed-generation.test.ts b/packages/pds/tests/feed-generation.test.ts deleted file mode 100644 index 41b21901ba4..00000000000 --- a/packages/pds/tests/feed-generation.test.ts +++ /dev/null @@ -1,518 +0,0 @@ -import { AtUri, AtpAgent } from '@atproto/api' -import { Handler as SkeletonHandler } from '@atproto/pds/src/lexicon/types/app/bsky/feed/getFeedSkeleton' -import { UnknownFeedError } from '@atproto/api/src/client/types/app/bsky/feed/getFeed' -import { SeedClient } from './seeds/client' -import basicSeed from './seeds/basic' -import { TestNetworkNoAppView } from '@atproto/dev-env' -import { TestFeedGen } from '@atproto/dev-env/src/feed-gen' -import { TID } from '@atproto/common' -import { adminAuth, forSnapshot, paginateAll } from './_util' -import { - FeedViewPost, - GeneratorView, -} from '@atproto/api/src/client/types/app/bsky/feed/defs' -import { SkeletonFeedPost } from '../src/lexicon/types/app/bsky/feed/defs' -import { RecordRef } from './seeds/client' -import { ids } from '../src/lexicon/lexicons' -import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' - -describe('feed generation', () => { - let network: TestNetworkNoAppView - let agent: AtpAgent - let sc: SeedClient - let gen: TestFeedGen - - let alice: string - let feedUriAll: string - let feedUriAllRef: RecordRef - let feedUriEven: string - let feedUriOdd: string // Unsupported by feed gen - let feedUriBadPagination: string - let feedUriPrime: string // Taken-down - let feedUriPrimeRef: RecordRef - - beforeAll(async () => { - network = await TestNetworkNoAppView.create({ - dbPostgresSchema: 'feed_generation', - }) - agent = network.pds.getClient() - sc = new SeedClient(agent) - await basicSeed(sc) - await network.processAll() - alice = sc.dids.alice - const allUri = AtUri.make(alice, 'app.bsky.feed.generator', 'all') - const feedUriBadPagination = AtUri.make( - alice, - 'app.bsky.feed.generator', - 'bad-pagination', - ) - const evenUri = AtUri.make(alice, 'app.bsky.feed.generator', 'even') - const primeUri = AtUri.make(alice, 'app.bsky.feed.generator', 'prime') - gen = await network.createFeedGen({ - [allUri.toString()]: feedGenHandler('all'), - [evenUri.toString()]: feedGenHandler('even'), - [feedUriBadPagination.toString()]: feedGenHandler('bad-pagination'), - [primeUri.toString()]: feedGenHandler('prime'), - }) - }) - - afterAll(async () => { - await network.close() - }) - - it('describes the feed generator', async () => { - const res = await agent.api.app.bsky.feed.describeFeedGenerator() - expect(res.data.did).toBe(network.pds.ctx.cfg.feedGenDid) - }) - - it('feed gen records can be created.', async () => { - const all = await agent.api.app.bsky.feed.generator.create( - { repo: alice, rkey: 'all' }, - { - did: gen.did, - displayName: 'All', - description: 'Provides all feed candidates', - createdAt: new Date().toISOString(), - }, - sc.getHeaders(alice), - ) - const even = await agent.api.app.bsky.feed.generator.create( - { repo: alice, rkey: 'even' }, - { - did: gen.did, - displayName: 'Even', - description: 'Provides even-indexed feed candidates', - createdAt: new Date().toISOString(), - }, - sc.getHeaders(alice), - ) - // Unsupported by feed gen - const odd = await agent.api.app.bsky.feed.generator.create( - { repo: alice, rkey: 'odd' }, - { - did: gen.did, - displayName: 'Temp', // updated in next test - description: 'Temp', // updated in next test - createdAt: new Date().toISOString(), - }, - sc.getHeaders(alice), - ) - const badPagination = await agent.api.app.bsky.feed.generator.create( - { repo: alice, rkey: 'bad-pagination' }, - { - did: gen.did, - displayName: 'Bad Pagination', - description: - 'Provides all feed candidates, blindly ignoring pagination limit', - createdAt: new Date().toISOString(), - }, - sc.getHeaders(alice), - ) - // Taken-down - const prime = await agent.api.app.bsky.feed.generator.create( - { repo: alice, rkey: 'prime' }, - { - did: gen.did, - displayName: 'Prime', - description: 'Provides prime-indexed feed candidates', - createdAt: new Date().toISOString(), - }, - sc.getHeaders(alice), - ) - await agent.api.com.atproto.admin.takeModerationAction( - { - action: TAKEDOWN, - subject: { - $type: 'com.atproto.repo.strongRef', - uri: prime.uri, - cid: prime.cid, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - feedUriAll = all.uri - feedUriAllRef = new RecordRef(all.uri, all.cid) - feedUriEven = even.uri - feedUriOdd = odd.uri - feedUriBadPagination = badPagination.uri - feedUriPrime = prime.uri - feedUriPrimeRef = new RecordRef(prime.uri, prime.cid) - }) - - it('feed gen records can be updated', async () => { - await agent.api.com.atproto.repo.putRecord( - { - repo: alice, - collection: ids.AppBskyFeedGenerator, - rkey: 'odd', - record: { - did: gen.did, - displayName: 'Odd', - description: 'Provides odd-indexed feed candidates', - createdAt: new Date().toISOString(), - }, - }, - { headers: sc.getHeaders(alice), encoding: 'application/json' }, - ) - }) - - it('getActorFeeds fetches feed generators by actor.', async () => { - // add some likes - await sc.like(sc.dids.bob, feedUriAllRef) - await sc.like(sc.dids.carol, feedUriAllRef) - - const results = (results) => results.flatMap((res) => res.feeds) - const paginator = async (cursor?: string) => { - const res = await agent.api.app.bsky.feed.getActorFeeds( - { actor: alice, cursor, limit: 2 }, - { headers: sc.getHeaders(sc.dids.bob) }, - ) - return res.data - } - - const paginatedAll: GeneratorView[] = results(await paginateAll(paginator)) - - expect(paginatedAll.length).toEqual(4) - expect(paginatedAll[0].uri).toEqual(feedUriOdd) - expect(paginatedAll[1].uri).toEqual(feedUriBadPagination) - expect(paginatedAll[2].uri).toEqual(feedUriEven) - expect(paginatedAll[3].uri).toEqual(feedUriAll) - expect(paginatedAll.map((fg) => fg.uri)).not.toContain(feedUriPrime) // taken-down - expect(forSnapshot(paginatedAll)).toMatchSnapshot() - }) - - it('embeds feed generator records in posts', async () => { - const res = await agent.api.app.bsky.feed.post.create( - { repo: sc.dids.bob }, - { - text: 'cool feed!', - embed: { - $type: 'app.bsky.embed.record', - record: feedUriAllRef.raw, - }, - createdAt: new Date().toISOString(), - }, - sc.getHeaders(sc.dids.bob), - ) - const view = await agent.api.app.bsky.feed.getPosts( - { uris: [res.uri] }, - { headers: sc.getHeaders(sc.dids.bob) }, - ) - expect(view.data.posts.length).toBe(1) - expect(forSnapshot(view.data.posts[0])).toMatchSnapshot() - }) - - it('does not embed taken-down feed generator records in posts', async () => { - const res = await agent.api.app.bsky.feed.post.create( - { repo: sc.dids.bob }, - { - text: 'weird feed', - embed: { - $type: 'app.bsky.embed.record', - record: feedUriPrimeRef.raw, - }, - createdAt: new Date().toISOString(), - }, - sc.getHeaders(sc.dids.bob), - ) - const view = await agent.api.app.bsky.feed.getPosts( - { uris: [res.uri] }, - { headers: sc.getHeaders(sc.dids.bob) }, - ) - expect(view.data.posts.length).toBe(1) - expect(forSnapshot(view.data.posts[0])).toMatchSnapshot() - }) - - describe('getFeedGenerator', () => { - it('describes a feed gen & returns online status', async () => { - const resEven = await agent.api.app.bsky.feed.getFeedGenerator( - { feed: feedUriAll }, - { headers: sc.getHeaders(sc.dids.bob) }, - ) - expect(forSnapshot(resEven.data)).toMatchSnapshot() - expect(resEven.data.isOnline).toBe(true) - expect(resEven.data.isValid).toBe(true) - }) - - it('does not describe taken-down feed', async () => { - const tryGetFeed = agent.api.app.bsky.feed.getFeedGenerator( - { feed: feedUriPrime }, - { headers: sc.getHeaders(sc.dids.bob) }, - ) - await expect(tryGetFeed).rejects.toThrow('could not find feed') - }) - - // @TODO temporarily skipping while external feedgens catch-up on describeFeedGenerator - it.skip('handles an unsupported algo', async () => { - const resOdd = await agent.api.app.bsky.feed.getFeedGenerator( - { feed: feedUriOdd }, - { headers: sc.getHeaders(sc.dids.bob) }, - ) - expect(resOdd.data.isOnline).toBe(true) - expect(resOdd.data.isValid).toBe(false) - }) - - // @TODO temporarily skipping while external feedgens catch-up on describeFeedGenerator - it.skip('handles an offline feed', async () => { - // make an invalid feed gen in bob's repo - const allUriBob = AtUri.make( - sc.dids.bob, - 'app.bsky.feed.generator', - 'all', - ) - const bobFg = await network.createFeedGen({ - [allUriBob.toString()]: feedGenHandler('all'), - }) - - await agent.api.app.bsky.feed.generator.create( - { repo: sc.dids.bob, rkey: 'all' }, - { - did: bobFg.did, - displayName: 'All by bob', - description: 'Provides all feed candidates - by bob', - createdAt: new Date().toISOString(), - }, - sc.getHeaders(sc.dids.bob), - ) - - // now take it offline - await bobFg.close() - - const res = await agent.api.app.bsky.feed.getFeedGenerator( - { - feed: allUriBob.toString(), - }, - { headers: sc.getHeaders(sc.dids.alice) }, - ) - expect(res.data.isOnline).toBe(false) - expect(res.data.isValid).toBe(false) - }) - }) - - describe('getFeedGenerators', () => { - it('describes multiple feed gens', async () => { - const resEven = await agent.api.app.bsky.feed.getFeedGenerators( - { feeds: [feedUriEven, feedUriAll, feedUriPrime] }, - { headers: sc.getHeaders(sc.dids.bob) }, - ) - expect(forSnapshot(resEven.data)).toMatchSnapshot() - expect(resEven.data.feeds.map((fg) => fg.uri)).not.toContain(feedUriPrime) // taken-down - }) - }) - - describe('getPopularFeedGenerators', () => { - it('gets popular feed generators', async () => { - const res = await agent.api.app.bsky.unspecced.getPopularFeedGenerators( - {}, - { headers: sc.getHeaders(sc.dids.bob) }, - ) - expect(res.data.feeds.map((f) => f.likeCount)).toEqual([2, 0, 0, 0]) - expect(res.data.feeds.map((f) => f.uri)).not.toContain(feedUriPrime) // taken-down - }) - - it('paginates', async () => { - const resFull = - await agent.api.app.bsky.unspecced.getPopularFeedGenerators( - {}, - { headers: sc.getHeaders(sc.dids.bob) }, - ) - - const resOne = - await agent.api.app.bsky.unspecced.getPopularFeedGenerators( - { limit: 2 }, - { headers: sc.getHeaders(sc.dids.bob) }, - ) - const resTwo = - await agent.api.app.bsky.unspecced.getPopularFeedGenerators( - { cursor: resOne.data.cursor }, - { headers: sc.getHeaders(sc.dids.bob) }, - ) - expect([...resOne.data.feeds, ...resTwo.data.feeds]).toEqual( - resFull.data.feeds, - ) - }) - - it('searches', async () => { - const res = await agent.api.app.bsky.unspecced.getPopularFeedGenerators( - { query: 'pagination' }, - { headers: sc.getHeaders(sc.dids.bob) }, - ) - - expect(res.data.feeds[0].displayName).toBe('Bad Pagination') - }) - }) - - describe('getFeed', () => { - it('resolves basic feed contents.', async () => { - const feed = await agent.api.app.bsky.feed.getFeed( - { feed: feedUriEven }, - { headers: sc.getHeaders(alice) }, - ) - expect(feed.data.feed.map((item) => item.post.uri)).toEqual([ - sc.posts[sc.dids.alice][0].ref.uriStr, - sc.posts[sc.dids.carol][0].ref.uriStr, - sc.replies[sc.dids.carol][0].ref.uriStr, - ]) - expect(forSnapshot(feed.data.feed)).toMatchSnapshot() - }) - - it('paginates, handling replies and reposts.', async () => { - const results = (results) => results.flatMap((res) => res.feed) - const paginator = async (cursor?: string) => { - const res = await agent.api.app.bsky.feed.getFeed( - { feed: feedUriAll, cursor, limit: 2 }, - { headers: sc.getHeaders(alice) }, - ) - return res.data - } - - const paginatedAll: FeedViewPost[] = results(await paginateAll(paginator)) - - // Unknown post uri is omitted - expect(paginatedAll.map((item) => item.post.uri)).toEqual([ - sc.posts[sc.dids.alice][0].ref.uriStr, - sc.posts[sc.dids.bob][0].ref.uriStr, - sc.posts[sc.dids.carol][0].ref.uriStr, - sc.replies[sc.dids.carol][0].ref.uriStr, - sc.posts[sc.dids.dan][1].ref.uriStr, - ]) - expect(forSnapshot(paginatedAll)).toMatchSnapshot() - }) - - it('paginates, handling feed not respecting limit.', async () => { - const res = await agent.api.app.bsky.feed.getFeed( - { feed: feedUriBadPagination, limit: 3 }, - { headers: sc.getHeaders(alice) }, - ) - // refused to respect pagination limit, so it got cut short by appview but the cursor remains. - expect(res.data.feed.length).toBeLessThanOrEqual(3) - expect(parseInt(res.data.cursor || '', 10)).toBeGreaterThanOrEqual(3) - expect(res.data.feed.map((item) => item.post.uri)).toEqual([ - sc.posts[sc.dids.alice][0].ref.uriStr, - sc.posts[sc.dids.bob][0].ref.uriStr, - sc.posts[sc.dids.carol][0].ref.uriStr, - ]) - }) - - it('fails on unknown feed.', async () => { - const tryGetFeed = agent.api.app.bsky.feed.getFeed( - { feed: feedUriOdd }, - { headers: sc.getHeaders(alice) }, - ) - await expect(tryGetFeed).rejects.toThrow(UnknownFeedError) - }) - - it('resolves contents of taken-down feed.', async () => { - const tryGetFeed = agent.api.app.bsky.feed.getFeed( - { feed: feedUriPrime }, - { headers: sc.getHeaders(alice) }, - ) - await expect(tryGetFeed).resolves.toBeDefined() - }) - - it('receives proper auth details.', async () => { - const feed = await agent.api.app.bsky.feed.getFeed( - { feed: feedUriEven }, - { headers: sc.getHeaders(alice) }, - ) - expect(feed.data['$auth']?.['aud']).toEqual(gen.did) - expect(feed.data['$auth']?.['iss']).toEqual(alice) - }) - - it('receives proper auth details.', async () => { - const feed = await agent.api.app.bsky.feed.getFeed( - { feed: feedUriEven }, - { headers: sc.getHeaders(alice) }, - ) - expect(feed.data['$auth']?.['aud']).toEqual(gen.did) - expect(feed.data['$auth']?.['iss']).toEqual(alice) - }) - - it('provides timing info in server-timing header.', async () => { - const result = await agent.api.app.bsky.feed.getFeed( - { feed: feedUriEven }, - { headers: sc.getHeaders(alice) }, - ) - expect(result.headers['server-timing']).toMatch( - /^skele;dur=\d+, hydr;dur=\d+$/, - ) - }) - - it('returns an upstream failure error when the feed is down.', async () => { - await gen.close() // @NOTE must be last test - const tryGetFeed = agent.api.app.bsky.feed.getFeed( - { feed: feedUriEven }, - { headers: sc.getHeaders(alice) }, - ) - await expect(tryGetFeed).rejects.toThrow('feed unavailable') - }) - }) - - const feedGenHandler = - (feedName: 'even' | 'all' | 'prime' | 'bad-pagination'): SkeletonHandler => - async ({ req, params }) => { - const { limit, cursor } = params - const candidates: SkeletonFeedPost[] = [ - { post: sc.posts[sc.dids.alice][0].ref.uriStr }, - { post: sc.posts[sc.dids.bob][0].ref.uriStr }, - { post: sc.posts[sc.dids.carol][0].ref.uriStr }, - { post: `at://did:plc:unknown/app.bsky.feed.post/${TID.nextStr()}` }, // Doesn't exist - { post: sc.replies[sc.dids.carol][0].ref.uriStr }, // Reply - // Repost (accurate) - { - post: sc.posts[sc.dids.dan][1].ref.uriStr, - reason: { - $type: 'app.bsky.feed.defs#skeletonReasonRepost', - repost: sc.reposts[sc.dids.carol][0].uriStr, - }, - }, - // Repost (inaccurate) - { - post: sc.posts[alice][1].ref.uriStr, - reason: { - $type: 'app.bsky.feed.defs#skeletonReasonRepost', - repost: sc.reposts[sc.dids.carol][0].uriStr, - }, - }, - ] - const offset = cursor ? parseInt(cursor, 10) : 0 - const fullFeed = candidates.filter((_, i) => { - if (feedName === 'even') { - return i % 2 === 0 - } - if (feedName === 'prime') { - return [2, 3, 5, 7, 11, 13].includes(i) - } - return true - }) - const feedResults = - feedName === 'bad-pagination' - ? fullFeed.slice(offset) // does not respect limit - : fullFeed.slice(offset, offset + limit) - const lastResult = feedResults.at(-1) - return { - encoding: 'application/json', - body: { - feed: feedResults, - cursor: lastResult - ? (fullFeed.indexOf(lastResult) + 1).toString() - : undefined, - $auth: jwtBody(req.headers.authorization), // for testing purposes - }, - } - } -}) - -const jwtBody = (authHeader?: string): Record | undefined => { - if (!authHeader?.startsWith('Bearer')) return undefined - const jwt = authHeader.replace('Bearer ', '') - const [, bodyb64] = jwt.split('.') - const body = JSON.parse(Buffer.from(bodyb64, 'base64').toString()) - if (!body || typeof body !== 'object') return undefined - return body -} diff --git a/packages/pds/tests/file-uploads.test.ts b/packages/pds/tests/file-uploads.test.ts index fa36a4fb473..cd26c7fa0a8 100644 --- a/packages/pds/tests/file-uploads.test.ts +++ b/packages/pds/tests/file-uploads.test.ts @@ -5,8 +5,6 @@ import { CloseFn, runTestServer, TestServerInfo } from './_util' import { Database, ServerConfig } from '../src' import DiskBlobStore from '../src/storage/disk-blobstore' import * as uint8arrays from 'uint8arrays' -import * as image from '../src/image' -import axios from 'axios' import { randomBytes } from '@atproto/crypto' import { BlobRef } from '@atproto/lexicon' import { ids } from '../src/lexicon/lexicons' @@ -147,24 +145,6 @@ describe('file uploads', () => { expect(uint8arrays.equals(smallFile, new Uint8Array(data))).toBeTruthy() }) - it('serves the referenced blob', async () => { - const profile = await aliceAgent.api.app.bsky.actor.getProfile({ - actor: 'alice.test', - }) - const avatar = profile.data.avatar as string - expect(typeof avatar).toBe('string') - const url = avatar.replace(cfg.publicUrl, serverUrl) - const res = await axios.get(url, { responseType: 'stream' }) - expect(res.headers['content-type']).toBe('image/jpeg') - const info = await image.getInfo(res.data) - expect(info).toEqual( - expect.objectContaining({ - height: 1000, - width: 1000, - }), - ) - }) - let largeBlob: BlobRef let largeFile: Uint8Array diff --git a/packages/pds/tests/handles.test.ts b/packages/pds/tests/handles.test.ts index b0827b2e953..d6b6fd60caa 100644 --- a/packages/pds/tests/handles.test.ts +++ b/packages/pds/tests/handles.test.ts @@ -50,6 +50,15 @@ describe('handles', () => { await close() }) + const getHandleFromDb = async (did: string): Promise => { + const res = await ctx.db.db + .selectFrom('did_handle') + .selectAll() + .where('did', '=', did) + .executeTakeFirst() + return res?.handle + } + it('resolves handles', async () => { const res = await agent.api.com.atproto.identity.resolveHandle({ handle: 'alice.test', @@ -93,35 +102,6 @@ describe('handles', () => { sc.accounts[alice].refreshJwt = res.data.refreshJwt }) - it('returns the correct handle in views', async () => { - const profile = await agent.api.app.bsky.actor.getProfile( - { actor: alice }, - { headers: sc.getHeaders(bob) }, - ) - expect(profile.data.handle).toBe(newHandle) - - const timeline = await agent.api.app.bsky.feed.getTimeline( - {}, - { headers: sc.getHeaders(bob) }, - ) - - const alicePosts = timeline.data.feed.filter( - (post) => post.post.author.did === alice, - ) - for (const post of alicePosts) { - expect(post.post.author.handle).toBe(newHandle) - } - - const followers = await agent.api.app.bsky.graph.getFollowers( - { actor: bob }, - { headers: sc.getHeaders(bob) }, - ) - - const aliceFollows = followers.data.followers.filter((f) => f.did === alice) - expect(aliceFollows.length).toBe(1) - expect(aliceFollows[0].handle).toBe(newHandle) - }) - it('does not allow taking a handle that already exists', async () => { const attempt = agent.api.com.atproto.identity.updateHandle( { handle: 'Bob.test' }, @@ -194,11 +174,8 @@ describe('handles', () => { }, { headers: sc.getHeaders(alice), encoding: 'application/json' }, ) - const profile = await agent.api.app.bsky.actor.getProfile( - { actor: alice }, - { headers: sc.getHeaders(bob) }, - ) - expect(profile.data.handle).toBe('alice.external') + const dbHandle = await getHandleFromDb(alice) + expect(dbHandle).toBe('alice.external') const data = await idResolver.did.resolveAtprotoData(alice) expect(data.handle).toBe('alice.external') @@ -225,11 +202,8 @@ describe('handles', () => { 'External handle did not resolve to DID', ) - const profile = await agent.api.app.bsky.actor.getProfile( - { actor: alice }, - { headers: sc.getHeaders(bob) }, - ) - expect(profile.data.handle).toBe('alice.external') + const dbHandle = await getHandleFromDb(alice) + expect(dbHandle).toBe('alice.external') }) it('allows admin overrules of service domains', async () => { @@ -244,11 +218,8 @@ describe('handles', () => { }, ) - const profile = await agent.api.app.bsky.actor.getProfile( - { actor: bob }, - { headers: sc.getHeaders(bob) }, - ) - expect(profile.data.handle).toBe('bob-alt.test') + const dbHandle = await getHandleFromDb(bob) + expect(dbHandle).toBe('bob-alt.test') }) it('allows admin override of reserved domains', async () => { @@ -263,11 +234,8 @@ describe('handles', () => { }, ) - const profile = await agent.api.app.bsky.actor.getProfile( - { actor: bob }, - { headers: sc.getHeaders(bob) }, - ) - expect(profile.data.handle).toBe('dril.test') + const dbHandle = await getHandleFromDb(bob) + expect(dbHandle).toBe('dril.test') }) it('requires admin auth', async () => { diff --git a/packages/pds/tests/image/server.test.ts b/packages/pds/tests/image/server.test.ts deleted file mode 100644 index 76dc7f57978..00000000000 --- a/packages/pds/tests/image/server.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import * as http from 'http' -import os from 'os' -import path from 'path' -import fs from 'fs' -import { AddressInfo } from 'net' -import axios, { AxiosInstance } from 'axios' -import { getInfo } from '../../src/image/sharp' -import { BlobDiskCache, ImageProcessingServer } from '../../src/image/server' -import { DiskBlobStore } from '../../src' -import { cidForCbor } from '@atproto/common' -import { CID } from 'multiformats/cid' - -describe('image processing server', () => { - let server: ImageProcessingServer - let httpServer: http.Server - let client: AxiosInstance - - let fileCid: CID - - beforeAll(async () => { - const salt = '9dd04221f5755bce5f55f47464c27e1e' - const key = - 'f23ecd142835025f42c3db2cf25dd813956c178392760256211f9d315f8ab4d8' - const storage = await DiskBlobStore.create( - path.join(os.tmpdir(), 'img-processing-tests'), - ) - // this CID isn't accurate for the data, but it works for the sake of the test - fileCid = await cidForCbor('key-landscape-small') - await storage.putPermanent( - fileCid, - fs.createReadStream('tests/image/fixtures/key-landscape-small.jpg'), - ) - const cache = new BlobDiskCache() - server = new ImageProcessingServer(salt, key, storage, cache) - httpServer = server.app.listen() - const { port } = httpServer.address() as AddressInfo - client = axios.create({ - baseURL: `http://localhost:${port}`, - validateStatus: () => true, - }) - }) - - afterAll(async () => { - if (httpServer) httpServer.close() - if (server) await server.cache.clearAll() - }) - - it('processes image from storage.', async () => { - const res = await client.get( - server.uriBuilder.getSignedPath({ - cid: fileCid, - format: 'jpeg', - fit: 'cover', - width: 500, - height: 500, - min: true, - }), - { responseType: 'stream' }, - ) - - const info = await getInfo(res.data) - expect(info).toEqual( - expect.objectContaining({ - height: 500, - width: 500, - size: 67221, - }), - ) - expect(res.headers).toEqual( - expect.objectContaining({ - 'content-type': 'image/jpeg', - 'cache-control': 'public, max-age=31536000', - 'content-length': '67221', - }), - ) - }) - - it('caches results.', async () => { - const path = server.uriBuilder.getSignedPath({ - cid: fileCid, - format: 'jpeg', - width: 25, // Special number for this test - height: 25, - }) - const res1 = await client.get(path, { responseType: 'arraybuffer' }) - expect(res1.headers['x-cache']).toEqual('miss') - const res2 = await client.get(path, { responseType: 'arraybuffer' }) - expect(res2.headers['x-cache']).toEqual('hit') - const res3 = await client.get(path, { responseType: 'arraybuffer' }) - expect(res3.headers['x-cache']).toEqual('hit') - expect(Buffer.compare(res1.data, res2.data)).toEqual(0) - expect(Buffer.compare(res1.data, res3.data)).toEqual(0) - }) - - it('errors on bad signature.', async () => { - const path = server.uriBuilder.getSignedPath({ - cid: fileCid, - format: 'jpeg', - fit: 'cover', - width: 500, - height: 500, - min: true, - }) - expect(path).toEqual( - `/G37yf764s6331dxOWiaOYEiLdg8OJxeE-RNxPDKB9Ck/rs:fill:500:500:1:0/plain/${fileCid.toString()}@jpeg`, - ) - const res = await client.get(path.replace('/G', '/bad_'), {}) - expect(res.status).toEqual(400) - expect(res.data).toEqual({ message: 'Invalid path: bad signature' }) - }) - - it('errors on missing file.', async () => { - const missingCid = await cidForCbor('missing-file') - const res = await client.get( - server.uriBuilder.getSignedPath({ - cid: missingCid, - format: 'jpeg', - fit: 'cover', - width: 500, - height: 500, - min: true, - }), - ) - expect(res.status).toEqual(404) - expect(res.data).toEqual({ message: 'Image not found' }) - }) -}) diff --git a/packages/pds/tests/image/sharp.test.ts b/packages/pds/tests/image/sharp.test.ts deleted file mode 100644 index d0a46b662b3..00000000000 --- a/packages/pds/tests/image/sharp.test.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { createReadStream } from 'fs' -import { Options, getInfo, resize } from '../../src/image/sharp' - -describe('sharp image processor', () => { - it('scales up to cover.', async () => { - const result = await processFixture('key-landscape-small.jpg', { - format: 'jpeg', - fit: 'cover', - width: 500, - height: 500, - min: true, - }) - expect(result).toEqual( - expect.objectContaining({ - height: 500, - width: 500, - }), - ) - }) - - it('scales up to inside (landscape).', async () => { - const result = await processFixture('key-landscape-small.jpg', { - format: 'jpeg', - fit: 'inside', - width: 500, - height: 500, - min: true, - }) - expect(result).toEqual( - expect.objectContaining({ - height: 290, - width: 500, - }), - ) - }) - - it('scales up to inside (portrait).', async () => { - const result = await processFixture('key-portrait-small.jpg', { - format: 'jpeg', - fit: 'inside', - width: 500, - height: 500, - min: true, - }) - expect(result).toEqual( - expect.objectContaining({ - height: 500, - width: 290, - }), - ) - }) - - it('scales up to min.', async () => { - const result = await processFixture('key-landscape-small.jpg', { - format: 'jpeg', - width: 500, - height: 500, - min: { height: 200, width: 200 }, - }) - expect(result).toEqual( - expect.objectContaining({ - height: 200, - width: 345, - }), - ) - }) - - it('does not scale image up when min is false.', async () => { - const result = await processFixture('key-landscape-small.jpg', { - format: 'jpeg', - width: 500, - height: 500, - min: false, - }) - expect(result).toEqual( - expect.objectContaining({ - height: 87, - width: 150, - mime: 'image/jpeg', - }), - ) - }) - - it('scales down to cover.', async () => { - const result = await processFixture('key-landscape-large.jpg', { - format: 'jpeg', - fit: 'cover', - width: 500, - height: 500, - }) - expect(result).toEqual( - expect.objectContaining({ - height: 500, - width: 500, - }), - ) - }) - - it('scales down to inside (landscape).', async () => { - const result = await processFixture('key-landscape-large.jpg', { - format: 'jpeg', - fit: 'inside', - width: 500, - height: 500, - }) - expect(result).toEqual( - expect.objectContaining({ - height: 290, - width: 500, - }), - ) - }) - - it('scales down to inside (portrait).', async () => { - const result = await processFixture('key-portrait-large.jpg', { - format: 'jpeg', - fit: 'inside', - width: 500, - height: 500, - }) - expect(result).toEqual( - expect.objectContaining({ - height: 500, - width: 290, - }), - ) - }) - - it('converts jpeg to png.', async () => { - const result = await processFixture('key-landscape-small.jpg', { - format: 'png', - width: 500, - height: 500, - min: false, - }) - expect(result).toEqual( - expect.objectContaining({ - height: 87, - width: 150, - size: expect.any(Number), - mime: 'image/png', - }), - ) - }) - - it('controls quality (jpeg).', async () => { - const high = await processFixture('key-portrait-small.jpg', { - format: 'jpeg', - width: 500, - height: 500, - quality: 90, - }) - const low = await processFixture('key-portrait-small.jpg', { - format: 'jpeg', - width: 500, - height: 500, - quality: 10, - }) - expect(high.size).toBeGreaterThan(1000) - expect(low.size).toBeLessThan(1000) - }) - - it('controls quality (png).', async () => { - const high = await processFixture('key-portrait-small.jpg', { - format: 'png', - width: 500, - height: 500, - quality: 80, - }) - const low = await processFixture('key-portrait-small.jpg', { - format: 'png', - width: 500, - height: 500, - quality: 10, - }) - expect(high.size).toBeGreaterThan(3000) - expect(low.size).toBeLessThan(3000) - }) - - async function processFixture(fixture: string, options: Options) { - const image = createReadStream(`${__dirname}/fixtures/${fixture}`) - const resized = await resize(image, options) - return await getInfo(resized) - } -}) diff --git a/packages/pds/tests/image/uri.test.ts b/packages/pds/tests/image/uri.test.ts deleted file mode 100644 index ddf5468fe51..00000000000 --- a/packages/pds/tests/image/uri.test.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { cidForCbor } from '@atproto/common' -import { CID } from 'multiformats/cid' -import { ImageUriBuilder, BadPathError } from '../../src/image/uri' - -describe('image uri builder', () => { - let uriBuilder: ImageUriBuilder - let cid: CID - - beforeAll(async () => { - const endpoint = 'https://example.com' - const salt = '9dd04221f5755bce5f55f47464c27e1e' - const key = - 'f23ecd142835025f42c3db2cf25dd813956c178392760256211f9d315f8ab4d8' - uriBuilder = new ImageUriBuilder(endpoint, salt, key) - cid = await cidForCbor('test cid') - }) - - it('signs and verifies uri options.', () => { - const path = uriBuilder.getSignedPath({ - cid, - format: 'png', - height: 200, - width: 300, - }) - expect(path).toEqual( - `/8Lpp5Y4ZQFkwTxDDgc1hz8haG6-lUBHsGsyNYoDEaXc/rs:fill:300:200:0:0/plain/${cid.toString()}@png`, - ) - expect(uriBuilder.getVerifiedOptions(path)).toEqual({ - signature: '8Lpp5Y4ZQFkwTxDDgc1hz8haG6-lUBHsGsyNYoDEaXc', - cid, - format: 'png', - fit: 'cover', - height: 200, - width: 300, - min: false, - }) - }) - - it('errors on bad signature.', () => { - const tryGetVerifiedOptions = (path) => () => - uriBuilder.getVerifiedOptions(path) - - tryGetVerifiedOptions( - // Confirm this is a good signed uri - `/BtHM_4IOak5MOc2gOPDxbfS4_HG6VPcry2OAV03L29g/rs:fill:300:200:0:0/plain/${cid.toString()}@png`, - ) - - expect( - tryGetVerifiedOptions( - // Tamper with signature - `/DtHM_4IOak5MOc2gOPDxbfS4_HG6VPcry2OAV03L29g/rs:fill:300:200:0:0/plain/${cid.toString()}@png`, - ), - ).toThrow(new BadPathError('Invalid path: bad signature')) - - expect( - tryGetVerifiedOptions( - // Tamper with params - `/DtHM_4IOak5MOc2gOPDxbfS4_HG6VPcry2OAV03L29g/rs:fill:300:200:0:0/plain/${cid.toString()}@jpeg`, - ), - ).toThrow(new BadPathError('Invalid path: bad signature')) - - expect( - tryGetVerifiedOptions( - // Missing signature - `/rs:fill:300:200:0:0/plain/${cid.toString()}@jpeg`, - ), - ).toThrow(new BadPathError('Invalid path: missing signature')) - }) - - it('supports basic options.', () => { - const path = ImageUriBuilder.getPath({ - cid, - format: 'png', - height: 200, - width: 300, - }) - expect(path).toEqual(`/rs:fill:300:200:0:0/plain/${cid.toString()}@png`) - expect(ImageUriBuilder.getOptions(path)).toEqual({ - cid, - format: 'png', - fit: 'cover', - height: 200, - width: 300, - min: false, - }) - }) - - it('supports fit option.', () => { - const path = ImageUriBuilder.getPath({ - cid, - format: 'png', - fit: 'inside', - height: 200, - width: 300, - }) - expect(path).toEqual(`/rs:fit:300:200:0:0/plain/${cid.toString()}@png`) - expect(ImageUriBuilder.getOptions(path)).toEqual({ - cid, - format: 'png', - fit: 'inside', - height: 200, - width: 300, - min: false, - }) - }) - - it('supports min=true option.', () => { - const path = ImageUriBuilder.getPath({ - cid, - format: 'png', - height: 200, - width: 300, - min: true, - }) - expect(path).toEqual(`/rs:fill:300:200:1:0/plain/${cid.toString()}@png`) - expect(ImageUriBuilder.getOptions(path)).toEqual({ - cid, - format: 'png', - fit: 'cover', - height: 200, - width: 300, - min: true, - }) - }) - - it('supports min={height,width} option.', () => { - const path = ImageUriBuilder.getPath({ - cid, - format: 'jpeg', - height: 200, - width: 300, - min: { height: 50, width: 100 }, - }) - expect(path).toEqual( - `/rs:fill:300:200:0:0/mw:100/mh:50/plain/${cid.toString()}@jpeg`, - ) - expect(ImageUriBuilder.getOptions(path)).toEqual({ - cid, - format: 'jpeg', - fit: 'cover', - height: 200, - width: 300, - min: { height: 50, width: 100 }, - }) - }) - - it('errors on bad cid/format part.', () => { - expect( - tryGetOptions(`/rs:fill:300:200:1:0/plain/${cid.toString()}@mp4`), - ).toThrow(new BadPathError('Invalid path: bad cid/format part')) - expect(tryGetOptions(`/rs:fill:300:200:1:0/plain/@jpg`)).toThrow( - new BadPathError('Invalid path: bad cid/format part'), - ) - expect( - tryGetOptions(`/rs:fill:300:200:1:0/plain/${cid.toString()}@`), - ).toThrow(new BadPathError('Invalid path: bad cid/format part')) - expect( - tryGetOptions(`/rs:fill:300:200:1:0/plain/${cid.toString()}@`), - ).toThrow(new BadPathError('Invalid path: bad cid/format part')) - expect( - tryGetOptions(`/rs:fill:300:200:1:0/plain/${cid.toString()}@x@jpeg`), - ).toThrow(new BadPathError('Invalid path: bad cid/format part')) - }) - - it('errors on mismatching min settings.', () => { - expect( - tryGetOptions( - `/rs:fill:300:200:1:0/mw:100/mh:50/plain/${cid.toString()}@jpeg`, - ), - ).toThrow(new BadPathError('Invalid path: bad min width/height param')) - expect( - tryGetOptions(`/rs:fill:300:200:0:0/mw:100/plain/${cid.toString()}@jpeg`), - ).toThrow(new BadPathError('Invalid path: bad min width/height param')) - }) - - it('errors on bad fit setting.', () => { - expect( - tryGetOptions(`/rs:blah:300:200:1:0/plain/${cid.toString()}@jpeg`), - ).toThrow(new BadPathError('Invalid path: bad resize fit param')) - }) - - it('errors on bad dimension settings.', () => { - expect( - tryGetOptions(`/rs:fill:30x:200:1:0/plain/${cid.toString()}@jpeg`), - ).toThrow(new BadPathError('Invalid path: bad resize height/width param')) - expect( - tryGetOptions(`/rs:fill:300:20x:1:0/plain/${cid.toString()}@jpeg`), - ).toThrow(new BadPathError('Invalid path: bad resize height/width param')) - expect( - tryGetOptions( - `/rs:fill:300:200:1:0/mw:10x/mh:50/plain/${cid.toString()}@jpeg`, - ), - ).toThrow(new BadPathError('Invalid path: bad min width/height param')) - expect( - tryGetOptions( - `/rs:fill:300:200:1:0/mw:100/mh:5x/plain/${cid.toString()}@jpeg`, - ), - ).toThrow(new BadPathError('Invalid path: bad min width/height param')) - }) - - function tryGetOptions(path: string) { - return () => ImageUriBuilder.getOptions(path) - } -}) diff --git a/packages/pds/tests/indexing.test.ts b/packages/pds/tests/indexing.test.ts deleted file mode 100644 index a99053a07c7..00000000000 --- a/packages/pds/tests/indexing.test.ts +++ /dev/null @@ -1,299 +0,0 @@ -import AtpAgent, { - AppBskyActorProfile, - AppBskyFeedPost, - AppBskyFeedLike, - AppBskyFeedRepost, -} from '@atproto/api' -import { AtUri } from '@atproto/syntax' -import { CloseFn, forSnapshot, runTestServer, TestServerInfo } from './_util' -import { SeedClient } from './seeds/client' -import usersSeed from './seeds/users' -import { Database } from '../src' -import { prepareCreate, prepareDelete, prepareUpdate } from '../src/repo' -import { ids } from '../src/lexicon/lexicons' - -describe('indexing', () => { - let server: TestServerInfo - let close: CloseFn - let agent: AtpAgent - let sc: SeedClient - - beforeAll(async () => { - server = await runTestServer({ - dbPostgresSchema: 'indexing', - }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await usersSeed(sc) - }) - - afterAll(async () => { - await close() - }) - - it('indexes posts.', async () => { - const { db, services } = server.ctx - const createdAt = new Date().toISOString() - const createRecord = await prepareCreate({ - did: sc.dids.alice, - collection: ids.AppBskyFeedPost, - record: { - $type: ids.AppBskyFeedPost, - text: '@bob.test how are you?', - facets: [ - { - index: { byteStart: 0, byteEnd: 9 }, - features: [ - { - $type: `${ids.AppBskyRichtextFacet}#mention`, - did: sc.dids.bob, - }, - ], - }, - ], - createdAt, - } as AppBskyFeedPost.Record, - }) - const { uri } = createRecord - const deleteRecord = prepareDelete({ - did: sc.dids.alice, - collection: ids.AppBskyFeedPost, - rkey: uri.rkey, - }) - - // Create - await services - .repo(db) - .processWrites({ did: sc.dids.alice, writes: [createRecord] }, 1) - await server.processAll() - - const getAfterCreate = await agent.api.app.bsky.feed.getPostThread( - { uri: uri.toString() }, - { headers: sc.getHeaders(sc.dids.alice) }, - ) - expect(forSnapshot(getAfterCreate.data)).toMatchSnapshot() - const createNotifications = await getNotifications(db, uri) - - // Delete - await services - .repo(db) - .processWrites({ did: sc.dids.alice, writes: [deleteRecord] }, 1) - await server.processAll() - - const getAfterDelete = agent.api.app.bsky.feed.getPostThread( - { uri: uri.toString() }, - { headers: sc.getHeaders(sc.dids.alice) }, - ) - await expect(getAfterDelete).rejects.toThrow(/Post not found:/) - const deleteNotifications = await getNotifications(db, uri) - - expect( - forSnapshot({ - createNotifications, - deleteNotifications, - }), - ).toMatchSnapshot() - }) - - it('indexes profiles.', async () => { - const { db, services } = server.ctx - const createRecord = await prepareCreate({ - did: sc.dids.dan, - collection: ids.AppBskyActorProfile, - rkey: 'self', - record: { - $type: ids.AppBskyActorProfile, - displayName: 'dan', - } as AppBskyActorProfile.Record, - }) - const { uri } = createRecord - const updateRecord = await prepareUpdate({ - did: sc.dids.dan, - collection: ids.AppBskyActorProfile, - rkey: uri.rkey, - record: { - $type: ids.AppBskyActorProfile, - displayName: 'danny', - } as AppBskyActorProfile.Record, - }) - const deleteRecord = prepareDelete({ - did: sc.dids.dan, - collection: ids.AppBskyActorProfile, - rkey: uri.rkey, - }) - - // Create - await services - .repo(db) - .processWrites({ did: sc.dids.dan, writes: [createRecord] }, 1) - await server.processAll() - - const getAfterCreate = await agent.api.app.bsky.actor.getProfile( - { actor: sc.dids.dan }, - { headers: sc.getHeaders(sc.dids.alice) }, - ) - expect(forSnapshot(getAfterCreate.data)).toMatchSnapshot() - const createNotifications = await getNotifications(db, uri) - - // Update - await services - .repo(db) - .processWrites({ did: sc.dids.dan, writes: [updateRecord] }, 1) - await server.processAll() - - const getAfterUpdate = await agent.api.app.bsky.actor.getProfile( - { actor: sc.dids.dan }, - { headers: sc.getHeaders(sc.dids.alice) }, - ) - expect(forSnapshot(getAfterUpdate.data)).toMatchSnapshot() - const updateNotifications = await getNotifications(db, uri) - - // Delete - await services - .repo(db) - .processWrites({ did: sc.dids.dan, writes: [deleteRecord] }, 1) - await server.processAll() - - const getAfterDelete = await agent.api.app.bsky.actor.getProfile( - { actor: sc.dids.dan }, - { headers: sc.getHeaders(sc.dids.alice) }, - ) - expect(forSnapshot(getAfterDelete.data)).toMatchSnapshot() - const deleteNotifications = await getNotifications(db, uri) - - expect( - forSnapshot({ - createNotifications, - updateNotifications, - deleteNotifications, - }), - ).toMatchSnapshot() - }) - - it('does not notify user of own like or repost', async () => { - const { db, services } = server.ctx - const createdAt = new Date().toISOString() - - const originalPost = await prepareCreate({ - did: sc.dids.bob, - collection: ids.AppBskyFeedPost, - record: { - $type: ids.AppBskyFeedPost, - text: 'original post', - createdAt, - } as AppBskyFeedPost.Record, - }) - - const originalPostRef = { - uri: originalPost.uri.toString(), - cid: originalPost.cid.toString(), - } - - // own actions - const ownLike = await prepareCreate({ - did: sc.dids.bob, - collection: ids.AppBskyFeedLike, - record: { - $type: ids.AppBskyFeedLike, - subject: originalPostRef, - createdAt, - } as AppBskyFeedLike.Record, - }) - const ownRepost = await prepareCreate({ - did: sc.dids.bob, - collection: ids.AppBskyFeedRepost, - record: { - $type: ids.AppBskyFeedRepost, - subject: originalPostRef, - createdAt, - } as AppBskyFeedRepost.Record, - }) - - // other actions - const aliceLike = await prepareCreate({ - did: sc.dids.alice, - collection: ids.AppBskyFeedLike, - record: { - $type: ids.AppBskyFeedLike, - subject: originalPostRef, - createdAt, - } as AppBskyFeedLike.Record, - }) - const aliceRepost = await prepareCreate({ - did: sc.dids.alice, - collection: ids.AppBskyFeedRepost, - record: { - $type: ids.AppBskyFeedRepost, - subject: originalPostRef, - createdAt, - } as AppBskyFeedRepost.Record, - }) - - await services.repo(db).processWrites( - { - did: sc.dids.bob, - writes: [originalPost, ownLike, ownRepost], - }, - 1, - ) - await services.repo(db).processWrites( - { - did: sc.dids.alice, - writes: [aliceLike, aliceRepost], - }, - 1, - ) - - await server.processAll() - - const { - data: { notifications }, - } = await agent.api.app.bsky.notification.listNotifications( - {}, - { headers: sc.getHeaders(sc.dids.bob) }, - ) - - expect(notifications).toHaveLength(2) - expect( - notifications.every((n) => { - return n.author.did !== sc.dids.bob - }), - ).toBeTruthy() - - // Cleanup - const del = (uri: AtUri) => { - return prepareDelete({ - did: uri.host, - collection: uri.collection, - rkey: uri.rkey, - }) - } - - // Delete - await services.repo(db).processWrites( - { - did: sc.dids.bob, - writes: [del(originalPost.uri), del(ownLike.uri), del(ownRepost.uri)], - }, - 1, - ) - await services.repo(db).processWrites( - { - did: sc.dids.alice, - writes: [del(aliceLike.uri), del(aliceRepost.uri)], - }, - 1, - ) - await server.processAll() - }) - - async function getNotifications(db: Database, uri: AtUri) { - return await db.db - .selectFrom('user_notification') - .selectAll() - .where('recordUri', '=', uri.toString()) - .orderBy('indexedAt') - .execute() - } -}) diff --git a/packages/pds/tests/labeler/apply-labels.test.ts b/packages/pds/tests/labeler/apply-labels.test.ts index 756fd2c13fd..80fedc459d5 100644 --- a/packages/pds/tests/labeler/apply-labels.test.ts +++ b/packages/pds/tests/labeler/apply-labels.test.ts @@ -1,32 +1,25 @@ import AtpAgent from '@atproto/api' -import { - adminAuth, - CloseFn, - moderatorAuth, - runTestServer, - TestServerInfo, -} from '../_util' +import { TestNetworkNoAppView } from '@atproto/dev-env' +import { adminAuth, moderatorAuth } from '../_util' import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' describe('unspecced.applyLabels', () => { - let server: TestServerInfo - let close: CloseFn + let network: TestNetworkNoAppView let agent: AtpAgent let sc: SeedClient beforeAll(async () => { - server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'apply_labels', }) - close = server.close - agent = new AtpAgent({ service: server.url }) + agent = network.pds.getClient() sc = new SeedClient(agent) await basicSeed(sc) }) afterAll(async () => { - await close() + await network.close() }) it('requires admin auth.', async () => { @@ -34,7 +27,7 @@ describe('unspecced.applyLabels', () => { { labels: [ { - src: server.ctx.cfg.labelerDid, + src: network.pds.ctx.cfg.labelerDid, uri: sc.dids.carol, val: 'cats', neg: false, @@ -56,7 +49,7 @@ describe('unspecced.applyLabels', () => { { labels: [ { - src: server.ctx.cfg.labelerDid, + src: network.pds.ctx.cfg.labelerDid, uri: post.uriStr, cid: post.cidStr, val: 'birds', @@ -64,7 +57,7 @@ describe('unspecced.applyLabels', () => { cts: new Date().toISOString(), }, { - src: server.ctx.cfg.labelerDid, + src: network.pds.ctx.cfg.labelerDid, uri: post.uriStr, cid: post.cidStr, val: 'bats', @@ -86,7 +79,7 @@ describe('unspecced.applyLabels', () => { { labels: [ { - src: server.ctx.cfg.labelerDid, + src: network.pds.ctx.cfg.labelerDid, uri: post.uriStr, cid: post.cidStr, val: 'birds', @@ -94,7 +87,7 @@ describe('unspecced.applyLabels', () => { cts: new Date().toISOString(), }, { - src: server.ctx.cfg.labelerDid, + src: network.pds.ctx.cfg.labelerDid, uri: post.uriStr, cid: post.cidStr, val: 'bats', @@ -116,14 +109,14 @@ describe('unspecced.applyLabels', () => { { labels: [ { - src: server.ctx.cfg.labelerDid, + src: network.pds.ctx.cfg.labelerDid, uri: sc.dids.carol, val: 'birds', neg: false, cts: new Date().toISOString(), }, { - src: server.ctx.cfg.labelerDid, + src: network.pds.ctx.cfg.labelerDid, uri: sc.dids.carol, val: 'bats', neg: false, @@ -144,14 +137,14 @@ describe('unspecced.applyLabels', () => { { labels: [ { - src: server.ctx.cfg.labelerDid, + src: network.pds.ctx.cfg.labelerDid, uri: sc.dids.carol, val: 'birds', neg: true, cts: new Date().toISOString(), }, { - src: server.ctx.cfg.labelerDid, + src: network.pds.ctx.cfg.labelerDid, uri: sc.dids.carol, val: 'bats', neg: true, diff --git a/packages/pds/tests/migrations/blob-creator.test.ts b/packages/pds/tests/migrations/blob-creator.test.ts deleted file mode 100644 index bc9cc72faf4..00000000000 --- a/packages/pds/tests/migrations/blob-creator.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { Database } from '../../src' -import { randomStr } from '@atproto/crypto' -import { cidForCbor, TID } from '@atproto/common' -import { Kysely } from 'kysely' -import { AtUri } from '@atproto/syntax' - -describe.skip('blob creator migration', () => { - let db: Database - let rawDb: Kysely - - beforeAll(async () => { - if (process.env.DB_POSTGRES_URL) { - db = Database.postgres({ - url: process.env.DB_POSTGRES_URL, - schema: 'migration_blob_creator', - }) - } else { - db = Database.memory() - } - await db.migrateToOrThrow('_20230310T205728933Z') - - rawDb = db.db - }) - - afterAll(async () => { - await db.close() - }) - - const dids = ['did:example:alice', 'did:example:bob', 'did:example:carol'] - const getCidStr = async () => { - const cid = await cidForCbor({ test: randomStr(20, 'base32') }) - return cid.toString() - } - - const repoBlob = async (did: string, cid: string) => { - const uri = AtUri.make(did, 'com.atproto.collection', TID.nextStr()) - return { - cid, - recordUri: uri.toString(), - commit: await getCidStr(), - did, - takedownId: null, - } - } - - let blobsSnap - let repoBlobsSnap - - it('creates a some blobs', async () => { - const blobs: any[] = [] - const repoBlobs: any[] = [] - for (let i = 0; i < 1000; i++) { - const cid = await getCidStr() - blobs.push({ - cid, - mimeType: 'image/jpeg', - size: Math.floor(Math.random() * 1000000), - tempKey: null, - width: Math.floor(Math.random() * 1000), - height: Math.floor(Math.random() * 1000), - createdAt: new Date().toISOString(), - }) - if (i % 2 === 0) { - repoBlobs.push(await repoBlob(dids[0], cid)) - } else { - repoBlobs.push(await repoBlob(dids[1], cid)) - } - - if (i % 5 === 0) { - repoBlobs.push(await repoBlob(dids[2], cid)) - } - } - await rawDb.insertInto('blob').values(blobs).execute() - await rawDb.insertInto('repo_blob').values(repoBlobs).execute() - - blobsSnap = await rawDb - .selectFrom('blob') - .selectAll() - .orderBy('cid') - .execute() - repoBlobsSnap = await rawDb - .selectFrom('repo_blob') - .selectAll() - .orderBy('cid') - .orderBy('did') - .execute() - }) - - it('migrates up', async () => { - const migration = await db.migrator.migrateTo('_20230313T232322844Z') - expect(migration.error).toBeUndefined() - }) - - it('correctly migrated data', async () => { - const blobs = await rawDb - .selectFrom('blob') - .selectAll() - .orderBy('cid') - .orderBy('creator') - .execute() - const repoBlobs = await rawDb - .selectFrom('repo_blob') - .selectAll() - .orderBy('cid') - .orderBy('did') - .execute() - - expect(blobs.length).toBe(repoBlobs.length) - expect(repoBlobs.length).toBe(repoBlobsSnap.length) - - for (const blob of blobs) { - const snapped = blobsSnap.find((b) => b.cid === blob.cid) - const { creator, ...rest } = blob - expect(snapped).toEqual(rest) - const found = repoBlobsSnap.find( - (b) => b.cid === blob.cid && b.did === creator, - ) - expect(found).toBeDefined() - } - }) - - it('migrates down', async () => { - const migration = await db.migrator.migrateTo('_20230310T205728933Z') - expect(migration.error).toBeUndefined() - - const updatedBlobs = await rawDb - .selectFrom('blob') - .selectAll() - .orderBy('cid') - .execute() - const updatedRepoBlobs = await rawDb - .selectFrom('repo_blob') - .selectAll() - .orderBy('cid') - .orderBy('did') - .execute() - - expect(updatedBlobs).toEqual(blobsSnap) - expect(updatedRepoBlobs).toEqual(repoBlobsSnap) - }) -}) diff --git a/packages/pds/tests/migrations/indexed-at-on-record.test.ts b/packages/pds/tests/migrations/indexed-at-on-record.test.ts deleted file mode 100644 index 664ae3e16cd..00000000000 --- a/packages/pds/tests/migrations/indexed-at-on-record.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Database } from '../../src' -import { randomStr } from '@atproto/crypto' -import { dataToCborBlock, TID } from '@atproto/common' -import { AtUri } from '@atproto/syntax' -import { Kysely } from 'kysely' - -describe.skip('indexedAt on record migration', () => { - let db: Database - let rawDb: Kysely - - beforeAll(async () => { - if (process.env.DB_POSTGRES_URL) { - db = Database.postgres({ - url: process.env.DB_POSTGRES_URL, - schema: 'migration_indexed_at_on_record', - }) - } else { - db = Database.memory() - } - - await db.migrateToOrThrow('_20221230T215012029Z') - rawDb = db.db - }) - - afterAll(async () => { - await db.close() - }) - - const randomDate = () => { - const start = new Date(2022, 0, 1) - const end = new Date() - return new Date( - start.getTime() + Math.random() * (end.getTime() - start.getTime()), - ).toISOString() - } - - const times: { [cid: string]: string } = {} - - it('fills the db with some records & blocks', async () => { - const blocks: any[] = [] - const records: any[] = [] - for (let i = 0; i < 100; i++) { - const date = randomDate() - const record = { test: randomStr(8, 'base32') } - const block = await dataToCborBlock(record) - blocks.push({ - cid: block.cid.toString(), - content: block.bytes, - size: block.bytes.length, - indexedAt: date, - }) - const uri = AtUri.make('did:example:alice', 'fake.posts', TID.nextStr()) - records.push({ - uri: uri.toString(), - cid: block.cid.toString(), - did: uri.hostname, - collection: uri.collection, - rkey: uri.rkey, - }) - times[block.cid.toString()] = date - } - - await rawDb.insertInto('ipld_block').values(blocks).execute() - await rawDb.insertInto('record').values(records).execute() - }) - - it('migrates up', async () => { - await db.migrateToOrThrow('_20230127T215753149Z') - }) - - it('associated the date to the correct record', async () => { - const res = await rawDb.selectFrom('record').selectAll().execute() - res.forEach((row) => { - expect(row.indexedAt).toEqual(times[row.cid]) - }) - }) -}) diff --git a/packages/pds/tests/migrations/repo-sync-data-pt2.test.ts b/packages/pds/tests/migrations/repo-sync-data-pt2.test.ts deleted file mode 100644 index 91971fb7043..00000000000 --- a/packages/pds/tests/migrations/repo-sync-data-pt2.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import AtpAgent from '@atproto/api' -import { Database } from '../../src' -import { Kysely } from 'kysely' -import { CloseFn, runTestServer } from '../_util' -import { SeedClient } from '../seeds/client' -import basicSeed from '../seeds/basic' - -describe.skip('repo sync data migration', () => { - let db: Database - let rawDb: Kysely - let close: CloseFn - - beforeAll(async () => { - const server = await runTestServer({ - dbPostgresSchema: 'migration_repo_sync_data_pt_two', - }) - db = server.ctx.db - rawDb = db.db - close = server.close - const agent = new AtpAgent({ service: server.url }) - const sc = new SeedClient(agent) - await basicSeed(sc) - }) - - afterAll(async () => { - await close() - }) - - it('migrates down to pt2', async () => { - await db.migrateToOrThrow('_20230201T200606704Z') - }) - - const getSnapshot = async () => { - const [history, blocks] = await Promise.all([ - rawDb - .selectFrom('repo_commit_history') - .selectAll() - .orderBy('creator') - .orderBy('commit') - .execute(), - rawDb - .selectFrom('repo_commit_block') - .selectAll() - .orderBy('creator') - .orderBy('commit') - .orderBy('block') - .execute(), - ]) - return { history, blocks } - } - - let snapshot: { history: any[]; blocks: any[] } - - it('snapshots current state of commits', async () => { - snapshot = await getSnapshot() - }) - - it('migrates down to pt1', async () => { - await db.migrateToOrThrow('_20230127T224743452Z') - }) - - it('deletes some random commits', async () => { - const toDelete: string[] = [] - for (const commit of snapshot.history) { - if (Math.random() < 0.2) { - toDelete.push(commit.commit) - } - } - await rawDb - .deleteFrom('repo_commit_block') - .where('commit', 'in', toDelete) - .execute() - await rawDb - .deleteFrom('repo_commit_history') - .where('commit', 'in', toDelete) - .execute() - }) - - it('migrates up', async () => { - await db.migrateToOrThrow('_20230201T200606704Z') - }) - - it('backfilled missing commits', async () => { - const newSnapshot = await getSnapshot() - expect(newSnapshot).toEqual(snapshot) - }) -}) diff --git a/packages/pds/tests/migrations/repo-sync-data.test.ts b/packages/pds/tests/migrations/repo-sync-data.test.ts deleted file mode 100644 index 2fd17c199eb..00000000000 --- a/packages/pds/tests/migrations/repo-sync-data.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { Database } from '../../src' -import { MemoryBlockstore, Repo, WriteOpAction } from '@atproto/repo' -import { P256Keypair, Keypair, randomStr } from '@atproto/crypto' -import { TID } from '@atproto/common' -import { Kysely } from 'kysely' -import { CID } from 'multiformats/cid' - -describe.skip('repo sync data migration', () => { - let db: Database - let rawDb: Kysely - let memoryStore: MemoryBlockstore - let keypair: Keypair - let repo: Repo - - const aliceDid = 'did:example:alice' - const bobDid = 'did:example:bob' - - beforeAll(async () => { - if (process.env.DB_POSTGRES_URL) { - db = Database.postgres({ - url: process.env.DB_POSTGRES_URL, - schema: 'migration_repo_sync_data', - }) - } else { - db = Database.memory() - } - await db.migrateToOrThrow('_20230127T215753149Z') - rawDb = db.db - memoryStore = new MemoryBlockstore() - keypair = await P256Keypair.create() - repo = await Repo.create(memoryStore, keypair.did(), keypair) - }) - - afterAll(async () => { - await db.close() - }) - - it('fills the db with some repo data', async () => { - for (let i = 0; i < 100; i++) { - repo = await repo.applyWrites( - { - action: WriteOpAction.Create, - collection: randomStr(8, 'base32'), - rkey: TID.nextStr(), - record: { name: randomStr(32, 'base32') }, - }, - keypair, - ) - } - const blocks: any[] = [] - const creators: any[] = [] - for (const entry of memoryStore.blocks.entries()) { - blocks.push({ - cid: entry.cid.toString(), - size: entry.bytes.length, - content: entry.bytes, - }) - creators.push({ - cid: entry.cid.toString(), - did: aliceDid, - }) - const registerTwice = Math.random() > 0.1 - if (registerTwice) { - creators.push({ - cid: entry.cid.toString(), - did: bobDid, - }) - } - } - const rawDb: Kysely = db.db - await Promise.all([ - rawDb.insertInto('ipld_block').values(blocks).execute(), - rawDb.insertInto('ipld_block_creator').values(creators).execute(), - rawDb - .insertInto('repo_root') - .values([ - { - did: aliceDid, - root: repo.cid.toString(), - indexedAt: new Date().toISOString(), - }, - { - did: bobDid, - root: repo.cid.toString(), - indexedAt: new Date().toISOString(), - }, - ]) - .execute(), - ]) - }) - - it('migrates up', async () => { - const migration = await db.migrator.migrateTo('_20230201T200606704Z') - expect(migration.error).toBeUndefined() - }) - - it('fills in missing block creators', async () => { - const aliceBlocks = await rawDb - .selectFrom('ipld_block_creator') - .selectAll() - .where('did', '=', aliceDid) - .execute() - const bobBlocks = await rawDb - .selectFrom('ipld_block_creator') - .selectAll() - .where('did', '=', bobDid) - .execute() - - const aliceCids = aliceBlocks.map((row) => row.cid).sort() - const bobCids = bobBlocks.map((row) => row.cid).sort() - - expect(aliceCids).toEqual(bobCids) - }) - - it('correctly constructs repo history', async () => { - const checkRepoContents = async (did: string) => { - const history = await rawDb - .selectFrom('repo_commit_history') - .where('creator', '=', did) - .selectAll() - .execute() - const blocks = await rawDb - .selectFrom('repo_commit_block') - .where('creator', '=', did) - .selectAll() - .execute() - const commits = await memoryStore.getCommits(repo.cid, null) - if (!commits) { - throw new Error('Could not get commit log from memoryStore') - } - for (let i = 0; i < commits.length; i++) { - const commit = commits[i] - const prev = commits[i - 1]?.commit?.toString() || null - const filteredHistory = history.filter( - (row) => row.commit === commit.commit.toString(), - ) - expect(filteredHistory.length).toBe(1) - expect(filteredHistory[0].prev).toEqual(prev) - - const filteredBlocks = blocks.filter( - (row) => row.commit === commit.commit.toString(), - ) - expect(filteredBlocks.length).toEqual(commit.blocks.size) - filteredBlocks.forEach((block) => { - expect(commit.blocks.has(CID.parse(block.block))).toBeTruthy() - }) - } - } - - await checkRepoContents(aliceDid) - await checkRepoContents(bobDid) - }) -}) diff --git a/packages/pds/tests/migrations/user-partitioned-cids.test.ts b/packages/pds/tests/migrations/user-partitioned-cids.test.ts deleted file mode 100644 index e6eff220445..00000000000 --- a/packages/pds/tests/migrations/user-partitioned-cids.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Database } from '../../src' -import { randomStr } from '@atproto/crypto' -import { dataToCborBlock } from '@atproto/common' -import { Kysely } from 'kysely' -import { Block } from 'multiformats/block' -import * as uint8arrays from 'uint8arrays' - -describe.skip('user partitioned cids migration', () => { - let db: Database - let rawDb: Kysely - - beforeAll(async () => { - if (process.env.DB_POSTGRES_URL) { - db = Database.postgres({ - url: process.env.DB_POSTGRES_URL, - schema: 'migration_user_partitioned_cids', - }) - } else { - db = Database.memory() - } - await db.migrateToOrThrow('_20230201T200606704Z') - - rawDb = db.db - }) - - afterAll(async () => { - await db.close() - }) - - const dids = ['did:example:one', 'did:example:two', 'did:example:three'] - const blocks: Block[] = [] - - it('fills the db with some cids', async () => { - for (let i = 0; i < 4; i++) { - const block = await dataToCborBlock({ - test: randomStr(32, 'base32'), - }) - - blocks.push(block) - } - - const blocksToInsert = blocks.map((b) => ({ - cid: b.cid.toString(), - size: b.bytes.length, - content: b.bytes, - })) - await rawDb.insertInto('ipld_block').values(blocksToInsert).execute() - - // block 0 is owned by only user 0 - // block 1 is owned by only user 1 - // block 2 is owned by users 0 & 1 - // block 3 is owned by all three users - const creatorsToInsert = [ - { cid: blocks[0].cid.toString(), did: dids[0] }, - { cid: blocks[1].cid.toString(), did: dids[1] }, - { cid: blocks[2].cid.toString(), did: dids[0] }, - { cid: blocks[2].cid.toString(), did: dids[1] }, - { cid: blocks[3].cid.toString(), did: dids[0] }, - { cid: blocks[3].cid.toString(), did: dids[1] }, - { cid: blocks[3].cid.toString(), did: dids[2] }, - ] - await rawDb - .insertInto('ipld_block_creator') - .values(creatorsToInsert) - .execute() - }) - - it('migrates up', async () => { - const migration = await db.migrator.migrateTo('_20230202T170426672Z') - expect(migration.error).toBeUndefined() - }) - - it('correctly partitions user ipld blocks', async () => { - const fromDb = await rawDb.selectFrom('ipld_block').selectAll().execute() - - const first = fromDb.filter((b) => b.cid === blocks[0].cid.toString()) - expect(first.length).toBe(1) - expect(first[0].creator).toEqual(dids[0]) - expect(uint8arrays.equals(first[0].content, blocks[0].bytes)).toBeTruthy() - - const second = fromDb.filter((b) => b.cid === blocks[1].cid.toString()) - expect(second.length).toBe(1) - expect(second[0].creator).toEqual(dids[1]) - expect(uint8arrays.equals(second[0].content, blocks[1].bytes)).toBeTruthy() - - const third = fromDb.filter((b) => b.cid === blocks[2].cid.toString()) - expect(third.length).toBe(2) - const thirdCreators = third.map((row) => row.creator) - expect(thirdCreators.sort()).toEqual(dids.slice(0, 2).sort()) - third.forEach((row) => { - expect(uint8arrays.equals(row.content, blocks[2].bytes)).toBeTruthy() - }) - - const fourth = fromDb.filter((b) => b.cid === blocks[3].cid.toString()) - expect(fourth.length).toBe(3) - const fourthCreators = fourth.map((row) => row.creator) - expect(fourthCreators.sort()).toEqual(dids.sort()) - fourth.forEach((row) => { - expect(uint8arrays.equals(row.content, blocks[3].bytes)).toBeTruthy() - }) - }) -}) diff --git a/packages/pds/tests/migrations/user-table-did-pkey.test.ts b/packages/pds/tests/migrations/user-table-did-pkey.test.ts deleted file mode 100644 index 881907e71b8..00000000000 --- a/packages/pds/tests/migrations/user-table-did-pkey.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { Database } from '../../src' -import { randomStr } from '@atproto/crypto' -import { Kysely } from 'kysely' - -describe.skip('user table did pkey migration', () => { - let db: Database - let rawDb: Kysely - - beforeAll(async () => { - if (process.env.DB_POSTGRES_URL) { - db = Database.postgres({ - url: process.env.DB_POSTGRES_URL, - schema: 'migration_user_table_did_pkey', - }) - } else { - db = Database.memory() - } - await db.migrateToOrThrow('_20230202T172831900Z') - - rawDb = db.db - }) - - afterAll(async () => { - await db.close() - }) - - const randDateBefore = (beforeUnix: number): Date => { - return new Date(beforeUnix - Math.floor(1000000 * Math.random())) - } - - let userSnap - let didHandleSnap - - it('creates a bunch of users', async () => { - const didHandles: any[] = [] - const users: any[] = [] - for (let i = 0; i < 1000; i++) { - const did = `did:plc:${randomStr(24, 'base32')}` - const handle = `${randomStr(8, 'base32')}.bsky.social` - const email = `${randomStr(8, 'base32')}@test.com` - const password = randomStr(32, 'base32') - const lastSeenNotifs = randDateBefore(Date.now()) - const createdAt = randDateBefore(lastSeenNotifs.getTime()) - const passwordResetToken = - Math.random() > 0.9 ? randomStr(4, 'base32') : null - const passwordResetGrantedAt = - Math.random() > 0.5 ? randDateBefore(lastSeenNotifs.getTime()) : null - didHandles.push({ did, handle }) - users.push({ - handle, - email, - password, - lastSeenNotifs: lastSeenNotifs.toISOString(), - createdAt: createdAt.toISOString(), - passwordResetToken, - passwordResetGrantedAt: passwordResetGrantedAt?.toISOString() || null, - }) - } - await rawDb.insertInto('did_handle').values(didHandles).execute() - await rawDb.insertInto('user').values(users).execute() - - didHandleSnap = await rawDb - .selectFrom('did_handle') - .selectAll() - .orderBy('did') - .execute() - userSnap = await rawDb - .selectFrom('user') - .selectAll() - .orderBy('email') - .execute() - - // console.log( - // await rawDb.selectFrom('user').select('lastSeenNotifs').execute(), - // ) - }) - - it('migrates up', async () => { - const migration = await db.migrator.migrateTo('_20230208T222001557Z') - expect(migration.error).toBeUndefined() - }) - - it('correctly migrated data', async () => { - const updatedDidHandle = await rawDb - .selectFrom('did_handle') - .selectAll() - .orderBy('did') - .execute() - const updatedUser = await rawDb - .selectFrom('user_account') - .selectAll() - .orderBy('email') - .execute() - const updatedUserState = await rawDb - .selectFrom('user_state') - .selectAll() - .orderBy('did') - .execute() - - expect(updatedDidHandle).toEqual(didHandleSnap) - - expect(updatedUser.length).toBe(userSnap.length) - for (let i = 0; i < updatedUser.length; i++) { - const { handle, password, lastSeenNotifs, ...rest } = userSnap[i] - const expectedDid = didHandleSnap.find((row) => row.handle === handle).did - const expected = { did: expectedDid, passwordScrypt: password, ...rest } - expect(updatedUser[i]).toEqual(expected) - const lastSeen = updatedUserState.find( - (row) => row.did === expectedDid, - )?.lastSeenNotifs - expect(lastSeen).toEqual(lastSeenNotifs) - } - }) - - it('migrates down', async () => { - const migration = await db.migrator.migrateTo('_20230202T172831900Z') - expect(migration.error).toBeUndefined() - - const updatedDidHandle = await rawDb - .selectFrom('did_handle') - .selectAll() - .orderBy('did') - .execute() - const updatedUser = await rawDb - .selectFrom('user') - .selectAll() - .orderBy('email') - .execute() - - expect(updatedDidHandle).toEqual(didHandleSnap) - expect(updatedUser).toEqual(userSnap) - }) -}) diff --git a/packages/pds/tests/moderation.test.ts b/packages/pds/tests/moderation.test.ts index 0861d852aca..34e52a156a0 100644 --- a/packages/pds/tests/moderation.test.ts +++ b/packages/pds/tests/moderation.test.ts @@ -1130,18 +1130,11 @@ describe('moderation', () => { describe('blob takedown', () => { let post: { ref: RecordRef; images: ImageRef[] } let blob: ImageRef - let imageUri: string let actionId: number + beforeAll(async () => { post = sc.posts[sc.dids.carol][0] blob = post.images[1] - imageUri = server.ctx.imgUriBuilder - .getCommonSignedUri('feed_thumbnail', blob.image.ref.toString()) - .replace(server.ctx.cfg.publicUrl, server.url) - // Warm image server cache - await fetch(imageUri) - const cached = await fetch(imageUri) - expect(cached.headers.get('x-cache')).toEqual('hit') const takeAction = await agent.api.com.atproto.admin.takeModerationAction( { action: TAKEDOWN, @@ -1178,12 +1171,6 @@ describe('moderation', () => { await expect(referenceBlob).rejects.toThrow('Could not find blob:') }) - it('prevents image blob from being served, even when cached.', async () => { - const fetchImage = await fetch(imageUri) - expect(fetchImage.status).toEqual(404) - expect(await fetchImage.json()).toEqual({ message: 'Image not found' }) - }) - it('restores blob when action is reversed.', async () => { await agent.api.com.atproto.admin.reverseModerationAction( { @@ -1200,12 +1187,6 @@ describe('moderation', () => { // Can post and reference blob const post = await sc.post(sc.dids.alice, 'pic', [], [blob]) expect(post.images[0].image.ref.equals(blob.image.ref)).toBeTruthy() - - // Can fetch through image server - const fetchImage = await fetch(imageUri) - expect(fetchImage.status).toEqual(200) - const size = Number(fetchImage.headers.get('content-length')) - expect(size).toBeGreaterThan(9000) }) }) }) diff --git a/packages/pds/tests/proxied/__snapshots__/timeline-skeleton.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/timeline-skeleton.test.ts.snap deleted file mode 100644 index 1bb947bb64c..00000000000 --- a/packages/pds/tests/proxied/__snapshots__/timeline-skeleton.test.ts.snap +++ /dev/null @@ -1,2798 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`proxies timeline skeleton feed skeleton construction 1`] = ` -Object { - "cursor": "0000000000000::bafycid", - "feed": Array [ - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(6)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(6)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(3)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(3)", - "val": "test-label", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(3)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(3)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", - "muted": false, - }, - }, - "cid": "cids(6)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "of course", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(7)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(6)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(6)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(3)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(3)", - "val": "test-label", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(3)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(3)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(7)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(3)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(3)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(12)", - "muted": false, - }, - }, - "cid": "cids(8)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", - "muted": false, - }, - }, - "cid": "cids(9)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(13)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(10)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(11)", - "uri": "record(14)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(11)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(9)", - "uri": "record(13)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(0)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(10)", - "val": "test-label", - }, - ], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(8)", - "uri": "record(11)", - }, - }, - "text": "yoohoo label_me", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(10)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(6)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(6)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(12)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000+00:00", - "text": "bobby boy here", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(15)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", - "muted": false, - }, - }, - "cid": "cids(9)", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(6)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(6)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(11)", - "embeds": Array [], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(14)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(10)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(11)", - "uri": "record(14)", - }, - }, - }, - "text": "hi im carol", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(13)", - "viewer": Object { - "like": "record(16)", - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(6)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(6)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(11)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(14)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(13)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(13)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(17)", - "val": "self-label", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label", - }, - ], - }, - "text": "hey there", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(17)", - "viewer": Object {}, - }, - }, - ], -} -`; - -exports[`proxies timeline skeleton timeline skeleton construction 1`] = ` -Object { - "cursor": "0000000000000::bafycid", - "feed": Array [ - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object {}, - }, - "reason": Object { - "$type": "app.bsky.feed.defs#reasonRepost", - "by": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(3)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(3)", - "val": "test-label", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(3)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(3)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - "reason": Object { - "$type": "app.bsky.feed.defs#reasonRepost", - "by": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(6)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(3)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "muted": false, - }, - }, - "cid": "cids(7)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(9)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(12)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(9)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(8)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(9)", - "uri": "record(12)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(7)", - "uri": "record(9)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(0)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(8)", - "viewer": Object {}, - }, - "reason": Object { - "$type": "app.bsky.feed.defs#reasonRepost", - "by": Object { - "did": "user(3)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "muted": false, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(3)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(3)", - "val": "test-label", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(3)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(3)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(3)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "muted": false, - }, - }, - "cid": "cids(10)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "of course", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(13)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(3)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(3)", - "val": "test-label", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(3)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(3)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(11)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(6)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(3)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "muted": false, - }, - }, - "cid": "cids(7)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(9)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(8)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(9)", - "uri": "record(12)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(8)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(7)", - "uri": "record(9)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(0)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(11)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(14)", - "val": "test-label", - }, - ], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(6)", - "uri": "record(8)", - }, - }, - "text": "yoohoo label_me", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(14)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(12)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000+00:00", - "text": "bobby boy here", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(15)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(6)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(3)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "muted": false, - }, - }, - "cid": "cids(7)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(9)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(12)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(9)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(8)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(9)", - "uri": "record(12)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(7)", - "uri": "record(9)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(0)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(8)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(13)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "text": "dan here!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(16)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(3)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "muted": false, - }, - }, - "cid": "cids(7)", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(9)", - "embeds": Array [], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(12)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(8)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(9)", - "uri": "record(12)", - }, - }, - }, - "text": "hi im carol", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(9)", - "viewer": Object { - "like": "record(17)", - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(9)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(12)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(14)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(14)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(18)", - "val": "self-label", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label", - }, - ], - }, - "text": "hey there", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(18)", - "viewer": Object {}, - }, - }, - ], -} -`; diff --git a/packages/pds/tests/proxied/timeline-skeleton.test.ts b/packages/pds/tests/proxied/timeline-skeleton.test.ts deleted file mode 100644 index d7617a74a16..00000000000 --- a/packages/pds/tests/proxied/timeline-skeleton.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import AtpAgent, { AtUri } from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' -import { SeedClient } from '../seeds/client' -import basicSeed from '../seeds/basic' -import { forSnapshot } from '../_util' -import * as bsky from '@atproto/bsky' -import { makeAlgos } from '../../src' - -describe('proxies timeline skeleton', () => { - let network: TestNetwork - let agent: AtpAgent - let sc: SeedClient - - let alice: string - - const feedGenDid = 'did:example:feed-gen' - const feedPublisherDid = 'did:example:feed-publisher' - - beforeAll(async () => { - network = await TestNetwork.create({ - dbPostgresSchema: 'proxy_timeline_skeleton', - pds: { - feedGenDid, - algos: makeAlgos(feedPublisherDid), - enableInProcessAppView: true, - bskyAppViewProxy: false, - }, - bsky: { - feedGenDid, - algos: bsky.makeAlgos(feedPublisherDid), - }, - }) - agent = network.pds.getClient() - sc = new SeedClient(agent) - await basicSeed(sc) - await network.processAll() - alice = sc.dids.alice - }) - - afterAll(async () => { - await network.close() - }) - - it('timeline skeleton construction', async () => { - const res = await agent.api.app.bsky.feed.getTimeline( - {}, - { - headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' }, - }, - ) - - expect(forSnapshot(res.data)).toMatchSnapshot() - const pt1 = await agent.api.app.bsky.feed.getTimeline( - { - limit: 2, - }, - { - headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' }, - }, - ) - const pt2 = await agent.api.app.bsky.feed.getTimeline( - { - cursor: pt1.data.cursor, - }, - { - headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' }, - }, - ) - expect([...pt1.data.feed, ...pt2.data.feed]).toEqual(res.data.feed) - }) - - it('feed skeleton construction', async () => { - const uri = AtUri.make( - feedPublisherDid, - 'app.bsky.feed.generator', - 'mutuals', - ) - const res = await agent.api.app.bsky.feed.getFeed( - { feed: uri.toString() }, - { - headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' }, - }, - ) - expect(forSnapshot(res.data)).toMatchSnapshot() - const pt1 = await agent.api.app.bsky.feed.getFeed( - { feed: uri.toString(), limit: 2 }, - { - headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' }, - }, - ) - const pt2 = await agent.api.app.bsky.feed.getFeed( - { feed: uri.toString(), cursor: pt1.data.cursor }, - { - headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' }, - }, - ) - expect([...pt1.data.feed, ...pt2.data.feed]).toEqual(res.data.feed) - }) -}) diff --git a/packages/pds/tests/server.test.ts b/packages/pds/tests/server.test.ts index ae9f34fed26..3994df3a9ab 100644 --- a/packages/pds/tests/server.test.ts +++ b/packages/pds/tests/server.test.ts @@ -1,12 +1,13 @@ import { AddressInfo } from 'net' import express from 'express' import axios, { AxiosError } from 'axios' -import AtpAgent from '@atproto/api' +import AtpAgent, { AtUri } from '@atproto/api' import { CloseFn, runTestServer, TestServerInfo } from './_util' import { handler as errorHandler } from '../src/error' import { SeedClient } from './seeds/client' import basicSeed from './seeds/basic' import { Database } from '../src' +import { randomStr } from '@atproto/crypto' describe('server', () => { let server: TestServerInfo @@ -86,8 +87,26 @@ describe('server', () => { }) it('compresses large json responses', async () => { + // first create a large record + const record = { + text: 'blahblabh', + createdAt: new Date().toISOString(), + } + for (let i = 0; i < 100; i++) { + record[randomStr(8, 'base32')] = randomStr(32, 'base32') + } + const createRes = await agent.com.atproto.repo.createRecord( + { + repo: alice, + collection: 'app.bsky.feed.post', + record, + }, + { headers: sc.getHeaders(alice), encoding: 'application/json' }, + ) + const uri = new AtUri(createRes.data.uri) + const res = await axios.get( - `${server.url}/xrpc/app.bsky.feed.getTimeline`, + `${server.url}/xrpc/com.atproto.repo.getRecord?repo=${uri.host}&collection=${uri.collection}&rkey=${uri.rkey}`, { decompress: false, headers: { ...sc.getHeaders(alice), 'accept-encoding': 'gzip' }, @@ -120,7 +139,7 @@ describe('server', () => { it('healthcheck fails when database is unavailable.', async () => { // destroy to release lock & allow db to close - await server.ctx.sequencerLeader.destroy() + await server.ctx.sequencerLeader?.destroy() await db.close() let error: AxiosError diff --git a/packages/pds/tests/views/__snapshots__/author-feed.test.ts.snap b/packages/pds/tests/views/__snapshots__/author-feed.test.ts.snap deleted file mode 100644 index ca56abeb646..00000000000 --- a/packages/pds/tests/views/__snapshots__/author-feed.test.ts.snap +++ /dev/null @@ -1,2495 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`pds author feed views fetches full author feeds for self (sorted, minimal viewer state). 1`] = ` -Array [ - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(6)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(6)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(3)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(3)", - "val": "test-label", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(3)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(3)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(6)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(2)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(2)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(9)", - "muted": false, - }, - }, - "cid": "cids(7)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(3)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(12)", - "following": "record(11)", - "muted": false, - }, - }, - "cid": "cids(8)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(10)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(9)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(10)", - "uri": "record(13)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(8)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(8)", - "uri": "record(10)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(0)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(6)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(7)", - "val": "test-label", - }, - ], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(7)", - "uri": "record(8)", - }, - }, - "text": "yoohoo label_me", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(7)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(11)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(11)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(14)", - "val": "self-label", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label", - }, - ], - }, - "text": "hey there", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(14)", - "viewer": Object {}, - }, - }, -] -`; - -exports[`pds author feed views fetches full author feeds for self (sorted, minimal viewer state). 2`] = ` -Array [ - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(0)", - "val": "test-label", - }, - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(0)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(2)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(2)", - }, - "root": Object { - "cid": "cids(3)", - "uri": "record(2)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(0)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(5)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(5)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", - "muted": false, - }, - }, - "cid": "cids(3)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object { - "like": "record(6)", - }, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(5)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(5)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", - "muted": false, - }, - }, - "cid": "cids(3)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object { - "like": "record(6)", - }, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(5)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000+00:00", - "text": "bobby boy here", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(7)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(6)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(8)", - "viewer": Object {}, - }, - }, -] -`; - -exports[`pds author feed views fetches full author feeds for self (sorted, minimal viewer state). 3`] = ` -Array [ - Object { - "post": Object { - "author": Object { - "did": "user(0)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(0)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(1)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(4)", - "val": "self-label-a", - }, - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(4)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "muted": false, - }, - }, - "cid": "cids(4)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(2)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(1)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(2)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(3)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(4)", - "uri": "record(2)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(1)", - "uri": "record(1)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(1)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object { - "repost": "record(5)", - }, - }, - "reason": Object { - "$type": "app.bsky.feed.defs#reasonRepost", - "by": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(6)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(7)", - "uri": "record(7)", - }, - "root": Object { - "cid": "cids(7)", - "uri": "record(7)", - }, - }, - "text": "of course", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(6)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(8)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(10)", - "val": "self-label-a", - }, - Object { - "cid": "cids(8)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(10)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", - "muted": false, - }, - }, - "cid": "cids(7)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(7)", - "viewer": Object { - "like": "record(11)", - }, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(8)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(10)", - "val": "self-label-a", - }, - Object { - "cid": "cids(8)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(10)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", - "muted": false, - }, - }, - "cid": "cids(7)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(7)", - "viewer": Object { - "like": "record(11)", - }, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(1)", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(4)", - "val": "self-label-a", - }, - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(4)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "muted": false, - }, - }, - "cid": "cids(4)", - "embeds": Array [], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(2)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(2)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(3)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(4)", - "uri": "record(2)", - }, - }, - }, - "text": "hi im carol", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(1)", - "viewer": Object {}, - }, - }, -] -`; - -exports[`pds author feed views fetches full author feeds for self (sorted, minimal viewer state). 4`] = ` -Array [ - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(4)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(3)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object { - "repost": "record(5)", - }, - }, - "reason": Object { - "$type": "app.bsky.feed.defs#reasonRepost", - "by": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(8)", - "muted": false, - }, - }, - "cid": "cids(3)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(4)", - "val": "test-label", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(4)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(3)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(4)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(3)", - "viewer": Object { - "like": "record(7)", - "repost": "record(6)", - }, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(3)", - "viewer": Object { - "like": "record(7)", - "repost": "record(6)", - }, - }, - "reason": Object { - "$type": "app.bsky.feed.defs#reasonRepost", - "by": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(6)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(3)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(7)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(8)", - "muted": false, - }, - }, - "cid": "cids(9)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(12)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(11)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(8)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(9)", - "uri": "record(12)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(7)", - "uri": "record(11)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(0)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(10)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(10)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "text": "dan here!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(13)", - "viewer": Object {}, - }, - }, -] -`; - -exports[`pds author feed views omits reposts from muted users. 1`] = ` -Array [ - Object { - "post": Object { - "author": Object { - "did": "user(0)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(0)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "muted": true, - }, - }, - "cid": "cids(0)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "following": "record(3)", - "muted": false, - }, - }, - "cid": "cids(1)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(5)", - "val": "self-label-a", - }, - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(5)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(4)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(4)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(2)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(2)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(3)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(4)", - "uri": "record(4)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(1)", - "uri": "record(2)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(1)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(0)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(0)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "muted": true, - }, - }, - "cid": "cids(6)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "text": "dan here!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(6)", - "viewer": Object {}, - }, - }, -] -`; - -exports[`pds author feed views reflects fetching user's state in the feed. 1`] = ` -Array [ - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(5)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(4)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(8)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(8)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "muted": false, - }, - }, - "cid": "cids(3)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(5)", - "val": "test-label", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(5)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(4)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(4)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(5)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(4)", - "viewer": Object { - "like": "record(6)", - }, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(6)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(2)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(2)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(7)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(3)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(8)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(11)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(9)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(10)", - "uri": "record(12)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(10)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(8)", - "uri": "record(11)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(0)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(6)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(9)", - "val": "test-label", - }, - ], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(7)", - "uri": "record(10)", - }, - }, - "text": "yoohoo label_me", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(9)", - "viewer": Object { - "like": "record(13)", - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(4)", - "viewer": Object { - "like": "record(6)", - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(11)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(11)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(14)", - "val": "self-label", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label", - }, - ], - }, - "text": "hey there", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(14)", - "viewer": Object {}, - }, - }, -] -`; diff --git a/packages/pds/tests/views/__snapshots__/blocks.test.ts.snap b/packages/pds/tests/views/__snapshots__/blocks.test.ts.snap deleted file mode 100644 index 9b168ae33a5..00000000000 --- a/packages/pds/tests/views/__snapshots__/blocks.test.ts.snap +++ /dev/null @@ -1,383 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`pds views with blocking blocks record embeds 1`] = ` -Object { - "thread": Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewBlocked", - "author": Object { - "did": "user(2)", - "viewer": Object { - "blockedBy": false, - "blocking": "record(5)", - }, - }, - "blocked": true, - "uri": "record(4)", - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(3)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(3)", - "uri": "record(4)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(0)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(0)", - "val": "test-label", - }, - ], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(2)", - "uri": "record(3)", - }, - }, - "text": "yoohoo label_me", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(0)", - "viewer": Object {}, - }, - }, -} -`; - -exports[`pds views with blocking blocks thread parent 1`] = ` -Object { - "thread": Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "parent": Object { - "$type": "app.bsky.feed.defs#blockedPost", - "author": Object { - "did": "user(1)", - "viewer": Object { - "blockedBy": true, - }, - }, - "blocked": true, - "uri": "record(4)", - }, - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(4)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(4)", - }, - }, - "text": "alice replies to dan", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(0)", - "viewer": Object {}, - }, - "replies": Array [], - }, -} -`; - -exports[`pds views with blocking blocks thread reply 1`] = ` -Object { - "thread": Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object { - "like": "record(4)", - "repost": "record(3)", - }, - }, - "replies": Array [ - Object { - "$type": "app.bsky.feed.defs#blockedPost", - "author": Object { - "did": "user(1)", - "viewer": Object { - "blockedBy": false, - "blocking": "record(6)", - }, - }, - "blocked": true, - "uri": "record(5)", - }, - Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(8)", - "muted": false, - }, - }, - "cid": "cids(2)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(2)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(7)", - "val": "test-label", - }, - Object { - "cid": "cids(2)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(7)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(4)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(7)", - "viewer": Object {}, - }, - }, - ], - }, -} -`; diff --git a/packages/pds/tests/views/__snapshots__/follows.test.ts.snap b/packages/pds/tests/views/__snapshots__/follows.test.ts.snap deleted file mode 100644 index fbcd95eb3a4..00000000000 --- a/packages/pds/tests/views/__snapshots__/follows.test.ts.snap +++ /dev/null @@ -1,703 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`pds follow views blocks followers by actor takedown 1`] = ` -Object { - "cursor": "0000000000000::bafycid", - "followers": Array [ - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-eve", - "did": "user(1)", - "displayName": "display-eve", - "handle": "eve.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", - "muted": false, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-bob", - "did": "user(2)", - "displayName": "display-bob", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", - "muted": false, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-carol", - "did": "user(3)", - "displayName": "display-carol", - "handle": "carol.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - ], - "subject": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-alice", - "did": "user(0)", - "displayName": "display-alice", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, -} -`; - -exports[`pds follow views blocks follows by actor takedown 1`] = ` -Object { - "cursor": "0000000000000::bafycid", - "follows": Array [ - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-eve", - "did": "user(1)", - "displayName": "display-eve", - "handle": "eve.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", - "muted": false, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-carol", - "did": "user(2)", - "displayName": "display-carol", - "handle": "carol.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", - "muted": false, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-bob", - "did": "user(3)", - "displayName": "display-bob", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - ], - "subject": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-alice", - "did": "user(0)", - "displayName": "display-alice", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, -} -`; - -exports[`pds follow views fetches followers 1`] = ` -Object { - "cursor": "0000000000000::bafycid", - "followers": Array [ - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-eve", - "did": "user(1)", - "displayName": "display-eve", - "handle": "eve.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", - "muted": false, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-dan", - "did": "user(2)", - "displayName": "display-dan", - "handle": "dan.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", - "muted": false, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-bob", - "did": "user(3)", - "displayName": "display-bob", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-carol", - "did": "user(4)", - "displayName": "display-carol", - "handle": "carol.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", - "muted": false, - }, - }, - ], - "subject": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-alice", - "did": "user(0)", - "displayName": "display-alice", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, -} -`; - -exports[`pds follow views fetches followers 2`] = ` -Object { - "cursor": "0000000000000::bafycid", - "followers": Array [ - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-dan", - "did": "user(1)", - "displayName": "display-dan", - "handle": "dan.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", - "muted": false, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-alice", - "did": "user(2)", - "displayName": "display-alice", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - ], - "subject": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-bob", - "did": "user(0)", - "displayName": "display-bob", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", - "muted": false, - }, - }, -} -`; - -exports[`pds follow views fetches followers 3`] = ` -Object { - "cursor": "0000000000000::bafycid", - "followers": Array [ - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-eve", - "did": "user(1)", - "displayName": "display-eve", - "handle": "eve.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", - "muted": false, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-bob", - "did": "user(2)", - "displayName": "display-bob", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-alice", - "did": "user(3)", - "displayName": "display-alice", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - ], - "subject": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-carol", - "did": "user(0)", - "displayName": "display-carol", - "handle": "carol.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", - "muted": false, - }, - }, -} -`; - -exports[`pds follow views fetches followers 4`] = ` -Object { - "cursor": "0000000000000::bafycid", - "followers": Array [ - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-alice", - "did": "user(1)", - "displayName": "display-alice", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - ], - "subject": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-dan", - "did": "user(0)", - "displayName": "display-dan", - "handle": "dan.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", - "muted": false, - }, - }, -} -`; - -exports[`pds follow views fetches followers 5`] = ` -Object { - "cursor": "0000000000000::bafycid", - "followers": Array [ - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-dan", - "did": "user(1)", - "displayName": "display-dan", - "handle": "dan.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", - "muted": false, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-alice", - "did": "user(2)", - "displayName": "display-alice", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - ], - "subject": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-eve", - "did": "user(0)", - "displayName": "display-eve", - "handle": "eve.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", - "muted": false, - }, - }, -} -`; - -exports[`pds follow views fetches follows 1`] = ` -Object { - "cursor": "0000000000000::bafycid", - "follows": Array [ - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-eve", - "did": "user(1)", - "displayName": "display-eve", - "handle": "eve.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", - "muted": false, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-dan", - "did": "user(2)", - "displayName": "display-dan", - "handle": "dan.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", - "muted": false, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-carol", - "did": "user(3)", - "displayName": "display-carol", - "handle": "carol.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-bob", - "did": "user(4)", - "displayName": "display-bob", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", - "muted": false, - }, - }, - ], - "subject": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-alice", - "did": "user(0)", - "displayName": "display-alice", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, -} -`; - -exports[`pds follow views fetches follows 2`] = ` -Object { - "cursor": "0000000000000::bafycid", - "follows": Array [ - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-carol", - "did": "user(1)", - "displayName": "display-carol", - "handle": "carol.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", - "muted": false, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-alice", - "did": "user(2)", - "displayName": "display-alice", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - ], - "subject": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-bob", - "did": "user(0)", - "displayName": "display-bob", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", - "muted": false, - }, - }, -} -`; - -exports[`pds follow views fetches follows 3`] = ` -Object { - "cursor": "0000000000000::bafycid", - "follows": Array [ - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-alice", - "did": "user(1)", - "displayName": "display-alice", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - ], - "subject": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-carol", - "did": "user(0)", - "displayName": "display-carol", - "handle": "carol.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", - "muted": false, - }, - }, -} -`; - -exports[`pds follow views fetches follows 4`] = ` -Object { - "cursor": "0000000000000::bafycid", - "follows": Array [ - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-eve", - "did": "user(1)", - "displayName": "display-eve", - "handle": "eve.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", - "muted": false, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-bob", - "did": "user(2)", - "displayName": "display-bob", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-alice", - "did": "user(3)", - "displayName": "display-alice", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - ], - "subject": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-dan", - "did": "user(0)", - "displayName": "display-dan", - "handle": "dan.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", - "muted": false, - }, - }, -} -`; - -exports[`pds follow views fetches follows 5`] = ` -Object { - "cursor": "0000000000000::bafycid", - "follows": Array [ - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-carol", - "did": "user(1)", - "displayName": "display-carol", - "handle": "carol.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", - "muted": false, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-alice", - "did": "user(2)", - "displayName": "display-alice", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - ], - "subject": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "descript-eve", - "did": "user(0)", - "displayName": "display-eve", - "handle": "eve.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", - "muted": false, - }, - }, -} -`; diff --git a/packages/pds/tests/views/__snapshots__/likes.test.ts.snap b/packages/pds/tests/views/__snapshots__/likes.test.ts.snap deleted file mode 100644 index 3a15355409f..00000000000 --- a/packages/pds/tests/views/__snapshots__/likes.test.ts.snap +++ /dev/null @@ -1,120 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`pds like views fetches post likes 1`] = ` -Object { - "cursor": "0000000000000::bafycid", - "likes": Array [ - Object { - "actor": Object { - "did": "user(0)", - "handle": "eve.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "createdAt": "1970-01-01T00:00:00.000Z", - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - Object { - "actor": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(1)", - "muted": false, - }, - }, - "createdAt": "1970-01-01T00:00:00.000Z", - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - Object { - "actor": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", - "muted": false, - }, - }, - "createdAt": "1970-01-01T00:00:00.000Z", - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - Object { - "actor": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "hi im bob label_me", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(6)", - "val": "self-label-a", - }, - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(6)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - "createdAt": "1970-01-01T00:00:00.000Z", - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - ], - "uri": "record(0)", -} -`; - -exports[`pds like views fetches reply likes 1`] = ` -Object { - "cursor": "0000000000000::bafycid", - "likes": Array [ - Object { - "actor": Object { - "did": "user(0)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "createdAt": "1970-01-01T00:00:00.000Z", - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - ], - "uri": "record(0)", -} -`; diff --git a/packages/pds/tests/views/__snapshots__/mute-lists.test.ts.snap b/packages/pds/tests/views/__snapshots__/mute-lists.test.ts.snap deleted file mode 100644 index 6434e1e51b1..00000000000 --- a/packages/pds/tests/views/__snapshots__/mute-lists.test.ts.snap +++ /dev/null @@ -1,599 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`pds views with mutes from mute lists embeds lists in posts 1`] = ` -Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.graph.defs#listView", - "cid": "cids(3)", - "creator": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "description": "new descript", - "indexedAt": "1970-01-01T00:00:00.000Z", - "name": "updated alice mutes", - "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(2)", - "viewer": Object { - "muted": false, - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "list embed!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(0)", - "viewer": Object {}, -} -`; - -exports[`pds views with mutes from mute lists flags mutes in threads 1`] = ` -Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object { - "like": "record(4)", - "repost": "record(3)", - }, - }, - "replies": Array [ - Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": true, - "mutedByList": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "cid": "cids(3)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "name": "alice mutes", - "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(6)", - "viewer": Object { - "muted": true, - }, - }, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "of course", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(5)", - "viewer": Object {}, - }, - }, - Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(11)", - "val": "self-label-a", - }, - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(11)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(10)", - "muted": true, - "mutedByList": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "cid": "cids(3)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "name": "alice mutes", - "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(6)", - "viewer": Object { - "muted": true, - }, - }, - }, - }, - "cid": "cids(4)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(9)", - "val": "test-label", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(9)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(6)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(9)", - "viewer": Object {}, - }, - }, - ], -} -`; - -exports[`pds views with mutes from mute lists returns a users own list mutes 1`] = ` -Object { - "cursor": "0000000000000::bafycid", - "lists": Array [ - Object { - "cid": "cids(0)", - "creator": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "its me!", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "muted": false, - }, - }, - "description": "blah blah", - "indexedAt": "1970-01-01T00:00:00.000Z", - "name": "new list", - "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(0)", - "viewer": Object { - "muted": true, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "cid": "cids(2)", - "creator": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "its me!", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "muted": false, - }, - }, - "description": "big list of mutes", - "indexedAt": "1970-01-01T00:00:00.000Z", - "name": "alice mutes", - "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(3)", - "viewer": Object { - "muted": true, - }, - }, - ], -} -`; - -exports[`pds views with mutes from mute lists returns lists associated with a user 1`] = ` -Object { - "cursor": "0000000000000::bafycid", - "lists": Array [ - Object { - "cid": "cids(0)", - "creator": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "its me!", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "muted": false, - }, - }, - "description": "blah blah", - "indexedAt": "1970-01-01T00:00:00.000Z", - "name": "new list", - "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(0)", - "viewer": Object { - "muted": false, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "cid": "cids(2)", - "creator": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "its me!", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "muted": false, - }, - }, - "description": "big list of mutes", - "indexedAt": "1970-01-01T00:00:00.000Z", - "name": "alice mutes", - "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(3)", - "viewer": Object { - "muted": true, - }, - }, - ], -} -`; - -exports[`pds views with mutes from mute lists returns the contents of a list 1`] = ` -Object { - "cursor": "0000000000000::bafycid", - "items": Array [ - Object { - "subject": Object { - "did": "user(0)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "muted": true, - "mutedByList": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "name": "alice mutes", - "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(0)", - "viewer": Object { - "muted": true, - }, - }, - }, - }, - }, - Object { - "subject": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "hi im bob label_me", - "did": "user(1)", - "displayName": "bobby", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(2)", - "muted": true, - "mutedByList": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "name": "alice mutes", - "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(0)", - "viewer": Object { - "muted": true, - }, - }, - }, - }, - }, - ], - "list": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "cid": "cids(0)", - "creator": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "its me!", - "did": "user(2)", - "displayName": "ali", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(2)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(5)", - "val": "self-label-a", - }, - Object { - "cid": "cids(2)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(5)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(4)", - "muted": false, - }, - }, - "description": "big list of mutes", - "indexedAt": "1970-01-01T00:00:00.000Z", - "name": "alice mutes", - "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(0)", - "viewer": Object { - "muted": true, - }, - }, -} -`; diff --git a/packages/pds/tests/views/__snapshots__/mutes.test.ts.snap b/packages/pds/tests/views/__snapshots__/mutes.test.ts.snap deleted file mode 100644 index 8929a3c347b..00000000000 --- a/packages/pds/tests/views/__snapshots__/mutes.test.ts.snap +++ /dev/null @@ -1,75 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`mute views fetches mutes for the logged-in user. 1`] = ` -Array [ - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "Dr. Lowell DuBuque", - "handle": "elta48.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": true, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "Sally Funk", - "handle": "magnus53.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": true, - }, - }, - Object { - "did": "user(2)", - "handle": "nicolas-krajcik10.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": true, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "Patrick Sawayn", - "handle": "jeffrey-sawayn87.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": true, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(4)", - "displayName": "Kim Streich", - "handle": "adrienne49.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": true, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(5)", - "displayName": "Carlton Abernathy IV", - "handle": "aliya-hodkiewicz.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": true, - }, - }, -] -`; diff --git a/packages/pds/tests/views/__snapshots__/notifications.test.ts.snap b/packages/pds/tests/views/__snapshots__/notifications.test.ts.snap deleted file mode 100644 index 117bcdfbb79..00000000000 --- a/packages/pds/tests/views/__snapshots__/notifications.test.ts.snap +++ /dev/null @@ -1,1922 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`pds notification views fetches notifications omitting mentions and replies for taken-down posts 1`] = ` -Array [ - Object { - "author": Object { - "did": "user(0)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "reply", - "reasonSubject": "record(3)", - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - }, - "text": "indeed", - }, - "uri": "record(0)", - }, - Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(3)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "repost", - "reasonSubject": "record(3)", - "record": Object { - "$type": "app.bsky.feed.repost", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(2)", - "uri": "record(3)", - }, - }, - "uri": "record(5)", - }, - Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(4)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "repost", - "reasonSubject": "record(4)", - "record": Object { - "$type": "app.bsky.feed.repost", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - }, - "uri": "record(7)", - }, - Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "hi im bob label_me", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(6)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(11)", - "val": "self-label-a", - }, - Object { - "cid": "cids(6)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(11)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(10)", - "following": "record(9)", - "muted": false, - }, - }, - "cid": "cids(5)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [ - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(8)", - "val": "test-label", - }, - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(8)", - "val": "test-label-2", - }, - ], - "reason": "reply", - "reasonSubject": "record(4)", - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(7)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - "root": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "uri": "record(8)", - }, - Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(8)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(4)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - }, - "uri": "record(12)", - }, - Object { - "author": Object { - "did": "user(0)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(9)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(14)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(10)", - "uri": "record(14)", - }, - }, - "uri": "record(13)", - }, - Object { - "author": Object { - "did": "user(0)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(11)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(4)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - }, - "uri": "record(15)", - }, - Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "hi im bob label_me", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(6)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(11)", - "val": "self-label-a", - }, - Object { - "cid": "cids(6)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(11)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(10)", - "following": "record(9)", - "muted": false, - }, - }, - "cid": "cids(12)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(14)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(10)", - "uri": "record(14)", - }, - }, - "uri": "record(16)", - }, - Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "hi im bob label_me", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(6)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(11)", - "val": "self-label-a", - }, - Object { - "cid": "cids(6)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(11)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(10)", - "following": "record(9)", - "muted": false, - }, - }, - "cid": "cids(13)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(4)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - }, - "uri": "record(17)", - }, - Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "hi im bob label_me", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(6)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(11)", - "val": "self-label-a", - }, - Object { - "cid": "cids(6)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(11)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(10)", - "following": "record(9)", - "muted": false, - }, - }, - "cid": "cids(14)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "follow", - "record": Object { - "$type": "app.bsky.graph.follow", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": "user(3)", - }, - "uri": "record(10)", - }, - Object { - "author": Object { - "did": "user(0)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(15)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "follow", - "record": Object { - "$type": "app.bsky.graph.follow", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": "user(3)", - }, - "uri": "record(2)", - }, -] -`; - -exports[`pds notification views fetches notifications omitting records by a muted user 1`] = ` -Array [ - Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "hi im bob label_me", - "did": "user(0)", - "displayName": "bobby", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [ - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(0)", - "val": "test-label", - }, - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(0)", - "val": "test-label-2", - }, - ], - "reason": "reply", - "reasonSubject": "record(4)", - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(2)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(4)", - }, - "root": Object { - "cid": "cids(3)", - "uri": "record(4)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "uri": "record(0)", - }, - Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "hi im bob label_me", - "did": "user(0)", - "displayName": "bobby", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(4)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(6)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(5)", - "uri": "record(6)", - }, - }, - "uri": "record(5)", - }, - Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "hi im bob label_me", - "did": "user(0)", - "displayName": "bobby", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(6)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(4)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(3)", - "uri": "record(4)", - }, - }, - "uri": "record(7)", - }, - Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "hi im bob label_me", - "did": "user(0)", - "displayName": "bobby", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(7)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "follow", - "record": Object { - "$type": "app.bsky.graph.follow", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": "user(1)", - }, - "uri": "record(2)", - }, -] -`; - -exports[`pds notification views fetches notifications with a last-seen 1`] = ` -Array [ - Object { - "author": Object { - "did": "user(0)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "reply", - "reasonSubject": "record(3)", - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - }, - "text": "indeed", - }, - "uri": "record(0)", - }, - Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(3)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "repost", - "reasonSubject": "record(3)", - "record": Object { - "$type": "app.bsky.feed.repost", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(2)", - "uri": "record(3)", - }, - }, - "uri": "record(5)", - }, - Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(4)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "repost", - "reasonSubject": "record(4)", - "record": Object { - "$type": "app.bsky.feed.repost", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - }, - "uri": "record(7)", - }, - Object { - "author": Object { - "did": "user(0)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(5)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": true, - "labels": Array [], - "reason": "reply", - "reasonSubject": "record(4)", - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - "root": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - }, - "text": "of course", - }, - "uri": "record(8)", - }, - Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "hi im bob label_me", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(12)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(12)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "muted": false, - }, - }, - "cid": "cids(6)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": true, - "labels": Array [ - Object { - "cid": "cids(6)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(9)", - "val": "test-label", - }, - Object { - "cid": "cids(6)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(9)", - "val": "test-label-2", - }, - ], - "reason": "reply", - "reasonSubject": "record(4)", - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(8)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - "root": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "uri": "record(9)", - }, - Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(9)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": true, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(4)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - }, - "uri": "record(13)", - }, - Object { - "author": Object { - "did": "user(0)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(10)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": true, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(15)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(11)", - "uri": "record(15)", - }, - }, - "uri": "record(14)", - }, - Object { - "author": Object { - "did": "user(0)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(12)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": true, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(4)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - }, - "uri": "record(16)", - }, - Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "hi im bob label_me", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(12)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(12)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "muted": false, - }, - }, - "cid": "cids(13)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": true, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(15)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(11)", - "uri": "record(15)", - }, - }, - "uri": "record(17)", - }, - Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "hi im bob label_me", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(12)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(12)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "muted": false, - }, - }, - "cid": "cids(14)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": true, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(4)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - }, - "uri": "record(18)", - }, - Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(15)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": true, - "labels": Array [], - "reason": "mention", - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(16)", - "uri": "record(20)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(3)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - "uri": "record(19)", - }, - Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "hi im bob label_me", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(12)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(12)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "muted": false, - }, - }, - "cid": "cids(17)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": true, - "labels": Array [], - "reason": "follow", - "record": Object { - "$type": "app.bsky.graph.follow", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": "user(3)", - }, - "uri": "record(11)", - }, - Object { - "author": Object { - "did": "user(0)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(18)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": true, - "labels": Array [], - "reason": "follow", - "record": Object { - "$type": "app.bsky.graph.follow", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": "user(3)", - }, - "uri": "record(2)", - }, -] -`; - -exports[`pds notification views fetches notifications without a last-seen 1`] = ` -Array [ - Object { - "author": Object { - "did": "user(0)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "reply", - "reasonSubject": "record(3)", - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - }, - "text": "indeed", - }, - "uri": "record(0)", - }, - Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(3)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "repost", - "reasonSubject": "record(3)", - "record": Object { - "$type": "app.bsky.feed.repost", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(2)", - "uri": "record(3)", - }, - }, - "uri": "record(5)", - }, - Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(4)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "repost", - "reasonSubject": "record(4)", - "record": Object { - "$type": "app.bsky.feed.repost", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - }, - "uri": "record(7)", - }, - Object { - "author": Object { - "did": "user(0)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(5)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "reply", - "reasonSubject": "record(4)", - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - "root": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - }, - "text": "of course", - }, - "uri": "record(8)", - }, - Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "hi im bob label_me", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(12)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(12)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "muted": false, - }, - }, - "cid": "cids(6)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [ - Object { - "cid": "cids(6)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(9)", - "val": "test-label", - }, - Object { - "cid": "cids(6)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(9)", - "val": "test-label-2", - }, - ], - "reason": "reply", - "reasonSubject": "record(4)", - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(8)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - "root": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "uri": "record(9)", - }, - Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(9)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(4)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - }, - "uri": "record(13)", - }, - Object { - "author": Object { - "did": "user(0)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(10)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(15)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(11)", - "uri": "record(15)", - }, - }, - "uri": "record(14)", - }, - Object { - "author": Object { - "did": "user(0)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(12)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(4)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - }, - "uri": "record(16)", - }, - Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "hi im bob label_me", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(12)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(12)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "muted": false, - }, - }, - "cid": "cids(13)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(15)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(11)", - "uri": "record(15)", - }, - }, - "uri": "record(17)", - }, - Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "hi im bob label_me", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(12)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(12)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "muted": false, - }, - }, - "cid": "cids(14)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(4)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(1)", - "uri": "record(4)", - }, - }, - "uri": "record(18)", - }, - Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(15)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "mention", - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(16)", - "uri": "record(20)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(3)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - "uri": "record(19)", - }, - Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "hi im bob label_me", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(12)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(12)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "muted": false, - }, - }, - "cid": "cids(17)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "follow", - "record": Object { - "$type": "app.bsky.graph.follow", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": "user(3)", - }, - "uri": "record(11)", - }, - Object { - "author": Object { - "did": "user(0)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(18)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "follow", - "record": Object { - "$type": "app.bsky.graph.follow", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": "user(3)", - }, - "uri": "record(2)", - }, -] -`; - -exports[`pds notification views generates notifications for quotes 1`] = ` -Object { - "cursor": "0000000000000::bafycid", - "notifications": Array [ - Object { - "author": Object { - "did": "user(0)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "repost", - "reasonSubject": "record(1)", - "record": Object { - "$type": "app.bsky.feed.repost", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(1)", - "uri": "record(1)", - }, - }, - "uri": "record(0)", - }, - Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "its me!", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(4)", - "val": "self-label-a", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(4)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [ - Object { - "cid": "cids(2)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(2)", - "val": "test-label", - }, - ], - "reason": "quote", - "reasonSubject": "record(1)", - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(1)", - "uri": "record(1)", - }, - }, - "text": "yoohoo label_me", - }, - "uri": "record(2)", - }, - Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "its me!", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(4)", - "val": "self-label-a", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(4)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "muted": false, - }, - }, - "cid": "cids(4)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "follow", - "record": Object { - "$type": "app.bsky.graph.follow", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": "user(2)", - }, - "uri": "record(3)", - }, - ], -} -`; diff --git a/packages/pds/tests/views/__snapshots__/posts.test.ts.snap b/packages/pds/tests/views/__snapshots__/posts.test.ts.snap deleted file mode 100644 index 75ff566186e..00000000000 --- a/packages/pds/tests/views/__snapshots__/posts.test.ts.snap +++ /dev/null @@ -1,526 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`pds posts views fetches posts 1`] = ` -Array [ - Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(0)", - "val": "self-label", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label", - }, - ], - }, - "text": "hey there", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(0)", - "viewer": Object {}, - }, - Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(6)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(6)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(3)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(3)", - "viewer": Object {}, - }, - Object { - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", - "muted": false, - }, - }, - "cid": "cids(5)", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(6)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(6)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(3)", - "embeds": Array [], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(3)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(6)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(7)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(3)", - "uri": "record(3)", - }, - }, - }, - "text": "hi im carol", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(7)", - "viewer": Object { - "like": "record(10)", - }, - }, - Object { - "author": Object { - "did": "user(3)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(3)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(12)", - "muted": false, - }, - }, - "cid": "cids(8)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", - "muted": false, - }, - }, - "cid": "cids(5)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(6)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(6)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(3)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(3)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(7)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(6)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(7)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(3)", - "uri": "record(3)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(5)", - "uri": "record(7)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(0)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(11)", - "viewer": Object {}, - }, - Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(9)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(10)", - "uri": "record(14)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(13)", - "viewer": Object {}, - }, -] -`; diff --git a/packages/pds/tests/views/__snapshots__/profile.test.ts.snap b/packages/pds/tests/views/__snapshots__/profile.test.ts.snap deleted file mode 100644 index dbfd30bdbce..00000000000 --- a/packages/pds/tests/views/__snapshots__/profile.test.ts.snap +++ /dev/null @@ -1,286 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`pds profile views creates new profile 1`] = ` -Object { - "did": "user(0)", - "displayName": "danny boy", - "followersCount": 1, - "followsCount": 1, - "handle": "dan.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(0)", - "val": "repo-action-label", - }, - ], - "postsCount": 2, - "viewer": Object { - "blockedBy": false, - "muted": false, - }, -} -`; - -exports[`pds profile views fetches multiple profiles 1`] = ` -Array [ - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "its me!", - "did": "user(0)", - "displayName": "ali", - "followersCount": 2, - "followsCount": 3, - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-a", - }, - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-b", - }, - ], - "postsCount": 4, - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", - "muted": false, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "hi im bob label_me", - "did": "user(1)", - "displayName": "bobby", - "followersCount": 2, - "followsCount": 2, - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "postsCount": 3, - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - Object { - "did": "user(2)", - "followersCount": 2, - "followsCount": 1, - "handle": "carol.test", - "labels": Array [], - "postsCount": 2, - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - Object { - "did": "user(3)", - "followersCount": 1, - "followsCount": 1, - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(3)", - "val": "repo-action-label", - }, - ], - "postsCount": 2, - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "muted": false, - }, - }, -] -`; - -exports[`pds profile views fetches other's profile, with a follow 1`] = ` -Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "its me!", - "did": "user(0)", - "displayName": "ali", - "followersCount": 2, - "followsCount": 3, - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-a", - }, - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-b", - }, - ], - "postsCount": 4, - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", - "muted": false, - }, -} -`; - -exports[`pds profile views fetches other's profile, without a follow 1`] = ` -Object { - "did": "user(0)", - "followersCount": 1, - "followsCount": 1, - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(0)", - "val": "repo-action-label", - }, - ], - "postsCount": 2, - "viewer": Object { - "blockedBy": false, - "followedBy": "record(0)", - "muted": false, - }, -} -`; - -exports[`pds profile views fetches own profile 1`] = ` -Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "its me!", - "did": "user(0)", - "displayName": "ali", - "followersCount": 2, - "followsCount": 3, - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(0)", - "val": "self-label-a", - }, - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(0)", - "val": "self-label-b", - }, - ], - "postsCount": 4, - "viewer": Object { - "blockedBy": false, - "muted": false, - }, -} -`; - -exports[`pds profile views handles avatars & banners 1`] = ` -Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "banner": "https://pds.public.url/image/ejOTK5gy9tlarOXM6MhrPOs0C18LFpfd9qQ9lBrcIBE/rs:fill:3000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "description": "new descript", - "did": "user(0)", - "displayName": "ali", - "followersCount": 2, - "followsCount": 3, - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "postsCount": 4, - "viewer": Object { - "blockedBy": false, - "muted": false, - }, -} -`; - -exports[`pds profile views handles unsetting profile fields 1`] = ` -Object { - "did": "user(0)", - "followersCount": 2, - "followsCount": 3, - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "postsCount": 4, - "viewer": Object { - "blockedBy": false, - "muted": false, - }, -} -`; - -exports[`pds profile views updates profile 1`] = ` -Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "new descript", - "did": "user(0)", - "displayName": "ali", - "followersCount": 2, - "followsCount": 3, - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "postsCount": 4, - "viewer": Object { - "blockedBy": false, - "muted": false, - }, -} -`; diff --git a/packages/pds/tests/views/__snapshots__/reposts.test.ts.snap b/packages/pds/tests/views/__snapshots__/reposts.test.ts.snap deleted file mode 100644 index 69927a4b5ae..00000000000 --- a/packages/pds/tests/views/__snapshots__/reposts.test.ts.snap +++ /dev/null @@ -1,108 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`pds repost views fetches reposted-by for a post 1`] = ` -Array [ - Object { - "did": "user(0)", - "handle": "eve.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(0)", - "muted": false, - }, - }, - Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "description": "hi im bob label_me", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(5)", - "val": "self-label-a", - }, - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(5)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", - "muted": false, - }, - }, -] -`; - -exports[`pds repost views fetches reposted-by for a reply 1`] = ` -Array [ - Object { - "did": "user(0)", - "handle": "eve.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(0)", - "muted": false, - }, - }, -] -`; diff --git a/packages/pds/tests/views/__snapshots__/thread.test.ts.snap b/packages/pds/tests/views/__snapshots__/thread.test.ts.snap deleted file mode 100644 index df37cbb712d..00000000000 --- a/packages/pds/tests/views/__snapshots__/thread.test.ts.snap +++ /dev/null @@ -1,1883 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`pds thread views blocks ancestors by actor takedown 1`] = ` -Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "parent": Object { - "$type": "app.bsky.feed.defs#notFoundPost", - "notFound": true, - "uri": "record(5)", - }, - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(5)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(4)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 2, - "uri": "record(0)", - "viewer": Object { - "repost": "record(6)", - }, - }, - "replies": Array [], -} -`; - -exports[`pds thread views blocks ancestors by record takedown 1`] = ` -Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "parent": Object { - "$type": "app.bsky.feed.defs#notFoundPost", - "notFound": true, - "uri": "record(5)", - }, - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(5)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(4)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 2, - "uri": "record(0)", - "viewer": Object { - "repost": "record(6)", - }, - }, - "replies": Array [], -} -`; - -exports[`pds thread views blocks replies by actor takedown 1`] = ` -Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object { - "like": "record(4)", - }, - }, - "replies": Array [ - Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(6)", - "val": "self-label-a", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(6)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(2)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(5)", - "val": "test-label", - }, - Object { - "cid": "cids(2)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(5)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(4)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(5)", - "viewer": Object {}, - }, - "replies": Array [ - Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(5)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(5)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 2, - "uri": "record(7)", - "viewer": Object { - "repost": "record(8)", - }, - }, - "replies": Array [], - }, - ], - }, - ], -} -`; - -exports[`pds thread views blocks replies by record takedown 1`] = ` -Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object { - "like": "record(4)", - }, - }, - "replies": Array [ - Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(6)", - "val": "self-label-a", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(6)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(2)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(5)", - "val": "test-label", - }, - Object { - "cid": "cids(2)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(5)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(4)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(5)", - "viewer": Object {}, - }, - "replies": Array [], - }, - ], -} -`; - -exports[`pds thread views fetches ancestors 1`] = ` -Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "parent": Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "parent": Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(4)", - "viewer": Object { - "like": "record(8)", - }, - }, - "replies": Array [], - }, - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(7)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(7)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(3)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(5)", - "val": "test-label", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(5)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(4)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(4)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(5)", - "viewer": Object {}, - }, - "replies": Array [], - }, - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(5)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(4)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 2, - "uri": "record(0)", - "viewer": Object { - "repost": "record(6)", - }, - }, - "replies": Array [], -} -`; - -exports[`pds thread views fetches deep post thread 1`] = ` -Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object { - "like": "record(4)", - }, - }, - "replies": Array [ - Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "of course", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(5)", - "viewer": Object {}, - }, - "replies": Array [], - }, - Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(8)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(8)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(3)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(7)", - "val": "test-label", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(7)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(7)", - "viewer": Object {}, - }, - "replies": Array [ - Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(6)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(7)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 2, - "uri": "record(9)", - "viewer": Object { - "repost": "record(10)", - }, - }, - "replies": Array [], - }, - ], - }, - ], -} -`; - -exports[`pds thread views fetches shallow ancestors 1`] = ` -Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "parent": Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(7)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(7)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(3)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(5)", - "val": "test-label", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(5)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(4)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(4)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(5)", - "viewer": Object {}, - }, - "replies": Array [], - }, - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(5)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(4)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 2, - "uri": "record(0)", - "viewer": Object { - "repost": "record(6)", - }, - }, - "replies": Array [], -} -`; - -exports[`pds thread views fetches shallow post thread 1`] = ` -Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object { - "like": "record(4)", - }, - }, - "replies": Array [ - Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "of course", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(5)", - "viewer": Object {}, - }, - }, - Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(8)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(8)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(3)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(7)", - "val": "test-label", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(7)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(7)", - "viewer": Object {}, - }, - }, - ], -} -`; - -exports[`pds thread views handles deleted posts correctly 1`] = ` -Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "text": "Deletion thread", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(0)", - "viewer": Object {}, - }, - "replies": Array [ - Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(5)", - "val": "self-label-a", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(5)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "Reply", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(4)", - "viewer": Object {}, - }, - "replies": Array [ - Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(4)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(4)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "Reply reply", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(6)", - "viewer": Object {}, - }, - "replies": Array [], - }, - ], - }, - ], -} -`; - -exports[`pds thread views handles deleted posts correctly 2`] = ` -Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "text": "Deletion thread", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(0)", - "viewer": Object {}, - }, - "replies": Array [], -} -`; - -exports[`pds thread views handles deleted posts correctly 3`] = ` -Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "parent": Object { - "$type": "app.bsky.feed.defs#notFoundPost", - "notFound": true, - "uri": "record(5)", - }, - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(5)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(4)", - }, - }, - "text": "Reply reply", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(0)", - "viewer": Object {}, - }, - "replies": Array [], -} -`; - -exports[`pds thread views includes the muted status of post authors. 1`] = ` -Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": true, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object { - "like": "record(4)", - }, - }, - "replies": Array [ - Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "of course", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(5)", - "viewer": Object {}, - }, - "replies": Array [], - }, - Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(8)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(8)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(3)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(7)", - "val": "test-label", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(7)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(7)", - "viewer": Object {}, - }, - "replies": Array [ - Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(3)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": true, - }, - }, - "cid": "cids(6)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(7)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 2, - "uri": "record(9)", - "viewer": Object { - "repost": "record(10)", - }, - }, - "replies": Array [], - }, - ], - }, - ], -} -`; diff --git a/packages/pds/tests/views/__snapshots__/timeline.test.ts.snap b/packages/pds/tests/views/__snapshots__/timeline.test.ts.snap deleted file mode 100644 index 73f5e4f6748..00000000000 --- a/packages/pds/tests/views/__snapshots__/timeline.test.ts.snap +++ /dev/null @@ -1,7343 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`timeline views blocks posts, reposts, replies by actor takedown 1`] = ` -Array [ - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object {}, - }, - "reason": Object { - "$type": "app.bsky.feed.defs#reasonRepost", - "by": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - "reason": Object { - "$type": "app.bsky.feed.defs#reasonRepost", - "by": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(4)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(5)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewNotFound", - "notFound": true, - "uri": "record(7)", - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(6)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(6)", - "uri": "record(7)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(0)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(5)", - "val": "test-label", - }, - ], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(5)", - "uri": "record(6)", - }, - }, - "text": "yoohoo label_me", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(5)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(5)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewNotFound", - "notFound": true, - "uri": "record(7)", - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(6)", - "uri": "record(7)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(0)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(6)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(7)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "text": "dan here!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(8)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(8)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(8)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(9)", - "val": "self-label", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label", - }, - ], - }, - "text": "hey there", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(9)", - "viewer": Object {}, - }, - }, -] -`; - -exports[`timeline views blocks posts, reposts, replies by record takedown. 1`] = ` -Array [ - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object {}, - }, - "reason": Object { - "$type": "app.bsky.feed.defs#reasonRepost", - "by": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - "reason": Object { - "$type": "app.bsky.feed.defs#reasonRepost", - "by": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(4)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "of course", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(5)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(5)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewNotFound", - "notFound": true, - "uri": "record(9)", - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(8)", - "val": "test-label", - }, - ], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(6)", - "uri": "record(9)", - }, - }, - "text": "yoohoo label_me", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(8)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(8)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(13)", - "val": "self-label-a", - }, - Object { - "cid": "cids(8)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(13)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(12)", - "following": "record(11)", - "muted": false, - }, - }, - "cid": "cids(7)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000+00:00", - "text": "bobby boy here", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(10)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(9)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "text": "dan here!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(14)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(10)", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(8)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(13)", - "val": "self-label-a", - }, - Object { - "cid": "cids(8)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(13)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(12)", - "following": "record(11)", - "muted": false, - }, - }, - "cid": "cids(13)", - "embeds": Array [], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(13)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(16)", - "val": "kind", - }, - ], - "uri": "record(16)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(10)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(15)", - "val": "kind", - }, - ], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(11)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(12)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(13)", - "uri": "record(16)", - }, - }, - }, - "text": "hi im carol", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(15)", - "viewer": Object { - "like": "record(17)", - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(8)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(13)", - "val": "self-label-a", - }, - Object { - "cid": "cids(8)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(13)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(12)", - "following": "record(11)", - "muted": false, - }, - }, - "cid": "cids(13)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(13)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(16)", - "val": "kind", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(16)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(14)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(14)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(18)", - "val": "self-label", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label", - }, - ], - }, - "text": "hey there", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(18)", - "viewer": Object {}, - }, - }, -] -`; - -exports[`timeline views fetches authenticated user's home feed w/ reverse-chronological algorithm 1`] = ` -Array [ - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object {}, - }, - "reason": Object { - "$type": "app.bsky.feed.defs#reasonRepost", - "by": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(3)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(3)", - "val": "test-label", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(3)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(3)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - "reason": Object { - "$type": "app.bsky.feed.defs#reasonRepost", - "by": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(6)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(3)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "muted": false, - }, - }, - "cid": "cids(7)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(9)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(9)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(12)", - "val": "kind", - }, - ], - "uri": "record(12)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(9)", - "val": "kind", - }, - ], - "uri": "record(9)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(8)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(9)", - "uri": "record(12)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(7)", - "uri": "record(9)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(0)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(8)", - "viewer": Object {}, - }, - "reason": Object { - "$type": "app.bsky.feed.defs#reasonRepost", - "by": Object { - "did": "user(3)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "muted": false, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(3)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(3)", - "val": "test-label", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(3)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(3)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(3)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "muted": false, - }, - }, - "cid": "cids(10)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "of course", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(13)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(3)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(3)", - "val": "test-label", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(3)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(3)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(11)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(6)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(3)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "muted": false, - }, - }, - "cid": "cids(7)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(9)", - "val": "kind", - }, - ], - "uri": "record(9)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(8)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(9)", - "uri": "record(12)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(8)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(7)", - "uri": "record(9)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(0)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(11)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(14)", - "val": "test-label", - }, - ], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(6)", - "uri": "record(8)", - }, - }, - "text": "yoohoo label_me", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(14)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(12)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000+00:00", - "text": "bobby boy here", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(15)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(6)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(3)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "muted": false, - }, - }, - "cid": "cids(7)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(9)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(9)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(12)", - "val": "kind", - }, - ], - "uri": "record(12)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(9)", - "val": "kind", - }, - ], - "uri": "record(9)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(8)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(9)", - "uri": "record(12)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(7)", - "uri": "record(9)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(0)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(8)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(13)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "text": "dan here!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(16)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(3)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "muted": false, - }, - }, - "cid": "cids(7)", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(9)", - "embeds": Array [], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(9)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(12)", - "val": "kind", - }, - ], - "uri": "record(12)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(9)", - "val": "kind", - }, - ], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(8)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(9)", - "uri": "record(12)", - }, - }, - }, - "text": "hi im carol", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(9)", - "viewer": Object { - "like": "record(17)", - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(9)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(9)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(12)", - "val": "kind", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(12)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(14)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(14)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(18)", - "val": "self-label", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label", - }, - ], - }, - "text": "hey there", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(18)", - "viewer": Object {}, - }, - }, -] -`; - -exports[`timeline views fetches authenticated user's home feed w/ reverse-chronological algorithm 2`] = ` -Array [ - Object { - "post": Object { - "author": Object { - "did": "user(0)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(0)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "following": "record(3)", - "muted": false, - }, - }, - "cid": "cids(1)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(5)", - "val": "self-label-a", - }, - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(5)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(4)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(4)", - "val": "kind", - }, - ], - "uri": "record(4)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(2)", - "val": "kind", - }, - ], - "uri": "record(2)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(2)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(3)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(4)", - "uri": "record(4)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(1)", - "uri": "record(2)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(1)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object {}, - }, - "reason": Object { - "$type": "app.bsky.feed.defs#reasonRepost", - "by": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "following": "record(3)", - "muted": false, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(6)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(9)", - "uri": "record(11)", - }, - "root": Object { - "cid": "cids(8)", - "uri": "record(10)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(6)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(5)", - "val": "self-label-a", - }, - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(5)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(9)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(9)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(11)", - "val": "test-label", - }, - Object { - "cid": "cids(9)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(11)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(2)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(8)", - "uri": "record(10)", - }, - "root": Object { - "cid": "cids(8)", - "uri": "record(10)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(11)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(8)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(10)", - "viewer": Object { - "like": "record(12)", - }, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "following": "record(3)", - "muted": false, - }, - }, - "cid": "cids(10)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(8)", - "uri": "record(10)", - }, - "root": Object { - "cid": "cids(8)", - "uri": "record(10)", - }, - }, - "text": "of course", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(13)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(8)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(10)", - "viewer": Object { - "like": "record(12)", - }, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(8)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(10)", - "viewer": Object { - "like": "record(12)", - }, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(5)", - "val": "self-label-a", - }, - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(5)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(9)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(9)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(11)", - "val": "test-label", - }, - Object { - "cid": "cids(9)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(11)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(2)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(8)", - "uri": "record(10)", - }, - "root": Object { - "cid": "cids(8)", - "uri": "record(10)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(11)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(8)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(10)", - "viewer": Object { - "like": "record(12)", - }, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(8)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(10)", - "viewer": Object { - "like": "record(12)", - }, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(11)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(0)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(0)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "following": "record(3)", - "muted": false, - }, - }, - "cid": "cids(1)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(2)", - "val": "kind", - }, - ], - "uri": "record(2)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(2)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(3)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(4)", - "uri": "record(4)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(0)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(1)", - "uri": "record(2)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(1)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(11)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(14)", - "val": "test-label", - }, - ], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "yoohoo label_me", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(14)", - "viewer": Object { - "like": "record(15)", - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(5)", - "val": "self-label-a", - }, - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(5)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(12)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000+00:00", - "text": "bobby boy here", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(16)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(8)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(10)", - "viewer": Object { - "like": "record(12)", - }, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "following": "record(3)", - "muted": false, - }, - }, - "cid": "cids(1)", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(5)", - "val": "self-label-a", - }, - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(5)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(4)", - "embeds": Array [], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(4)", - "val": "kind", - }, - ], - "uri": "record(4)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(2)", - "val": "kind", - }, - ], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(2)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(3)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(4)", - "uri": "record(4)", - }, - }, - }, - "text": "hi im carol", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(2)", - "viewer": Object { - "like": "record(17)", - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(5)", - "val": "self-label-a", - }, - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(5)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(4)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(4)", - "val": "kind", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(4)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(13)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(13)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(18)", - "val": "self-label", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label", - }, - ], - }, - "text": "hey there", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(18)", - "viewer": Object {}, - }, - }, -] -`; - -exports[`timeline views fetches authenticated user's home feed w/ reverse-chronological algorithm 3`] = ` -Array [ - Object { - "post": Object { - "author": Object { - "did": "user(0)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(0)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(1)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(4)", - "val": "self-label-a", - }, - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(4)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "muted": false, - }, - }, - "cid": "cids(4)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(2)", - "val": "kind", - }, - ], - "uri": "record(2)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(1)", - "val": "kind", - }, - ], - "uri": "record(1)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(2)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(3)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(4)", - "uri": "record(2)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(1)", - "uri": "record(1)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(1)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object { - "repost": "record(5)", - }, - }, - "reason": Object { - "$type": "app.bsky.feed.defs#reasonRepost", - "by": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(6)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(9)", - "uri": "record(11)", - }, - "root": Object { - "cid": "cids(8)", - "uri": "record(10)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(6)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(4)", - "val": "self-label-a", - }, - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(4)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "muted": false, - }, - }, - "cid": "cids(9)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(9)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(11)", - "val": "test-label", - }, - Object { - "cid": "cids(9)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(11)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(2)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(8)", - "uri": "record(10)", - }, - "root": Object { - "cid": "cids(8)", - "uri": "record(10)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(11)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(8)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(10)", - "viewer": Object { - "like": "record(12)", - }, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(10)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(8)", - "uri": "record(10)", - }, - "root": Object { - "cid": "cids(8)", - "uri": "record(10)", - }, - }, - "text": "of course", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(13)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(8)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(10)", - "viewer": Object { - "like": "record(12)", - }, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(8)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(10)", - "viewer": Object { - "like": "record(12)", - }, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(11)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(0)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(0)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(1)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(1)", - "val": "kind", - }, - ], - "uri": "record(1)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(2)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(3)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(4)", - "uri": "record(2)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(0)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(1)", - "uri": "record(1)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(1)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(11)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(14)", - "val": "test-label", - }, - ], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "yoohoo label_me", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(14)", - "viewer": Object { - "like": "record(15)", - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(8)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(10)", - "viewer": Object { - "like": "record(12)", - }, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(1)", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(4)", - "val": "self-label-a", - }, - Object { - "cid": "cids(5)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(3)", - "uri": "record(4)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "muted": false, - }, - }, - "cid": "cids(4)", - "embeds": Array [], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(2)", - "val": "kind", - }, - ], - "uri": "record(2)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(1)", - "val": "kind", - }, - ], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(2)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(3)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(4)", - "uri": "record(2)", - }, - }, - }, - "text": "hi im carol", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(1)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(12)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(12)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(1)", - "uri": "record(16)", - "val": "self-label", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label", - }, - ], - }, - "text": "hey there", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(16)", - "viewer": Object {}, - }, - }, -] -`; - -exports[`timeline views fetches authenticated user's home feed w/ reverse-chronological algorithm 4`] = ` -Array [ - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(4)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(3)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object { - "repost": "record(5)", - }, - }, - "reason": Object { - "$type": "app.bsky.feed.defs#reasonRepost", - "by": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(8)", - "muted": false, - }, - }, - "cid": "cids(3)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(4)", - "val": "test-label", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(4)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(3)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(4)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(3)", - "viewer": Object { - "like": "record(7)", - "repost": "record(6)", - }, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(3)", - "viewer": Object { - "like": "record(7)", - "repost": "record(6)", - }, - }, - "reason": Object { - "$type": "app.bsky.feed.defs#reasonRepost", - "by": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(8)", - "muted": false, - }, - }, - "cid": "cids(3)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(4)", - "val": "test-label", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(4)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(3)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(4)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(3)", - "viewer": Object { - "like": "record(7)", - "repost": "record(6)", - }, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(2)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(3)", - "viewer": Object { - "like": "record(7)", - "repost": "record(6)", - }, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(8)", - "muted": false, - }, - }, - "cid": "cids(6)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000+00:00", - "text": "bobby boy here", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(10)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(7)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(3)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(8)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(8)", - "muted": false, - }, - }, - "cid": "cids(10)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(10)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(13)", - "val": "kind", - }, - ], - "uri": "record(13)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(8)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(12)", - "val": "kind", - }, - ], - "uri": "record(12)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(9)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(10)", - "uri": "record(13)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(8)", - "uri": "record(12)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(0)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(11)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(11)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "text": "dan here!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(14)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(9)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(9)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(8)", - "muted": false, - }, - }, - "cid": "cids(10)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(10)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(13)", - "val": "kind", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(13)", - "viewer": Object {}, - }, - }, -] -`; - -exports[`timeline views omits posts and reposts of muted authors. 1`] = ` -Array [ - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object {}, - }, - "reason": Object { - "$type": "app.bsky.feed.defs#reasonRepost", - "by": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": true, - }, - }, - "cid": "cids(3)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(3)", - "val": "test-label", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(3)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(3)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - "reason": Object { - "$type": "app.bsky.feed.defs#reasonRepost", - "by": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(3)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": true, - }, - }, - "cid": "cids(3)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(3)", - "val": "test-label", - }, - Object { - "cid": "cids(3)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(3)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(3)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(6)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(7)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(3)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(12)", - "following": "record(11)", - "muted": true, - }, - }, - "cid": "cids(8)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(8)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(10)", - "val": "kind", - }, - ], - "uri": "record(10)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(9)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(10)", - "uri": "record(13)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(9)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(8)", - "uri": "record(10)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(0)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(6)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(8)", - "val": "test-label", - }, - ], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(7)", - "uri": "record(9)", - }, - }, - "text": "yoohoo label_me", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(8)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(7)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(3)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(12)", - "following": "record(11)", - "muted": true, - }, - }, - "cid": "cids(8)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-a", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": true, - }, - }, - "cid": "cids(10)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(10)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(13)", - "val": "kind", - }, - ], - "uri": "record(13)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(8)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(10)", - "val": "kind", - }, - ], - "uri": "record(10)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(9)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(10)", - "uri": "record(13)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(8)", - "uri": "record(10)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(0)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(9)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(11)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "text": "dan here!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(14)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [ - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-a", - }, - Object { - "cid": "cids(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(1)", - "val": "self-label-b", - }, - ], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(12)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(12)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(15)", - "val": "self-label", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label", - }, - ], - }, - "text": "hey there", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(15)", - "viewer": Object {}, - }, - }, -] -`; diff --git a/packages/pds/tests/views/actor-likes.test.ts b/packages/pds/tests/views/actor-likes.test.ts deleted file mode 100644 index 15e2dc3e934..00000000000 --- a/packages/pds/tests/views/actor-likes.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import AtpAgent, { AtUri } from '@atproto/api' -import { runTestServer, CloseFn } from '../_util' -import { SeedClient } from '../seeds/client' -import basicSeed from '../seeds/basic' - -describe('pds actor likes feed views', () => { - let agent: AtpAgent - let close: CloseFn - let sc: SeedClient - - // account dids, for convenience - let alice: string - let bob: string - let carol: string - - beforeAll(async () => { - const server = await runTestServer({ - dbPostgresSchema: 'pds_views_actor_likes', - }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await basicSeed(sc) - alice = sc.dids.alice - bob = sc.dids.bob - carol = sc.dids.carol - await server.processAll() - }) - - afterAll(async () => { - await close() - }) - - it('returns posts liked by actor', async () => { - const { - data: { feed: bobLikes }, - } = await agent.api.app.bsky.feed.getActorLikes( - { actor: sc.accounts[bob].handle }, - { headers: sc.getHeaders(bob) }, - ) - - expect(bobLikes).toHaveLength(3) - - await expect( - agent.api.app.bsky.feed.getActorLikes( - { actor: sc.accounts[bob].handle }, - { headers: sc.getHeaders(carol) }, - ), - ).rejects.toThrow('Profile not found') - }) - - it('viewer has blocked author of liked post(s)', async () => { - const bobBlocksAlice = await agent.api.app.bsky.graph.block.create( - { - repo: bob, // bob blocks alice - }, - { - subject: alice, - createdAt: new Date().toISOString(), - }, - sc.getHeaders(bob), - ) - - const { - data: { feed }, - } = await agent.api.app.bsky.feed.getActorLikes( - { actor: sc.accounts[bob].handle }, - { headers: sc.getHeaders(bob) }, - ) - - expect( - feed.every((item) => { - return item.post.author.did !== alice - }), - ).toBe(true) - - // unblock - await agent.api.app.bsky.graph.block.delete( - { repo: bob, rkey: new AtUri(bobBlocksAlice.uri).rkey }, - sc.getHeaders(bob), - ) - }) - - it('liked post author has blocked viewer', async () => { - const aliceBlocksBob = await agent.api.app.bsky.graph.block.create( - { - repo: alice, // alice blocks bob - }, - { - subject: bob, - createdAt: new Date().toISOString(), - }, - sc.getHeaders(alice), - ) - - const { - data: { feed }, - } = await agent.api.app.bsky.feed.getActorLikes( - { actor: sc.accounts[bob].handle }, - { headers: sc.getHeaders(bob) }, - ) - - expect( - feed.every((item) => { - return item.post.author.did !== alice - }), - ).toBe(true) - - // unblock - await agent.api.app.bsky.graph.block.delete( - { repo: alice, rkey: new AtUri(aliceBlocksBob.uri).rkey }, - sc.getHeaders(alice), - ) - }) -}) diff --git a/packages/pds/tests/views/actor-search.test.ts b/packages/pds/tests/views/actor-search.test.ts deleted file mode 100644 index d110d8e2e68..00000000000 --- a/packages/pds/tests/views/actor-search.test.ts +++ /dev/null @@ -1,464 +0,0 @@ -import AtpAgent from '@atproto/api' -import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' -import { - runTestServer, - forSnapshot, - CloseFn, - paginateAll, - adminAuth, -} from '../_util' -import { SeedClient } from '../seeds/client' -import usersBulkSeed from '../seeds/users-bulk' -import { Database } from '../../src' - -describe('pds user search views', () => { - let agent: AtpAgent - let db: Database - let close: CloseFn - let sc: SeedClient - let headers: { [s: string]: string } - - beforeAll(async () => { - const server = await runTestServer({ - dbPostgresSchema: 'views_user_search', - }) - close = server.close - db = server.ctx.db - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await usersBulkSeed(sc) - headers = sc.getHeaders(Object.values(sc.dids)[0]) - await server.processAll() - }) - - afterAll(async () => { - await close() - }) - - it('typeahead gives relevant results', async () => { - const result = await agent.api.app.bsky.actor.searchActorsTypeahead( - { term: 'car' }, - { headers }, - ) - - const handles = result.data.actors.map((u) => u.handle) - - const shouldContain = [ - 'cara-wiegand69.test', - 'eudora-dietrich4.test', // Carol Littel - 'shane-torphy52.test', // Sadie Carter - 'aliya-hodkiewicz.test', // Carlton Abernathy IV - 'carlos6.test', - 'carolina-mcdermott77.test', - ] - - shouldContain.forEach((handle) => expect(handles).toContain(handle)) - - if (db.dialect === 'pg') { - expect(handles).toContain('cayla-marquardt39.test') // Fuzzy match supported by postgres - } else { - expect(handles).not.toContain('cayla-marquardt39.test') - } - - const shouldNotContain = [ - 'sven70.test', - 'hilario84.test', - 'santa-hermann78.test', - 'dylan61.test', - 'preston-harris.test', - 'loyce95.test', - 'melyna-zboncak.test', - ] - - shouldNotContain.forEach((handle) => expect(handles).not.toContain(handle)) - - const sorted = result.data.actors.sort((a, b) => - a.handle > b.handle ? 1 : -1, - ) - if (db.dialect === 'pg') { - expect(forSnapshot(sorted)).toEqual(snapTypeaheadPg) - } else { - expect(forSnapshot(sorted)).toEqual(snapTypeaheadSqlite) - } - }) - - it('typeahead gives empty result set when provided empty term', async () => { - const result = await agent.api.app.bsky.actor.searchActorsTypeahead( - { term: '' }, - { headers }, - ) - - expect(result.data.actors).toEqual([]) - }) - - it('typeahead applies limit', async () => { - const full = await agent.api.app.bsky.actor.searchActorsTypeahead( - { term: 'p' }, - { headers }, - ) - - expect(full.data.actors.length).toBeGreaterThan(5) - - const limited = await agent.api.app.bsky.actor.searchActorsTypeahead( - { term: 'p', limit: 5 }, - { headers }, - ) - - // @NOTE it's expected that searchActorsTypeahead doesn't have stable pagination - - const limitedIndexInFull = limited.data.actors.map((needle) => { - return full.data.actors.findIndex( - (haystack) => needle.did === haystack.did, - ) - }) - - // subset exists in full and is monotonic - expect(limitedIndexInFull.every((idx) => idx !== -1)).toEqual(true) - expect(limitedIndexInFull).toEqual( - [...limitedIndexInFull].sort((a, b) => a - b), - ) - }) - - it('search gives relevant results', async () => { - const result = await agent.api.app.bsky.actor.searchActors( - { term: 'car' }, - { headers }, - ) - - const handles = result.data.actors.map((u) => u.handle) - - const shouldContain = [ - 'cara-wiegand69.test', - 'eudora-dietrich4.test', // Carol Littel - 'shane-torphy52.test', // Sadie Carter - 'aliya-hodkiewicz.test', // Carlton Abernathy IV - 'carlos6.test', - 'carolina-mcdermott77.test', - ] - - shouldContain.forEach((handle) => expect(handles).toContain(handle)) - - if (db.dialect === 'pg') { - expect(handles).toContain('cayla-marquardt39.test') // Fuzzy match supported by postgres - } else { - expect(handles).not.toContain('cayla-marquardt39.test') - } - - const shouldNotContain = [ - 'sven70.test', - 'hilario84.test', - 'santa-hermann78.test', - 'dylan61.test', - 'preston-harris.test', - 'loyce95.test', - 'melyna-zboncak.test', - ] - - shouldNotContain.forEach((handle) => expect(handles).not.toContain(handle)) - - const sorted = result.data.actors.sort((a, b) => - a.handle > b.handle ? 1 : -1, - ) - if (db.dialect === 'pg') { - expect(forSnapshot(sorted)).toEqual(snapSearchPg) - } else { - expect(forSnapshot(sorted)).toEqual(snapSearchSqlite) - } - }) - - it('search gives empty result set when provided empty term', async () => { - const result = await agent.api.app.bsky.actor.searchActors( - { term: '' }, - { headers }, - ) - - expect(result.data.actors).toEqual([]) - }) - - it('paginates', async () => { - const results = (results) => results.flatMap((res) => res.actors) - const paginator = async (cursor?: string) => { - const res = await agent.api.app.bsky.actor.searchActors( - { term: 'p', cursor, limit: 3 }, - { headers }, - ) - return res.data - } - - const paginatedAll = await paginateAll(paginator) - paginatedAll.forEach((res) => - expect(res.actors.length).toBeLessThanOrEqual(3), - ) - - const full = await agent.api.app.bsky.actor.searchActors( - { term: 'p' }, - { headers }, - ) - - expect(full.data.actors.length).toBeGreaterThan(5) - const sortedFull = results([full.data]).sort((a, b) => - a.handle > b.handle ? 1 : -1, - ) - const sortedPaginated = results(paginatedAll).sort((a, b) => - a.handle > b.handle ? 1 : -1, - ) - expect(sortedPaginated).toEqual(sortedFull) - }) - - it('search handles bad input', async () => { - // Mostly for sqlite's benefit, since it uses LIKE and these are special characters that will - // get stripped. This input triggers a special case where there are no "safe" words for sqlite to search on. - const result = await agent.api.app.bsky.actor.searchActors( - { term: ' % _ ' }, - { headers }, - ) - - expect(result.data.actors).toEqual([]) - }) - - it('search blocks by actor takedown', async () => { - await agent.api.com.atproto.admin.takeModerationAction( - { - action: TAKEDOWN, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids['cara-wiegand69.test'], - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - const result = await agent.api.app.bsky.actor.searchActorsTypeahead( - { term: 'car' }, - { headers }, - ) - const handles = result.data.actors.map((u) => u.handle) - expect(handles).toContain('carlos6.test') - expect(handles).toContain('carolina-mcdermott77.test') - expect(handles).not.toContain('cara-wiegand69.test') - }) -}) - -// Not using jest snapshots because it doesn't handle the conditional pg/sqlite very well: -// you can achieve it using named snapshots, but when you run the tests for pg the test suite fails -// since the sqlite snapshots appear obsolete to jest (and vice-versa when you run the sqlite suite). - -const avatar = - 'https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg' - -const snapTypeaheadPg = [ - { - did: 'user(0)', - displayName: 'Carlton Abernathy IV', - handle: 'aliya-hodkiewicz.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(1)', - handle: 'cara-wiegand69.test', - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(2)', - handle: 'carlos6.test', - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(3)', - displayName: 'Latoya Windler', - handle: 'carolina-mcdermott77.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(4)', - displayName: 'Rachel Kshlerin', - handle: 'cayla-marquardt39.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(5)', - displayName: 'Carol Littel', - handle: 'eudora-dietrich4.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(6)', - displayName: 'Sadie Carter', - handle: 'shane-torphy52.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, -] - -const snapTypeaheadSqlite = [ - { - did: 'user(0)', - displayName: 'Carlton Abernathy IV', - handle: 'aliya-hodkiewicz.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(1)', - handle: 'cara-wiegand69.test', - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(2)', - handle: 'carlos6.test', - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(3)', - displayName: 'Latoya Windler', - handle: 'carolina-mcdermott77.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(4)', - displayName: 'Carol Littel', - handle: 'eudora-dietrich4.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(5)', - displayName: 'Sadie Carter', - handle: 'shane-torphy52.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, -] - -const snapSearchPg = [ - { - did: 'user(0)', - displayName: 'Carlton Abernathy IV', - indexedAt: '1970-01-01T00:00:00.000Z', - handle: 'aliya-hodkiewicz.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(1)', - handle: 'cara-wiegand69.test', - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(2)', - handle: 'carlos6.test', - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(3)', - displayName: 'Latoya Windler', - indexedAt: '1970-01-01T00:00:00.000Z', - handle: 'carolina-mcdermott77.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(4)', - displayName: 'Rachel Kshlerin', - indexedAt: '1970-01-01T00:00:00.000Z', - handle: 'cayla-marquardt39.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(5)', - displayName: 'Carol Littel', - indexedAt: '1970-01-01T00:00:00.000Z', - handle: 'eudora-dietrich4.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(6)', - displayName: 'Sadie Carter', - indexedAt: '1970-01-01T00:00:00.000Z', - handle: 'shane-torphy52.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, -] - -const snapSearchSqlite = [ - { - did: 'user(0)', - displayName: 'Carlton Abernathy IV', - indexedAt: '1970-01-01T00:00:00.000Z', - handle: 'aliya-hodkiewicz.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(1)', - handle: 'cara-wiegand69.test', - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(2)', - handle: 'carlos6.test', - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(3)', - displayName: 'Latoya Windler', - indexedAt: '1970-01-01T00:00:00.000Z', - handle: 'carolina-mcdermott77.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(4)', - displayName: 'Carol Littel', - indexedAt: '1970-01-01T00:00:00.000Z', - handle: 'eudora-dietrich4.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(5)', - displayName: 'Sadie Carter', - indexedAt: '1970-01-01T00:00:00.000Z', - handle: 'shane-torphy52.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, -] diff --git a/packages/pds/tests/views/author-feed.test.ts b/packages/pds/tests/views/author-feed.test.ts deleted file mode 100644 index 2f9e8603b7b..00000000000 --- a/packages/pds/tests/views/author-feed.test.ts +++ /dev/null @@ -1,363 +0,0 @@ -import AtpAgent, { ComAtprotoAdminTakeModerationAction } from '@atproto/api' -import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' -import { - runTestServer, - forSnapshot, - CloseFn, - paginateAll, - adminAuth, -} from '../_util' -import { SeedClient } from '../seeds/client' -import basicSeed from '../seeds/basic' -import { isRecord } from '../../src/lexicon/types/app/bsky/feed/post' -import { isView as isEmbedRecordWithMedia } from '../../src/lexicon/types/app/bsky/embed/recordWithMedia' -import { isView as isImageEmbed } from '../../src/lexicon/types/app/bsky/embed/images' - -describe('pds author feed views', () => { - let agent: AtpAgent - let close: CloseFn - let sc: SeedClient - - // account dids, for convenience - let alice: string - let bob: string - let carol: string - let dan: string - - const reverseModerationAction = async (id) => - agent.api.com.atproto.admin.reverseModerationAction( - { - id, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - - const takedownSubject = async ( - subject: ComAtprotoAdminTakeModerationAction.InputSchema['subject'], - ) => - agent.api.com.atproto.admin.takeModerationAction( - { - action: TAKEDOWN, - subject, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - - beforeAll(async () => { - const server = await runTestServer({ - dbPostgresSchema: 'views_author_feed', - }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await basicSeed(sc) - alice = sc.dids.alice - bob = sc.dids.bob - carol = sc.dids.carol - dan = sc.dids.dan - await server.processAll() - }) - - afterAll(async () => { - await close() - }) - - it('fetches full author feeds for self (sorted, minimal viewer state).', async () => { - const aliceForAlice = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: sc.accounts[alice].handle }, - { - headers: sc.getHeaders(alice), - }, - ) - - expect(forSnapshot(aliceForAlice.data.feed)).toMatchSnapshot() - - const bobForBob = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: sc.accounts[bob].handle }, - { - headers: sc.getHeaders(bob), - }, - ) - - expect(forSnapshot(bobForBob.data.feed)).toMatchSnapshot() - - const carolForCarol = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: sc.accounts[carol].handle }, - { - headers: sc.getHeaders(carol), - }, - ) - - expect(forSnapshot(carolForCarol.data.feed)).toMatchSnapshot() - - const danForDan = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: sc.accounts[dan].handle }, - { - headers: sc.getHeaders(dan), - }, - ) - - expect(forSnapshot(danForDan.data.feed)).toMatchSnapshot() - }) - - it("reflects fetching user's state in the feed.", async () => { - const aliceForCarol = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: sc.accounts[alice].handle }, - { - headers: sc.getHeaders(carol), - }, - ) - - aliceForCarol.data.feed.forEach((postView) => { - const { viewer, uri } = postView.post - expect(viewer?.like).toEqual(sc.likes[carol]?.[uri]?.toString()) - expect(viewer?.repost).toEqual(sc.reposts[carol][uri]?.toString()) - }) - - expect(forSnapshot(aliceForCarol.data.feed)).toMatchSnapshot() - }) - - it('omits reposts from muted users.', async () => { - await agent.api.app.bsky.graph.muteActor( - { actor: alice }, // Has a repost by dan: will be omitted from dan's feed - { headers: sc.getHeaders(bob), encoding: 'application/json' }, - ) - await agent.api.app.bsky.graph.muteActor( - { actor: dan }, // Feed author: their posts will still appear - { headers: sc.getHeaders(bob), encoding: 'application/json' }, - ) - const bobForDan = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: sc.accounts[dan].handle }, - { headers: sc.getHeaders(bob) }, - ) - - expect(forSnapshot(bobForDan.data.feed)).toMatchSnapshot() - - await agent.api.app.bsky.graph.unmuteActor( - { actor: alice }, - { headers: sc.getHeaders(bob), encoding: 'application/json' }, - ) - await agent.api.app.bsky.graph.unmuteActor( - { actor: dan }, - { headers: sc.getHeaders(bob), encoding: 'application/json' }, - ) - }) - - it('paginates', async () => { - const results = (results) => results.flatMap((res) => res.feed) - const paginator = async (cursor?: string) => { - const res = await agent.api.app.bsky.feed.getAuthorFeed( - { - actor: sc.accounts[alice].handle, - cursor, - limit: 2, - }, - { headers: sc.getHeaders(dan) }, - ) - return res.data - } - - const paginatedAll = await paginateAll(paginator) - paginatedAll.forEach((res) => - expect(res.feed.length).toBeLessThanOrEqual(2), - ) - - const full = await agent.api.app.bsky.feed.getAuthorFeed( - { - actor: sc.accounts[alice].handle, - }, - { headers: sc.getHeaders(dan) }, - ) - - expect(full.data.feed.length).toEqual(4) - expect(results(paginatedAll)).toEqual(results([full.data])) - }) - - it('blocked by actor takedown.', async () => { - const { data: preBlock } = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: alice }, - { headers: sc.getHeaders(carol) }, - ) - - expect(preBlock.feed.length).toBeGreaterThan(0) - - const { data: action } = await takedownSubject({ - $type: 'com.atproto.admin.defs#repoRef', - did: alice, - }) - - const attempt = agent.api.app.bsky.feed.getAuthorFeed( - { actor: alice }, - { headers: sc.getHeaders(carol) }, - ) - await expect(attempt).rejects.toThrow('Profile not found') - - // Cleanup - await reverseModerationAction(action.id) - }) - - it('blocked by record takedown.', async () => { - const { data: preBlock } = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: alice }, - { headers: sc.getHeaders(carol) }, - ) - - expect(preBlock.feed.length).toBeGreaterThan(0) - - const post = preBlock.feed[0].post - - const { data: action } = await takedownSubject({ - $type: 'com.atproto.repo.strongRef', - uri: post.uri, - cid: post.cid, - }) - - const { data: postBlock } = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: alice }, - { headers: sc.getHeaders(carol) }, - ) - - expect(postBlock.feed.length).toEqual(preBlock.feed.length - 1) - expect(postBlock.feed.map((item) => item.post.uri)).not.toContain(post.uri) - - // Cleanup - await reverseModerationAction(action.id) - }) - - it('includes takendown posts for admins', async () => { - const { data: preTakedown } = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: alice }, - { headers: sc.getHeaders(carol) }, - ) - - expect(preTakedown.feed.length).toBeGreaterThan(0) - - const post = preTakedown.feed[0].post - - const { data: takedownAction } = await takedownSubject({ - $type: 'com.atproto.repo.strongRef', - uri: post.uri, - cid: post.cid, - }) - - const [{ data: postTakedownForCarol }, { data: postTakedownForAdmin }] = - await Promise.all([ - agent.api.app.bsky.feed.getAuthorFeed( - { actor: alice }, - { headers: sc.getHeaders(carol) }, - ), - agent.api.app.bsky.feed.getAuthorFeed( - { actor: alice }, - { headers: { authorization: adminAuth() } }, - ), - ]) - - const takendownPostInCarolsFeed = postTakedownForCarol.feed.find( - (item) => item.post.uri === post.uri || !!post.takedownId, - ) - const takendownPostInAdminsFeed = postTakedownForAdmin.feed.find( - (item) => item.post.uri === post.uri || !!post.takedownId, - ) - expect(takendownPostInCarolsFeed).toBeFalsy() - expect(takendownPostInAdminsFeed).toBeTruthy() - expect(takendownPostInAdminsFeed?.post.takedownId).toEqual( - takedownAction.id, - ) - - // Cleanup - await reverseModerationAction(takedownAction.id) - }) - - it('can filter by posts_with_media', async () => { - const { data: carolFeed } = await agent.api.app.bsky.feed.getAuthorFeed( - { - actor: carol, - filter: 'posts_with_media', - }, - { - headers: sc.getHeaders(alice), - }, - ) - - expect(carolFeed.feed.length).toBeGreaterThan(0) - expect( - carolFeed.feed.every(({ post }) => { - const isRecordWithActorMedia = - isEmbedRecordWithMedia(post.embed) && isImageEmbed(post.embed?.media) - const isActorMedia = isImageEmbed(post.embed) - const isFromActor = post.author.did === carol - - return (isRecordWithActorMedia || isActorMedia) && isFromActor - }), - ).toBeTruthy() - - const { data: bobFeed } = await agent.api.app.bsky.feed.getAuthorFeed( - { - actor: bob, - filter: 'posts_with_media', - }, - { - headers: sc.getHeaders(alice), - }, - ) - - expect(bobFeed.feed.length).toBeGreaterThan(0) - expect( - bobFeed.feed.every(({ post }) => { - return isImageEmbed(post.embed) && post.author.did === bob - }), - ).toBeTruthy() - - const { data: danFeed } = await agent.api.app.bsky.feed.getAuthorFeed( - { - actor: dan, - filter: 'posts_with_media', - }, - { - headers: sc.getHeaders(alice), - }, - ) - - expect(danFeed.feed.length).toEqual(0) - }) - - it('filters by posts_no_replies', async () => { - const { data: carolFeed } = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: carol, filter: 'posts_no_replies' }, - { headers: sc.getHeaders(alice) }, - ) - - expect( - carolFeed.feed.every(({ post }) => { - return ( - (isRecord(post.record) && !post.record.reply) || - (isRecord(post.record) && post.record.reply) - ) - }), - ).toBeTruthy() - - const { data: danFeed } = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: dan, filter: 'posts_no_replies' }, - { headers: sc.getHeaders(alice) }, - ) - - expect( - danFeed.feed.every(({ post }) => { - return ( - (isRecord(post.record) && !post.record.reply) || - (isRecord(post.record) && post.record.reply) - ) - }), - ).toBeTruthy() - }) -}) diff --git a/packages/pds/tests/views/blocks.test.ts b/packages/pds/tests/views/blocks.test.ts deleted file mode 100644 index 8d8fb3dba76..00000000000 --- a/packages/pds/tests/views/blocks.test.ts +++ /dev/null @@ -1,531 +0,0 @@ -import assert from 'assert' -import AtpAgent, { AtUri } from '@atproto/api' -import { RecordRef } from '@atproto/bsky/tests/seeds/client' -import { BlockedActorError } from '@atproto/api/src/client/types/app/bsky/feed/getAuthorFeed' -import { BlockedByActorError } from '@atproto/api/src/client/types/app/bsky/feed/getAuthorFeed' -import { isThreadViewPost } from '@atproto/api/src/client/types/app/bsky/feed/defs' -import { - isViewRecord as isEmbedViewRecord, - isViewBlocked as isEmbedViewBlocked, -} from '@atproto/api/src/client/types/app/bsky/embed/record' -import { runTestServer, CloseFn, TestServerInfo, forSnapshot } from '../_util' -import { SeedClient } from '../seeds/client' -import basicSeed from '../seeds/basic' - -describe('pds views with blocking', () => { - let server: TestServerInfo - let agent: AtpAgent - let close: CloseFn - let sc: SeedClient - let danBlockCarol: { uri: string } - let aliceReplyToDan: { ref: RecordRef } - let carolReplyToDan: { ref: RecordRef } - - let alice: string - let carol: string - let dan: string - let danBlockUri: string - - beforeAll(async () => { - server = await runTestServer({ - dbPostgresSchema: 'views_block', - }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await basicSeed(sc) - alice = sc.dids.alice - carol = sc.dids.carol - dan = sc.dids.dan - // add follows to ensure blocks work even w follows - await sc.follow(carol, dan) - await sc.follow(dan, carol) - aliceReplyToDan = await sc.reply( - alice, - sc.posts[dan][0].ref, - sc.posts[dan][0].ref, - 'alice replies to dan', - ) - carolReplyToDan = await sc.reply( - carol, - sc.posts[dan][0].ref, - sc.posts[dan][0].ref, - 'carol replies to dan', - ) - // dan blocks carol - danBlockCarol = await agent.api.app.bsky.graph.block.create( - { repo: dan }, - { createdAt: new Date().toISOString(), subject: carol }, - sc.getHeaders(dan), - ) - danBlockUri = danBlockCarol.uri - await server.processAll() - }) - - afterAll(async () => { - await close() - }) - - it('blocks thread post', async () => { - const { data: threadAlice } = await agent.api.app.bsky.feed.getPostThread( - { depth: 1, uri: sc.posts[carol][0].ref.uriStr }, - { headers: sc.getHeaders(dan) }, - ) - expect(threadAlice).toEqual({ - thread: { - $type: 'app.bsky.feed.defs#blockedPost', - uri: sc.posts[carol][0].ref.uriStr, - blocked: true, - author: { - did: carol, - viewer: { - blockedBy: false, - blocking: danBlockUri, - }, - }, - }, - }) - const { data: threadCarol } = await agent.api.app.bsky.feed.getPostThread( - { depth: 1, uri: sc.posts[dan][0].ref.uriStr }, - { headers: sc.getHeaders(carol) }, - ) - expect(threadCarol).toEqual({ - thread: { - $type: 'app.bsky.feed.defs#blockedPost', - uri: sc.posts[dan][0].ref.uriStr, - blocked: true, - author: { - did: dan, - viewer: { - blockedBy: true, - }, - }, - }, - }) - }) - - it('blocks thread reply', async () => { - // Contains reply by carol - const { data: thread } = await agent.api.app.bsky.feed.getPostThread( - { depth: 1, uri: sc.posts[alice][1].ref.uriStr }, - { headers: sc.getHeaders(dan) }, - ) - expect(forSnapshot(thread)).toMatchSnapshot() - }) - - it('blocks thread parent', async () => { - // Parent is a post by dan - const { data: thread } = await agent.api.app.bsky.feed.getPostThread( - { depth: 1, uri: aliceReplyToDan.ref.uriStr }, - { headers: sc.getHeaders(carol) }, - ) - expect(forSnapshot(thread)).toMatchSnapshot() - }) - - it('blocks record embeds', async () => { - // Contains a deep embed of carol's post, blocked by dan - const { data: thread } = await agent.api.app.bsky.feed.getPostThread( - { depth: 0, uri: sc.posts[alice][2].ref.uriStr }, - { headers: sc.getHeaders(dan) }, - ) - expect(forSnapshot(thread)).toMatchSnapshot() - }) - - it('errors on getting author feed', async () => { - const attempt1 = agent.api.app.bsky.feed.getAuthorFeed( - { actor: carol }, - { headers: sc.getHeaders(dan) }, - ) - await expect(attempt1).rejects.toThrow(BlockedActorError) - - const attempt2 = agent.api.app.bsky.feed.getAuthorFeed( - { actor: dan }, - { headers: sc.getHeaders(carol) }, - ) - await expect(attempt2).rejects.toThrow(BlockedByActorError) - }) - - it('strips blocked users out of getTimeline', async () => { - const resCarol = await agent.api.app.bsky.feed.getTimeline( - { limit: 100 }, - { headers: sc.getHeaders(carol) }, - ) - expect( - resCarol.data.feed.some((post) => post.post.author.did === dan), - ).toBeFalsy() - - const resDan = await agent.api.app.bsky.feed.getTimeline( - { limit: 100 }, - { headers: sc.getHeaders(dan) }, - ) - expect( - resDan.data.feed.some((post) => post.post.author.did === carol), - ).toBeFalsy() - }) - - it('strips blocked users out of getPopular', async () => { - for (let i = 0; i < 15; i++) { - const name = `user${i}` - await sc.createAccount(name, { - handle: `user${i}.test`, - email: `user${i}@test.com`, - password: 'password', - }) - await sc.like(sc.dids[name], sc.posts[alice][0].ref) - await sc.like(sc.dids[name], sc.posts[carol][0].ref) - await sc.like(sc.dids[name], sc.posts[dan][0].ref) - } - - const resCarol = await agent.api.app.bsky.unspecced.getPopular( - {}, - { headers: sc.getHeaders(carol) }, - ) - expect( - resCarol.data.feed.some((post) => post.post.author.did === alice), - ).toBeTruthy() - expect( - resCarol.data.feed.some((post) => post.post.author.did === carol), - ).toBeTruthy() - expect( - resCarol.data.feed.some((post) => post.post.author.did === dan), - ).toBeFalsy() - - const resDan = await agent.api.app.bsky.unspecced.getPopular( - {}, - { headers: sc.getHeaders(dan) }, - ) - expect( - resDan.data.feed.some((post) => post.post.author.did === alice), - ).toBeTruthy() - expect( - resDan.data.feed.some((post) => post.post.author.did === carol), - ).toBeFalsy() - expect( - resDan.data.feed.some((post) => post.post.author.did === dan), - ).toBeTruthy() - }) - - it('returns block status on getProfile', async () => { - const resCarol = await agent.api.app.bsky.actor.getProfile( - { actor: dan }, - { headers: sc.getHeaders(carol) }, - ) - expect(resCarol.data.viewer?.blocking).toBeUndefined - expect(resCarol.data.viewer?.blockedBy).toBe(true) - - const resDan = await agent.api.app.bsky.actor.getProfile( - { actor: carol }, - { headers: sc.getHeaders(dan) }, - ) - expect(resDan.data.viewer?.blocking).toBeDefined - expect(resDan.data.viewer?.blockedBy).toBe(false) - }) - - it('returns block status on getProfiles', async () => { - const resCarol = await agent.api.app.bsky.actor.getProfiles( - { actors: [alice, dan] }, - { headers: sc.getHeaders(carol) }, - ) - expect(resCarol.data.profiles[0].viewer?.blocking).toBeUndefined() - expect(resCarol.data.profiles[0].viewer?.blockedBy).toBe(false) - expect(resCarol.data.profiles[1].viewer?.blocking).toBeUndefined() - expect(resCarol.data.profiles[1].viewer?.blockedBy).toBe(true) - - const resDan = await agent.api.app.bsky.actor.getProfiles( - { actors: [alice, carol] }, - { headers: sc.getHeaders(dan) }, - ) - expect(resDan.data.profiles[0].viewer?.blocking).toBeUndefined() - expect(resDan.data.profiles[0].viewer?.blockedBy).toBe(false) - expect(resDan.data.profiles[1].viewer?.blocking).toBeDefined() - expect(resDan.data.profiles[1].viewer?.blockedBy).toBe(false) - }) - - it('does not return block violating follows', async () => { - const resCarol = await agent.api.app.bsky.graph.getFollows( - { actor: carol }, - { headers: sc.getHeaders(alice) }, - ) - expect(resCarol.data.follows.some((f) => f.did === dan)).toBe(false) - - const resDan = await agent.api.app.bsky.graph.getFollows( - { actor: dan }, - { headers: sc.getHeaders(alice) }, - ) - expect(resDan.data.follows.some((f) => f.did === carol)).toBe(false) - }) - - it('does not return block violating followers', async () => { - const resCarol = await agent.api.app.bsky.graph.getFollowers( - { actor: carol }, - { headers: sc.getHeaders(alice) }, - ) - expect(resCarol.data.followers.some((f) => f.did === dan)).toBe(false) - - const resDan = await agent.api.app.bsky.graph.getFollowers( - { actor: dan }, - { headers: sc.getHeaders(alice) }, - ) - expect(resDan.data.followers.some((f) => f.did === carol)).toBe(false) - }) - - it('does not return posts from blocked users', async () => { - const alicePost = sc.posts[alice][0].ref.uriStr - const carolPost = sc.posts[carol][0].ref.uriStr - const danPost = sc.posts[dan][0].ref.uriStr - - const resCarol = await agent.api.app.bsky.feed.getPosts( - { uris: [alicePost, carolPost, danPost] }, - { headers: sc.getHeaders(carol) }, - ) - expect(resCarol.data.posts.some((p) => p.uri === alicePost)).toBe(true) - expect(resCarol.data.posts.some((p) => p.uri === carolPost)).toBe(true) - expect(resCarol.data.posts.some((p) => p.uri === danPost)).toBe(false) - - const resDan = await agent.api.app.bsky.feed.getPosts( - { uris: [alicePost, carolPost, danPost] }, - { headers: sc.getHeaders(dan) }, - ) - expect(resDan.data.posts.some((p) => p.uri === alicePost)).toBe(true) - expect(resDan.data.posts.some((p) => p.uri === carolPost)).toBe(false) - expect(resDan.data.posts.some((p) => p.uri === danPost)).toBe(true) - }) - - it('does not return notifs for blocked accounts', async () => { - const resCarol = await agent.api.app.bsky.notification.listNotifications( - { - limit: 100, - }, - { headers: sc.getHeaders(carol) }, - ) - expect( - resCarol.data.notifications.some((notif) => notif.author.did === dan), - ).toBeFalsy() - - const resDan = await agent.api.app.bsky.notification.listNotifications( - { - limit: 100, - }, - { headers: sc.getHeaders(dan) }, - ) - expect( - resDan.data.notifications.some((notif) => notif.author.did === carol), - ).toBeFalsy() - }) - - it('does not return blocked accounts in actor search', async () => { - const resCarol = await agent.api.app.bsky.actor.searchActors( - { - term: 'dan.test', - }, - { headers: sc.getHeaders(carol) }, - ) - expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeFalsy() - - const resDan = await agent.api.app.bsky.actor.searchActors( - { - term: 'carol.test', - }, - { headers: sc.getHeaders(dan) }, - ) - expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy() - }) - - it('does not return blocked accounts in actor search typeahead', async () => { - const resCarol = await agent.api.app.bsky.actor.searchActorsTypeahead( - { - term: 'dan.test', - }, - { headers: sc.getHeaders(carol) }, - ) - expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeFalsy() - - const resDan = await agent.api.app.bsky.actor.searchActorsTypeahead( - { - term: 'carol.test', - }, - { headers: sc.getHeaders(dan) }, - ) - expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy() - }) - - it('does not return blocked accounts in get suggestions', async () => { - // unfollow so they _would_ show up in suggestions if not for block - await sc.unfollow(carol, dan) - await sc.unfollow(dan, carol) - - const resCarol = await agent.api.app.bsky.actor.getSuggestions( - { - limit: 100, - }, - { headers: sc.getHeaders(carol) }, - ) - expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeFalsy() - - const resDan = await agent.api.app.bsky.actor.getSuggestions( - { - limit: 100, - }, - { headers: sc.getHeaders(dan) }, - ) - expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy() - }) - - it('does not serve blocked replies', async () => { - const getThreadPostUri = (r) => r?.['post']?.['uri'] - // reply then block - const { data: replyThenBlock } = - await agent.api.app.bsky.feed.getPostThread( - { depth: 1, uri: sc.posts[dan][0].ref.uriStr }, - { headers: sc.getHeaders(alice) }, - ) - assert(isThreadViewPost(replyThenBlock.thread)) - expect(replyThenBlock.thread.replies?.map(getThreadPostUri)).toEqual([ - aliceReplyToDan.ref.uriStr, - ]) - - // unblock - await agent.api.app.bsky.graph.block.delete( - { repo: dan, rkey: new AtUri(danBlockCarol.uri).rkey }, - sc.getHeaders(dan), - ) - const { data: unblock } = await agent.api.app.bsky.feed.getPostThread( - { depth: 1, uri: sc.posts[dan][0].ref.uriStr }, - { headers: sc.getHeaders(alice) }, - ) - assert(isThreadViewPost(unblock.thread)) - expect(unblock.thread.replies?.map(getThreadPostUri)).toEqual([ - carolReplyToDan.ref.uriStr, - aliceReplyToDan.ref.uriStr, - ]) - - // block then reply - danBlockCarol = await agent.api.app.bsky.graph.block.create( - { repo: dan }, - { createdAt: new Date().toISOString(), subject: carol }, - sc.getHeaders(dan), - ) - const carolReplyToDan2 = await sc.reply( - carol, - sc.posts[dan][1].ref, - sc.posts[dan][1].ref, - 'carol replies to dan again', - ) - const { data: blockThenReply } = - await agent.api.app.bsky.feed.getPostThread( - { depth: 1, uri: sc.posts[dan][0].ref.uriStr }, - { headers: sc.getHeaders(alice) }, - ) - assert(isThreadViewPost(blockThenReply.thread)) - expect(replyThenBlock.thread.replies?.map(getThreadPostUri)).toEqual([ - aliceReplyToDan.ref.uriStr, - ]) - - // cleanup - await agent.api.app.bsky.feed.post.delete( - { repo: carol, rkey: carolReplyToDan2.ref.uri.rkey }, - sc.getHeaders(carol), - ) - }) - - it('does not serve blocked embeds to third-party', async () => { - // embed then block - const { data: embedThenBlock } = - await agent.api.app.bsky.feed.getPostThread( - { depth: 0, uri: sc.posts[dan][1].ref.uriStr }, - { headers: sc.getHeaders(alice) }, - ) - assert(isThreadViewPost(embedThenBlock.thread)) - assert(isEmbedViewBlocked(embedThenBlock.thread.post.embed?.record)) - - // unblock - await agent.api.app.bsky.graph.block.delete( - { repo: dan, rkey: new AtUri(danBlockCarol.uri).rkey }, - sc.getHeaders(dan), - ) - const { data: unblock } = await agent.api.app.bsky.feed.getPostThread( - { depth: 0, uri: sc.posts[dan][1].ref.uriStr }, - { headers: sc.getHeaders(alice) }, - ) - assert(isThreadViewPost(unblock.thread)) - assert(isEmbedViewRecord(unblock.thread.post.embed?.record)) - - // block then embed - danBlockCarol = await agent.api.app.bsky.graph.block.create( - { repo: dan }, - { createdAt: new Date().toISOString(), subject: carol }, - sc.getHeaders(dan), - ) - const carolEmbedsDan = await sc.post( - carol, - 'carol embeds dan', - undefined, - undefined, - sc.posts[dan][0].ref, - ) - const { data: blockThenEmbed } = - await agent.api.app.bsky.feed.getPostThread( - { depth: 0, uri: carolEmbedsDan.ref.uriStr }, - { headers: sc.getHeaders(alice) }, - ) - assert(isThreadViewPost(blockThenEmbed.thread)) - assert(isEmbedViewBlocked(blockThenEmbed.thread.post.embed?.record)) - - // cleanup - await agent.api.app.bsky.feed.post.delete( - { repo: carol, rkey: carolEmbedsDan.ref.uri.rkey }, - sc.getHeaders(carol), - ) - }) - - it('applies third-party blocking rules in feeds.', async () => { - // alice follows carol and dan, block exists between carol and dan. - const replyBlockedUri = carolReplyToDan.ref.uriStr - const embedBlockedUri = sc.posts[dan][1].ref.uriStr - const { data: timeline } = await agent.api.app.bsky.feed.getTimeline( - { limit: 100 }, - { headers: sc.getHeaders(alice) }, - ) - const replyBlockedPost = timeline.feed.find( - (item) => item.post.uri === replyBlockedUri, - ) - expect(replyBlockedPost).toBeUndefined() - const embedBlockedPost = timeline.feed.find( - (item) => item.post.uri === embedBlockedUri, - ) - assert(embedBlockedPost) - assert(isEmbedViewBlocked(embedBlockedPost.post.embed?.record)) - }) - - it('returns a list of blocks', async () => { - await agent.api.app.bsky.graph.block.create( - { repo: dan }, - { createdAt: new Date().toISOString(), subject: alice }, - sc.getHeaders(dan), - ) - - const res = await agent.api.app.bsky.graph.getBlocks( - {}, - { headers: sc.getHeaders(dan) }, - ) - const dids = res.data.blocks.map((block) => block.did).sort() - expect(dids).toEqual([alice, carol].sort()) - }) - - it('paginates getBlocks', async () => { - const full = await agent.api.app.bsky.graph.getBlocks( - {}, - { headers: sc.getHeaders(dan) }, - ) - const first = await agent.api.app.bsky.graph.getBlocks( - { limit: 1 }, - { headers: sc.getHeaders(dan) }, - ) - const second = await agent.api.app.bsky.graph.getBlocks( - { cursor: first.data.cursor }, - { headers: sc.getHeaders(dan) }, - ) - const combined = [...first.data.blocks, ...second.data.blocks] - expect(combined).toEqual(full.data.blocks) - }) -}) diff --git a/packages/pds/tests/views/follows.test.ts b/packages/pds/tests/views/follows.test.ts deleted file mode 100644 index 606b85f9d48..00000000000 --- a/packages/pds/tests/views/follows.test.ts +++ /dev/null @@ -1,266 +0,0 @@ -import AtpAgent from '@atproto/api' -import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' -import { - forSnapshot, - paginateAll, - adminAuth, - runTestServer, - CloseFn, -} from '../_util' -import { SeedClient } from '../seeds/client' -import followsSeed from '../seeds/follows' - -describe('pds follow views', () => { - let agent: AtpAgent - let close: CloseFn - let sc: SeedClient - - // account dids, for convenience - let alice: string - - beforeAll(async () => { - const server = await runTestServer({ - dbPostgresSchema: 'views_follows', - }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await followsSeed(sc) - alice = sc.dids.alice - await server.processAll() - }) - - afterAll(async () => { - await close() - }) - - it('fetches followers', async () => { - const aliceFollowers = await agent.api.app.bsky.graph.getFollowers( - { actor: sc.dids.alice }, - { headers: sc.getHeaders(alice) }, - ) - - expect(forSnapshot(aliceFollowers.data)).toMatchSnapshot() - - const bobFollowers = await agent.api.app.bsky.graph.getFollowers( - { actor: sc.dids.bob }, - { headers: sc.getHeaders(alice) }, - ) - - expect(forSnapshot(bobFollowers.data)).toMatchSnapshot() - - const carolFollowers = await agent.api.app.bsky.graph.getFollowers( - { actor: sc.dids.carol }, - { headers: sc.getHeaders(alice) }, - ) - - expect(forSnapshot(carolFollowers.data)).toMatchSnapshot() - - const danFollowers = await agent.api.app.bsky.graph.getFollowers( - { actor: sc.dids.dan }, - { headers: sc.getHeaders(alice) }, - ) - - expect(forSnapshot(danFollowers.data)).toMatchSnapshot() - - const eveFollowers = await agent.api.app.bsky.graph.getFollowers( - { actor: sc.dids.eve }, - { headers: sc.getHeaders(alice) }, - ) - - expect(forSnapshot(eveFollowers.data)).toMatchSnapshot() - }) - - it('fetches followers by handle', async () => { - const byDid = await agent.api.app.bsky.graph.getFollowers( - { actor: sc.dids.alice }, - { headers: sc.getHeaders(alice) }, - ) - const byHandle = await agent.api.app.bsky.graph.getFollowers( - { actor: sc.accounts[alice].handle }, - { headers: sc.getHeaders(alice) }, - ) - expect(byHandle.data).toEqual(byDid.data) - }) - - it('paginates followers', async () => { - const results = (results) => results.flatMap((res) => res.followers) - const paginator = async (cursor?: string) => { - const res = await agent.api.app.bsky.graph.getFollowers( - { - actor: sc.dids.alice, - cursor, - limit: 2, - }, - { headers: sc.getHeaders(alice) }, - ) - return res.data - } - - const paginatedAll = await paginateAll(paginator) - paginatedAll.forEach((res) => - expect(res.followers.length).toBeLessThanOrEqual(2), - ) - - const full = await agent.api.app.bsky.graph.getFollowers( - { actor: sc.dids.alice }, - { headers: sc.getHeaders(alice) }, - ) - - expect(full.data.followers.length).toEqual(4) - expect(results(paginatedAll)).toEqual(results([full.data])) - }) - - it('blocks followers by actor takedown', async () => { - const { data: modAction } = - await agent.api.com.atproto.admin.takeModerationAction( - { - action: TAKEDOWN, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.dan, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - - const aliceFollowers = await agent.api.app.bsky.graph.getFollowers( - { actor: sc.dids.alice }, - { headers: sc.getHeaders(alice) }, - ) - - expect(forSnapshot(aliceFollowers.data)).toMatchSnapshot() - - await agent.api.com.atproto.admin.reverseModerationAction( - { - id: modAction.id, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - }) - - it('fetches follows', async () => { - const aliceFollowers = await agent.api.app.bsky.graph.getFollows( - { actor: sc.dids.alice }, - { headers: sc.getHeaders(alice) }, - ) - - expect(forSnapshot(aliceFollowers.data)).toMatchSnapshot() - - const bobFollowers = await agent.api.app.bsky.graph.getFollows( - { actor: sc.dids.bob }, - { headers: sc.getHeaders(alice) }, - ) - - expect(forSnapshot(bobFollowers.data)).toMatchSnapshot() - - const carolFollowers = await agent.api.app.bsky.graph.getFollows( - { actor: sc.dids.carol }, - { headers: sc.getHeaders(alice) }, - ) - - expect(forSnapshot(carolFollowers.data)).toMatchSnapshot() - - const danFollowers = await agent.api.app.bsky.graph.getFollows( - { actor: sc.dids.dan }, - { headers: sc.getHeaders(alice) }, - ) - - expect(forSnapshot(danFollowers.data)).toMatchSnapshot() - - const eveFollowers = await agent.api.app.bsky.graph.getFollows( - { actor: sc.dids.eve }, - { headers: sc.getHeaders(alice) }, - ) - - expect(forSnapshot(eveFollowers.data)).toMatchSnapshot() - }) - - it('fetches follows by handle', async () => { - const byDid = await agent.api.app.bsky.graph.getFollows( - { actor: sc.dids.alice }, - { headers: sc.getHeaders(alice) }, - ) - const byHandle = await agent.api.app.bsky.graph.getFollows( - { actor: sc.accounts[alice].handle }, - { headers: sc.getHeaders(alice) }, - ) - expect(byHandle.data).toEqual(byDid.data) - }) - - it('paginates follows', async () => { - const results = (results) => results.flatMap((res) => res.follows) - const paginator = async (cursor?: string) => { - const res = await agent.api.app.bsky.graph.getFollows( - { - actor: sc.dids.alice, - cursor, - limit: 2, - }, - { headers: sc.getHeaders(alice) }, - ) - return res.data - } - - const paginatedAll = await paginateAll(paginator) - paginatedAll.forEach((res) => - expect(res.follows.length).toBeLessThanOrEqual(2), - ) - - const full = await agent.api.app.bsky.graph.getFollows( - { actor: sc.dids.alice }, - { headers: sc.getHeaders(alice) }, - ) - - expect(full.data.follows.length).toEqual(4) - expect(results(paginatedAll)).toEqual(results([full.data])) - }) - - it('blocks follows by actor takedown', async () => { - const { data: modAction } = - await agent.api.com.atproto.admin.takeModerationAction( - { - action: TAKEDOWN, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.dan, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - - const aliceFollows = await agent.api.app.bsky.graph.getFollows( - { actor: sc.dids.alice }, - { headers: sc.getHeaders(alice) }, - ) - - expect(forSnapshot(aliceFollows.data)).toMatchSnapshot() - - await agent.api.com.atproto.admin.reverseModerationAction( - { - id: modAction.id, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - }) -}) diff --git a/packages/pds/tests/views/likes.test.ts b/packages/pds/tests/views/likes.test.ts deleted file mode 100644 index d77251d8b25..00000000000 --- a/packages/pds/tests/views/likes.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import AtpAgent from '@atproto/api' -import { SeedClient } from '../seeds/client' -import likesSeed from '../seeds/likes' -import { - CloseFn, - constantDate, - forSnapshot, - paginateAll, - runTestServer, -} from '../_util' - -describe('pds like views', () => { - let agent: AtpAgent - let close: CloseFn - let sc: SeedClient - - // account dids, for convenience - let alice: string - let bob: string - - beforeAll(async () => { - const server = await runTestServer({ - dbPostgresSchema: 'views_likes', - }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await likesSeed(sc) - alice = sc.dids.alice - bob = sc.dids.bob - await server.processAll() - }) - - afterAll(async () => { - await close() - }) - - const getCursors = (items: { createdAt?: string }[]) => - items.map((item) => item.createdAt ?? constantDate) - - const getSortedCursors = (items: { createdAt?: string }[]) => - getCursors(items).sort((a, b) => tstamp(b) - tstamp(a)) - - const tstamp = (x: string) => new Date(x).getTime() - - it('fetches post likes', async () => { - const alicePost = await agent.api.app.bsky.feed.getLikes( - { uri: sc.posts[alice][1].ref.uriStr }, - { headers: sc.getHeaders(alice) }, - ) - - expect(forSnapshot(alicePost.data)).toMatchSnapshot() - expect(getCursors(alicePost.data.likes)).toEqual( - getSortedCursors(alicePost.data.likes), - ) - }) - - it('fetches reply likes', async () => { - const bobReply = await agent.api.app.bsky.feed.getLikes( - { uri: sc.replies[bob][0].ref.uriStr }, - { headers: sc.getHeaders(alice) }, - ) - - expect(forSnapshot(bobReply.data)).toMatchSnapshot() - expect(getCursors(bobReply.data.likes)).toEqual( - getSortedCursors(bobReply.data.likes), - ) - }) - - it('paginates', async () => { - const results = (results) => results.flatMap((res) => res.likes) - const paginator = async (cursor?: string) => { - const res = await agent.api.app.bsky.feed.getLikes( - { - uri: sc.posts[alice][1].ref.uriStr, - cursor, - limit: 2, - }, - { headers: sc.getHeaders(alice) }, - ) - return res.data - } - - const paginatedAll = await paginateAll(paginator) - paginatedAll.forEach((res) => - expect(res.likes.length).toBeLessThanOrEqual(2), - ) - - const full = await agent.api.app.bsky.feed.getLikes( - { uri: sc.posts[alice][1].ref.uriStr }, - { headers: sc.getHeaders(alice) }, - ) - - expect(full.data.likes.length).toEqual(4) - expect(results(paginatedAll)).toEqual(results([full.data])) - }) -}) diff --git a/packages/pds/tests/views/mute-lists.test.ts b/packages/pds/tests/views/mute-lists.test.ts deleted file mode 100644 index 64692a81b24..00000000000 --- a/packages/pds/tests/views/mute-lists.test.ts +++ /dev/null @@ -1,355 +0,0 @@ -import AtpAgent, { AtUri } from '@atproto/api' -import { runTestServer, CloseFn, TestServerInfo, forSnapshot } from '../_util' -import { SeedClient } from '../seeds/client' -import basicSeed from '../seeds/basic' -import { RecordRef } from '../seeds/client' - -describe('pds views with mutes from mute lists', () => { - let server: TestServerInfo - let agent: AtpAgent - let close: CloseFn - let sc: SeedClient - - let alice: string - let bob: string - let carol: string - let dan: string - - beforeAll(async () => { - server = await runTestServer({ - dbPostgresSchema: 'views_mute_lists', - }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await basicSeed(sc) - alice = sc.dids.alice - bob = sc.dids.bob - carol = sc.dids.carol - dan = sc.dids.dan - // add follows to ensure mutes work even w follows - await sc.follow(carol, dan) - await sc.follow(dan, carol) - await server.processAll() - }) - - afterAll(async () => { - await close() - }) - - let listUri: string - let listCid: string - - it('creates a list with some items', async () => { - const avatar = await sc.uploadFile( - alice, - 'tests/image/fixtures/key-portrait-small.jpg', - 'image/jpeg', - ) - // alice creates mute list with bob & carol that dan uses - const list = await agent.api.app.bsky.graph.list.create( - { repo: alice }, - { - name: 'alice mutes', - purpose: 'app.bsky.graph.defs#modlist', - description: 'big list of mutes', - avatar: avatar.image, - createdAt: new Date().toISOString(), - }, - sc.getHeaders(alice), - ) - listUri = list.uri - listCid = list.cid - await agent.api.app.bsky.graph.listitem.create( - { repo: alice }, - { - subject: sc.dids.bob, - list: list.uri, - reason: 'because', - createdAt: new Date().toISOString(), - }, - sc.getHeaders(alice), - ) - await agent.api.app.bsky.graph.listitem.create( - { repo: alice }, - { - subject: sc.dids.carol, - list: list.uri, - reason: 'idk', - createdAt: new Date().toISOString(), - }, - sc.getHeaders(alice), - ) - }) - - it('uses a list for mutes', async () => { - await agent.api.app.bsky.graph.muteActorList( - { - list: listUri, - }, - { encoding: 'application/json', headers: sc.getHeaders(dan) }, - ) - }) - - it('flags mutes in threads', async () => { - const res = await agent.api.app.bsky.feed.getPostThread( - { depth: 1, uri: sc.posts[alice][1].ref.uriStr }, - { headers: sc.getHeaders(dan) }, - ) - expect(forSnapshot(res.data.thread)).toMatchSnapshot() - }) - - it('does not show reposted content from a muted account in author feed', async () => { - await sc.repost(alice, sc.posts[carol][0].ref) - - const res = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: alice }, - { headers: sc.getHeaders(dan) }, - ) - expect( - res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)), - ).toBe(false) - }) - - it('removes content from muted users on getTimeline', async () => { - const res = await agent.api.app.bsky.feed.getTimeline( - { limit: 100 }, - { headers: sc.getHeaders(dan) }, - ) - expect( - res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)), - ).toBe(false) - }) - - it('flags muted users on getPopular', async () => { - for (let i = 0; i < 15; i++) { - const name = `user${i}` - await sc.createAccount(name, { - handle: `user${i}.test`, - email: `user${i}@test.com`, - password: 'password', - }) - await sc.like(sc.dids[name], sc.posts[alice][0].ref) - await sc.like(sc.dids[name], sc.posts[bob][0].ref) - await sc.like(sc.dids[name], sc.posts[carol][0].ref) - await sc.like(sc.dids[name], sc.posts[dan][0].ref) - } - - const res = await agent.api.app.bsky.unspecced.getPopular( - {}, - { headers: sc.getHeaders(dan) }, - ) - expect( - res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)), - ).toBe(false) - }) - - it('returns mute status on getProfile', async () => { - const res = await agent.api.app.bsky.actor.getProfile( - { actor: carol }, - { headers: sc.getHeaders(dan) }, - ) - expect(res.data.viewer?.muted).toBe(true) - expect(res.data.viewer?.mutedByList?.uri).toBe(listUri) - }) - - it('returns mute status on getProfiles', async () => { - const res = await agent.api.app.bsky.actor.getProfiles( - { actors: [alice, carol] }, - { headers: sc.getHeaders(dan) }, - ) - expect(res.data.profiles[0].viewer?.muted).toBe(false) - expect(res.data.profiles[0].viewer?.mutedByList).toBeUndefined() - expect(res.data.profiles[1].viewer?.muted).toBe(true) - expect(res.data.profiles[1].viewer?.mutedByList?.uri).toEqual(listUri) - }) - - it('does not return notifs for muted accounts', async () => { - const res = await agent.api.app.bsky.notification.listNotifications( - { - limit: 100, - }, - { headers: sc.getHeaders(dan) }, - ) - expect( - res.data.notifications.some((notif) => - [bob, carol].includes(notif.author.did), - ), - ).toBeFalsy() - }) - - it('flags muted accounts in in get suggestions', async () => { - // unfollow so they _would_ show up in suggestions if not for mute - await sc.unfollow(dan, carol) - - const res = await agent.api.app.bsky.actor.getSuggestions( - { - limit: 100, - }, - { headers: sc.getHeaders(dan) }, - ) - for (const actor of res.data.actors) { - if ([bob, carol].includes(actor.did)) { - expect(actor.viewer?.muted).toBe(true) - expect(actor.viewer?.mutedByList?.uri).toEqual(listUri) - } else { - expect(actor.viewer?.muted).toBe(false) - expect(actor.viewer?.mutedByList).toBeUndefined() - } - } - }) - - it('returns the contents of a list', async () => { - const res = await agent.api.app.bsky.graph.getList( - { list: listUri }, - { headers: sc.getHeaders(dan) }, - ) - expect(forSnapshot(res.data)).toMatchSnapshot() - }) - - it('paginates getList', async () => { - const full = await agent.api.app.bsky.graph.getList( - { list: listUri }, - { headers: sc.getHeaders(dan) }, - ) - const first = await agent.api.app.bsky.graph.getList( - { list: listUri, limit: 1 }, - { headers: sc.getHeaders(dan) }, - ) - const second = await agent.api.app.bsky.graph.getList( - { list: listUri, cursor: first.data.cursor }, - { headers: sc.getHeaders(dan) }, - ) - const combined = [...first.data.items, ...second.data.items] - expect(combined).toEqual(full.data.items) - }) - - let otherListUri: string - - it('returns lists associated with a user', async () => { - const listRes = await agent.api.app.bsky.graph.list.create( - { repo: alice }, - { - name: 'new list', - purpose: 'app.bsky.graph.defs#modlist', - description: 'blah blah', - createdAt: new Date().toISOString(), - }, - sc.getHeaders(alice), - ) - otherListUri = listRes.uri - - const res = await agent.api.app.bsky.graph.getLists( - { actor: alice }, - { headers: sc.getHeaders(dan) }, - ) - expect(forSnapshot(res.data)).toMatchSnapshot() - }) - - it('paginates getLists', async () => { - const full = await agent.api.app.bsky.graph.getLists( - { actor: alice }, - { headers: sc.getHeaders(dan) }, - ) - const first = await agent.api.app.bsky.graph.getLists( - { actor: alice, limit: 1 }, - { headers: sc.getHeaders(dan) }, - ) - const second = await agent.api.app.bsky.graph.getLists( - { actor: alice, cursor: first.data.cursor }, - { headers: sc.getHeaders(dan) }, - ) - const combined = [...first.data.lists, ...second.data.lists] - expect(combined).toEqual(full.data.lists) - }) - - it('returns a users own list mutes', async () => { - await agent.api.app.bsky.graph.muteActorList( - { - list: otherListUri, - }, - { encoding: 'application/json', headers: sc.getHeaders(dan) }, - ) - - const res = await agent.api.app.bsky.graph.getListMutes( - {}, - { headers: sc.getHeaders(dan) }, - ) - expect(forSnapshot(res.data)).toMatchSnapshot() - }) - - it('paginates getListMutes', async () => { - const full = await agent.api.app.bsky.graph.getListMutes( - {}, - { headers: sc.getHeaders(dan) }, - ) - const first = await agent.api.app.bsky.graph.getListMutes( - { limit: 1 }, - { headers: sc.getHeaders(dan) }, - ) - const second = await agent.api.app.bsky.graph.getListMutes( - { cursor: first.data.cursor }, - { headers: sc.getHeaders(dan) }, - ) - const combined = [...first.data.lists, ...second.data.lists] - expect(combined).toEqual(full.data.lists) - }) - - it('allows unsubscribing from a mute list', async () => { - await agent.api.app.bsky.graph.unmuteActorList( - { - list: otherListUri, - }, - { encoding: 'application/json', headers: sc.getHeaders(dan) }, - ) - - const res = await agent.api.app.bsky.graph.getListMutes( - {}, - { headers: sc.getHeaders(dan) }, - ) - expect(res.data.lists.length).toBe(1) - }) - - it('updates list', async () => { - const uri = new AtUri(listUri) - await agent.api.com.atproto.repo.putRecord( - { - repo: uri.hostname, - collection: uri.collection, - rkey: uri.rkey, - record: { - name: 'updated alice mutes', - purpose: 'app.bsky.graph.defs#modlist', - description: 'new descript', - createdAt: new Date().toISOString(), - }, - }, - { headers: sc.getHeaders(alice), encoding: 'application/json' }, - ) - - const got = await agent.api.app.bsky.graph.getList( - { list: listUri }, - { headers: sc.getHeaders(alice) }, - ) - expect(got.data.list.name).toBe('updated alice mutes') - expect(got.data.list.description).toBe('new descript') - expect(got.data.list.avatar).toBeUndefined() - expect(got.data.items.length).toBe(2) - }) - - it('embeds lists in posts', async () => { - const postRef = await sc.post( - alice, - 'list embed!', - undefined, - undefined, - new RecordRef(listUri, listCid), - ) - const res = await agent.api.app.bsky.feed.getPosts( - { uris: [postRef.ref.uriStr] }, - { headers: sc.getHeaders(alice) }, - ) - expect(res.data.posts.length).toBe(1) - expect(forSnapshot(res.data.posts[0])).toMatchSnapshot() - }) -}) diff --git a/packages/pds/tests/views/mutes.test.ts b/packages/pds/tests/views/mutes.test.ts deleted file mode 100644 index ac32d43ab6f..00000000000 --- a/packages/pds/tests/views/mutes.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import AtpAgent from '@atproto/api' -import { runTestServer, forSnapshot, CloseFn, paginateAll } from '../_util' -import { SeedClient } from '../seeds/client' -import usersBulkSeed from '../seeds/users-bulk' - -describe('mute views', () => { - let agent: AtpAgent - let close: CloseFn - let sc: SeedClient - let silas: string - - beforeAll(async () => { - const server = await runTestServer({ - dbPostgresSchema: 'views_mutes', - }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await usersBulkSeed(sc, 10) - silas = sc.dids['silas77.test'] - const mutes = [ - 'aliya-hodkiewicz.test', - 'adrienne49.test', - 'jeffrey-sawayn87.test', - 'nicolas-krajcik10.test', - 'magnus53.test', - 'elta48.test', - ] - for (const did of mutes) { - await agent.api.app.bsky.graph.muteActor( - { actor: did }, - { headers: sc.getHeaders(silas), encoding: 'application/json' }, - ) - } - await server.processAll() - }) - - afterAll(async () => { - await close() - }) - - it('fetches mutes for the logged-in user.', async () => { - const { data: view } = await agent.api.app.bsky.graph.getMutes( - {}, - { headers: sc.getHeaders(silas) }, - ) - expect(forSnapshot(view.mutes)).toMatchSnapshot() - }) - - it('paginates.', async () => { - const results = (results) => results.flatMap((res) => res.mutes) - const paginator = async (cursor?: string) => { - const { data: view } = await agent.api.app.bsky.graph.getMutes( - { cursor, limit: 2 }, - { headers: sc.getHeaders(silas) }, - ) - return view - } - - const paginatedAll = await paginateAll(paginator) - paginatedAll.forEach((res) => - expect(res.mutes.length).toBeLessThanOrEqual(2), - ) - - const full = await agent.api.app.bsky.graph.getMutes( - {}, - { headers: sc.getHeaders(silas) }, - ) - - expect(full.data.mutes.length).toEqual(6) - expect(results(paginatedAll)).toEqual(results([full.data])) - }) - - it('removes mute.', async () => { - const { data: initial } = await agent.api.app.bsky.graph.getMutes( - {}, - { headers: sc.getHeaders(silas) }, - ) - expect(initial.mutes.length).toEqual(6) - expect(initial.mutes.map((m) => m.handle)).toContain('elta48.test') - - await agent.api.app.bsky.graph.unmuteActor( - { actor: sc.dids['elta48.test'] }, - { headers: sc.getHeaders(silas), encoding: 'application/json' }, - ) - - const { data: final } = await agent.api.app.bsky.graph.getMutes( - {}, - { headers: sc.getHeaders(silas) }, - ) - expect(final.mutes.length).toEqual(5) - expect(final.mutes.map((m) => m.handle)).not.toContain('elta48.test') - - await agent.api.app.bsky.graph.muteActor( - { actor: sc.dids['elta48.test'] }, - { headers: sc.getHeaders(silas), encoding: 'application/json' }, - ) - }) - - it('does not allow muting self.', async () => { - const promise = agent.api.app.bsky.graph.muteActor( - { actor: silas }, - { headers: sc.getHeaders(silas), encoding: 'application/json' }, - ) - await expect(promise).rejects.toThrow('Cannot mute oneself') - }) -}) diff --git a/packages/pds/tests/views/notifications.test.ts b/packages/pds/tests/views/notifications.test.ts deleted file mode 100644 index 1cc86455126..00000000000 --- a/packages/pds/tests/views/notifications.test.ts +++ /dev/null @@ -1,310 +0,0 @@ -import AtpAgent from '@atproto/api' -import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' -import { - runTestServer, - forSnapshot, - CloseFn, - paginateAll, - adminAuth, - TestServerInfo, -} from '../_util' -import { SeedClient } from '../seeds/client' -import basicSeed from '../seeds/basic' -import { Database } from '../../src' -import { Notification } from '../../src/lexicon/types/app/bsky/notification/listNotifications' - -describe('pds notification views', () => { - let server: TestServerInfo - let agent: AtpAgent - let close: CloseFn - let db: Database - let sc: SeedClient - - // account dids, for convenience - let alice: string - - beforeAll(async () => { - server = await runTestServer({ - dbPostgresSchema: 'views_notifications', - }) - close = server.close - db = server.ctx.db - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await basicSeed(sc) - alice = sc.dids.alice - await server.processAll() - }) - - afterAll(async () => { - await close() - }) - - const sort = (notifs: Notification[]) => { - return notifs.sort((a, b) => { - if (a.indexedAt === b.indexedAt) { - return a.uri > b.uri ? -1 : 1 - } - return a.indexedAt > b.indexedAt ? -a : 1 - }) - } - - it('fetches notification count without a last-seen', async () => { - const notifCountAlice = - await agent.api.app.bsky.notification.getUnreadCount( - {}, - { headers: sc.getHeaders(alice) }, - ) - - expect(notifCountAlice.data.count).toBe(12) - - const notifCountBob = await agent.api.app.bsky.notification.getUnreadCount( - {}, - { headers: sc.getHeaders(sc.dids.bob) }, - ) - - expect(notifCountBob.data.count).toBe(4) - }) - - it('generates notifications for all reply ancestors', async () => { - // Add to reply chain, post ancestors: alice -> bob -> alice -> carol. - // Should have added one notification for each of alice and bob. - await sc.reply( - sc.dids.carol, - sc.posts[alice][1].ref, - sc.replies[alice][0].ref, - 'indeed', - ) - await server.processAll() - - const notifCountAlice = - await agent.api.app.bsky.notification.getUnreadCount( - {}, - { headers: sc.getHeaders(alice) }, - ) - - expect(notifCountAlice.data.count).toBe(13) - - const notifCountBob = await agent.api.app.bsky.notification.getUnreadCount( - {}, - { headers: sc.getHeaders(sc.dids.bob) }, - ) - - expect(notifCountBob.data.count).toBe(5) - }) - - it('does not give notifs for a deleted subject', async () => { - const root = await sc.post(sc.dids.alice, 'root') - const first = await sc.reply(sc.dids.bob, root.ref, root.ref, 'first') - await sc.deletePost(sc.dids.alice, root.ref.uri) - const second = await sc.reply(sc.dids.carol, root.ref, first.ref, 'second') - - const notifsAlice = await agent.api.app.bsky.notification.listNotifications( - {}, - { headers: sc.getHeaders(sc.dids.alice) }, - ) - const hasNotif = notifsAlice.data.notifications.some( - (notif) => notif.uri === second.ref.uriStr, - ) - expect(hasNotif).toBe(false) - - // cleanup - await sc.deletePost(sc.dids.bob, first.ref.uri) - await sc.deletePost(sc.dids.carol, second.ref.uri) - }) - - it('generates notifications for quotes', async () => { - // Dan was quoted by alice - const notifsDan = await agent.api.app.bsky.notification.listNotifications( - {}, - { headers: sc.getHeaders(sc.dids.dan) }, - ) - expect(forSnapshot(notifsDan.data)).toMatchSnapshot() - }) - - it('fetches notifications without a last-seen', async () => { - const notifRes = await agent.api.app.bsky.notification.listNotifications( - {}, - { headers: sc.getHeaders(alice) }, - ) - - const notifs = sort(notifRes.data.notifications) - expect(notifs.length).toBe(13) - - const readStates = notifs.map((notif) => notif.isRead) - expect(readStates).toEqual(notifs.map(() => false)) - - // @TODO while the exact order of these is not critically important, - // it's odd to see carol's follow after bob's. In the seed they occur in - // the opposite ordering. - expect(forSnapshot(notifs)).toMatchSnapshot() - }) - - it('fetches notifications omitting records by a muted user', async () => { - await agent.api.app.bsky.graph.muteActor( - { actor: sc.dids.carol }, // Replier - { headers: sc.getHeaders(alice), encoding: 'application/json' }, - ) - await agent.api.app.bsky.graph.muteActor( - { actor: sc.dids.dan }, // Mentioner - { headers: sc.getHeaders(alice), encoding: 'application/json' }, - ) - - const notifRes = await agent.api.app.bsky.notification.listNotifications( - {}, - { headers: sc.getHeaders(alice) }, - ) - const notifCount = await agent.api.app.bsky.notification.getUnreadCount( - {}, - { headers: sc.getHeaders(alice) }, - ) - - const notifs = sort(notifRes.data.notifications) - expect(notifs.length).toBe(4) - expect(forSnapshot(notifs)).toMatchSnapshot() - expect(notifCount.data.count).toBe(4) - - // Cleanup - await agent.api.app.bsky.graph.unmuteActor( - { actor: sc.dids.carol }, - { headers: sc.getHeaders(alice), encoding: 'application/json' }, - ) - await agent.api.app.bsky.graph.unmuteActor( - { actor: sc.dids.dan }, - { headers: sc.getHeaders(alice), encoding: 'application/json' }, - ) - }) - - it('fetches notifications omitting mentions and replies for taken-down posts', async () => { - const postRef1 = sc.replies[sc.dids.carol][0].ref // Reply - const postRef2 = sc.posts[sc.dids.dan][1].ref // Mention - const actionResults = await Promise.all( - [postRef1, postRef2].map((postRef) => - agent.api.com.atproto.admin.takeModerationAction( - { - action: TAKEDOWN, - subject: { - $type: 'com.atproto.repo.strongRef', - uri: postRef.uriStr, - cid: postRef.cidStr, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ), - ), - ) - - const notifRes = await agent.api.app.bsky.notification.listNotifications( - {}, - { headers: sc.getHeaders(alice) }, - ) - const notifCount = await agent.api.app.bsky.notification.getUnreadCount( - {}, - { headers: sc.getHeaders(alice) }, - ) - - const notifs = sort(notifRes.data.notifications) - expect(notifs.length).toBe(11) - expect(forSnapshot(notifs)).toMatchSnapshot() - expect(notifCount.data.count).toBe(11) - - // Cleanup - await Promise.all( - actionResults.map((result) => - agent.api.com.atproto.admin.reverseModerationAction( - { - id: result.data.id, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ), - ), - ) - }) - - it('paginates', async () => { - const results = (results) => - sort(results.flatMap((res) => res.notifications)) - const paginator = async (cursor?: string) => { - const res = await agent.api.app.bsky.notification.listNotifications( - { - cursor, - limit: 6, - }, - { headers: sc.getHeaders(alice) }, - ) - return res.data - } - - const paginatedAll = await paginateAll(paginator) - paginatedAll.forEach((res) => - expect(res.notifications.length).toBeLessThanOrEqual(6), - ) - - const full = await agent.api.app.bsky.notification.listNotifications( - {}, - { - headers: sc.getHeaders(alice), - }, - ) - - expect(full.data.notifications.length).toEqual(13) - expect(results(paginatedAll)).toEqual(results([full.data])) - }) - - it('updates notifications last seen', async () => { - const full = await agent.api.app.bsky.notification.listNotifications( - {}, - { - headers: sc.getHeaders(alice), - }, - ) - - // Need to look-up createdAt time as a cursor since it's not in the method's output - const beforeNotif = await db.db - .selectFrom('user_notification') - .selectAll() - .where('recordUri', '=', full.data.notifications[3].uri) - .executeTakeFirstOrThrow() - - await agent.api.app.bsky.notification.updateSeen( - { seenAt: beforeNotif.indexedAt }, - { encoding: 'application/json', headers: sc.getHeaders(alice) }, - ) - }) - - it('fetches notification count with a last-seen', async () => { - const notifCount = await agent.api.app.bsky.notification.getUnreadCount( - {}, - { headers: sc.getHeaders(alice) }, - ) - - expect(notifCount.data.count).toBe(3) - }) - - it('fetches notifications with a last-seen', async () => { - const notifRes = await agent.api.app.bsky.notification.listNotifications( - {}, - { - headers: sc.getHeaders(alice), - }, - ) - - const notifs = sort(notifRes.data.notifications) - expect(notifs.length).toBe(13) - - const readStates = notifs.map((notif) => notif.isRead) - expect(readStates).toEqual(notifs.map((_, i) => i >= 3)) - - expect(forSnapshot(notifs)).toMatchSnapshot() - }) -}) diff --git a/packages/pds/tests/views/popular.test.ts b/packages/pds/tests/views/popular.test.ts deleted file mode 100644 index ca21ee31f4f..00000000000 --- a/packages/pds/tests/views/popular.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import AtpAgent from '@atproto/api' -import { runTestServer, CloseFn, TestServerInfo } from '../_util' -import { SeedClient } from '../seeds/client' -import basicSeed from '../seeds/basic' - -describe('popular views', () => { - let server: TestServerInfo - let agent: AtpAgent - let close: CloseFn - let sc: SeedClient - - // account dids, for convenience - let alice: string - let bob: string - - const account = { - email: 'blah@test.com', - password: 'blh-pass', - } - - beforeAll(async () => { - server = await runTestServer({ - dbPostgresSchema: 'views_popular', - }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await basicSeed(sc) - - await sc.createAccount('eve', { - ...account, - email: 'eve@test.com', - handle: 'eve.test', - password: 'eve-pass', - }) - await sc.createAccount('frank', { - ...account, - email: 'frank@test.com', - handle: 'frank.test', - password: 'frank-pass', - }) - await sc.createAccount('george', { - ...account, - email: 'george@test.com', - handle: 'george.test', - password: 'george-pass', - }) - await sc.createAccount('helen', { - ...account, - email: 'helen@test.com', - handle: 'helen.test', - password: 'helen-pass', - }) - - alice = sc.dids.alice - bob = sc.dids.bob - await server.processAll() - }) - - afterAll(async () => { - await close() - }) - - it('returns well liked posts', async () => { - const img = await sc.uploadFile( - alice, - 'tests/image/fixtures/key-landscape-small.jpg', - 'image/jpeg', - ) - const one = await sc.post(alice, 'first post', undefined, [img]) - const two = await sc.post(bob, 'bobby boi') - const three = await sc.reply(bob, one.ref, one.ref, 'reply') - - for (let i = 0; i < 12; i++) { - const name = `user${i}` - await sc.createAccount(name, { - handle: `user${i}.test`, - email: `user${i}@test.com`, - password: 'password', - }) - await sc.like(sc.dids[name], one.ref) - await sc.like(sc.dids[name], two.ref) - await sc.like(sc.dids[name], three.ref) - } - await server.processAll() - - const res = await agent.api.app.bsky.unspecced.getPopular( - {}, - { headers: sc.getHeaders(alice) }, - ) - const feedUris = res.data.feed.map((i) => i.post.uri).sort() - const expected = [one.ref.uriStr, two.ref.uriStr].sort() - expect(feedUris).toEqual(expected) - }) - - it('does not return muted posts', async () => { - await agent.api.app.bsky.graph.muteActor( - { actor: bob }, - { headers: sc.getHeaders(alice), encoding: 'application/json' }, - ) - - const res = await agent.api.app.bsky.unspecced.getPopular( - {}, - { headers: sc.getHeaders(alice) }, - ) - expect(res.data.feed.length).toBe(1) - const dids = res.data.feed.map((post) => post.post.author.did) - expect(dids.includes(bob)).toBe(false) - }) -}) diff --git a/packages/pds/tests/views/posts.test.ts b/packages/pds/tests/views/posts.test.ts deleted file mode 100644 index f6aa0551b07..00000000000 --- a/packages/pds/tests/views/posts.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import AtpAgent from '@atproto/api' -import { runTestServer, forSnapshot, TestServerInfo } from '../_util' -import { SeedClient } from '../seeds/client' -import basicSeed from '../seeds/basic' - -describe('pds posts views', () => { - let server: TestServerInfo - let agent: AtpAgent - let sc: SeedClient - - beforeAll(async () => { - server = await runTestServer({ - dbPostgresSchema: 'views_posts', - }) - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await basicSeed(sc) - await server.processAll() - }) - - afterAll(async () => { - await server.close() - }) - - it('fetches posts', async () => { - const uris = [ - sc.posts[sc.dids.alice][0].ref.uriStr, - sc.posts[sc.dids.alice][1].ref.uriStr, - sc.posts[sc.dids.bob][0].ref.uriStr, - sc.posts[sc.dids.carol][0].ref.uriStr, - sc.posts[sc.dids.dan][1].ref.uriStr, - sc.replies[sc.dids.alice][0].ref.uriStr, - ] - const posts = await agent.api.app.bsky.feed.getPosts( - { uris }, - { headers: sc.getHeaders(sc.dids.alice) }, - ) - - expect(posts.data.posts.length).toBe(uris.length) - expect(forSnapshot(posts.data.posts)).toMatchSnapshot() - }) - - it('handles repeat uris', async () => { - const uris = [ - sc.posts[sc.dids.alice][0].ref.uriStr, - sc.posts[sc.dids.alice][0].ref.uriStr, - sc.posts[sc.dids.bob][0].ref.uriStr, - sc.posts[sc.dids.alice][0].ref.uriStr, - sc.posts[sc.dids.bob][0].ref.uriStr, - ] - - const posts = await agent.api.app.bsky.feed.getPosts( - { uris }, - { headers: sc.getHeaders(sc.dids.alice) }, - ) - - expect(posts.data.posts.length).toBe(2) - const receivedUris = posts.data.posts.map((p) => p.uri).sort() - const expected = [ - sc.posts[sc.dids.alice][0].ref.uriStr, - sc.posts[sc.dids.bob][0].ref.uriStr, - ].sort() - expect(receivedUris).toEqual(expected) - }) -}) diff --git a/packages/pds/tests/views/profile.test.ts b/packages/pds/tests/views/profile.test.ts deleted file mode 100644 index b1a3e5003a9..00000000000 --- a/packages/pds/tests/views/profile.test.ts +++ /dev/null @@ -1,285 +0,0 @@ -import fs from 'fs/promises' -import AtpAgent from '@atproto/api' -import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' -import { ids } from '../../src/lexicon/lexicons' -import { runTestServer, forSnapshot, CloseFn, adminAuth } from '../_util' -import { SeedClient } from '../seeds/client' -import basicSeed from '../seeds/basic' - -describe('pds profile views', () => { - let agent: AtpAgent - let close: CloseFn - let sc: SeedClient - - // account dids, for convenience - let alice: string - let bob: string - let dan: string - - beforeAll(async () => { - const server = await runTestServer({ - dbPostgresSchema: 'views_profile', - }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await basicSeed(sc) - alice = sc.dids.alice - bob = sc.dids.bob - dan = sc.dids.dan - await server.processAll() - }) - - afterAll(async () => { - await close() - }) - - it('fetches own profile', async () => { - const aliceForAlice = await agent.api.app.bsky.actor.getProfile( - { actor: alice }, - { headers: sc.getHeaders(alice) }, - ) - - expect(forSnapshot(aliceForAlice.data)).toMatchSnapshot() - }) - - it('reflects self-labels', async () => { - const aliceForBob = await agent.api.app.bsky.actor.getProfile( - { actor: alice }, - { headers: sc.getHeaders(bob) }, - ) - - const labels = aliceForBob.data.labels - ?.filter((label) => label.src === alice) - .map((label) => label.val) - .sort() - - expect(labels).toEqual(['self-label-a', 'self-label-b']) - }) - - it("fetches other's profile, with a follow", async () => { - const aliceForBob = await agent.api.app.bsky.actor.getProfile( - { actor: alice }, - { headers: sc.getHeaders(bob) }, - ) - - expect(forSnapshot(aliceForBob.data)).toMatchSnapshot() - }) - - it("fetches other's profile, without a follow", async () => { - const danForBob = await agent.api.app.bsky.actor.getProfile( - { actor: dan }, - { headers: sc.getHeaders(bob) }, - ) - - expect(forSnapshot(danForBob.data)).toMatchSnapshot() - }) - - it('fetches multiple profiles', async () => { - const { - data: { profiles }, - } = await agent.api.app.bsky.actor.getProfiles( - { - actors: [ - alice, - 'bob.test', - 'did:example:missing', - 'carol.test', - dan, - 'missing.test', - ], - }, - { headers: sc.getHeaders(bob) }, - ) - - expect(profiles.map((p) => p.handle)).toEqual([ - 'alice.test', - 'bob.test', - 'carol.test', - 'dan.test', - ]) - - expect(forSnapshot(profiles)).toMatchSnapshot() - }) - - it('updates profile', async () => { - await updateProfile(agent, alice, { - displayName: 'ali', - description: 'new descript', - avatar: sc.profiles[alice].avatar, - }) - - const aliceForAlice = await agent.api.app.bsky.actor.getProfile( - { actor: alice }, - { headers: sc.getHeaders(alice) }, - ) - - expect(forSnapshot(aliceForAlice.data)).toMatchSnapshot() - }) - - it('handles avatars & banners', async () => { - const avatarImg = await fs.readFile( - 'tests/image/fixtures/key-portrait-small.jpg', - ) - const bannerImg = await fs.readFile( - 'tests/image/fixtures/key-landscape-small.jpg', - ) - const avatarRes = await agent.api.com.atproto.repo.uploadBlob(avatarImg, { - headers: sc.getHeaders(alice), - encoding: 'image/jpeg', - }) - const bannerRes = await agent.api.com.atproto.repo.uploadBlob(bannerImg, { - headers: sc.getHeaders(alice), - encoding: 'image/jpeg', - }) - - await updateProfile(agent, alice, { - displayName: 'ali', - description: 'new descript', - avatar: avatarRes.data.blob, - banner: bannerRes.data.blob, - }) - - const aliceForAlice = await agent.api.app.bsky.actor.getProfile( - { actor: alice }, - { headers: sc.getHeaders(alice) }, - ) - - expect(forSnapshot(aliceForAlice.data)).toMatchSnapshot() - }) - - it('handles unsetting profile fields', async () => { - await updateProfile(agent, alice, {}) - - const aliceForAlice = await agent.api.app.bsky.actor.getProfile( - { actor: alice }, - { headers: sc.getHeaders(alice) }, - ) - - expect(aliceForAlice.data.displayName).toBeUndefined() - expect(aliceForAlice.data.description).toBeUndefined() - expect(aliceForAlice.data.avatar).toBeUndefined() - expect(aliceForAlice.data.banner).toBeUndefined() - expect(forSnapshot(aliceForAlice.data)).toMatchSnapshot() - }) - - it('creates new profile', async () => { - await updateProfile(agent, dan, { displayName: 'danny boy' }) - - const danForDan = await agent.api.app.bsky.actor.getProfile( - { actor: dan }, - { headers: sc.getHeaders(dan) }, - ) - - expect(forSnapshot(danForDan.data)).toMatchSnapshot() - }) - - it('fetches profile by handle', async () => { - const byDid = await agent.api.app.bsky.actor.getProfile( - { actor: alice }, - { - headers: sc.getHeaders(bob), - }, - ) - - const byHandle = await agent.api.app.bsky.actor.getProfile( - { actor: sc.accounts[alice].handle }, - { headers: sc.getHeaders(bob) }, - ) - - expect(byHandle.data).toEqual(byDid.data) - }) - - it('blocked by actor takedown', async () => { - const { data: action } = - await agent.api.com.atproto.admin.takeModerationAction( - { - action: TAKEDOWN, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: alice, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - const promise = agent.api.app.bsky.actor.getProfile( - { actor: alice }, - { headers: sc.getHeaders(bob) }, - ) - - await expect(promise).rejects.toThrow('Account has been taken down') - - // Cleanup - await agent.api.com.atproto.admin.reverseModerationAction( - { - id: action.id, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - }) - - it('includes muted status.', async () => { - const { data: initial } = await agent.api.app.bsky.actor.getProfile( - { actor: alice }, - { headers: sc.getHeaders(bob) }, - ) - - expect(initial.viewer?.muted).toEqual(false) - - await agent.api.app.bsky.graph.muteActor( - { actor: alice }, - { headers: sc.getHeaders(bob), encoding: 'application/json' }, - ) - const { data: muted } = await agent.api.app.bsky.actor.getProfile( - { actor: alice }, - { headers: sc.getHeaders(bob) }, - ) - - expect(muted.viewer?.muted).toEqual(true) - - const { data: fromBobUnrelated } = - await agent.api.app.bsky.actor.getProfile( - { actor: dan }, - { headers: sc.getHeaders(bob) }, - ) - const { data: toAliceUnrelated } = - await agent.api.app.bsky.actor.getProfile( - { actor: alice }, - { headers: sc.getHeaders(dan) }, - ) - - expect(fromBobUnrelated.viewer?.muted).toEqual(false) - expect(toAliceUnrelated.viewer?.muted).toEqual(false) - - await agent.api.app.bsky.graph.unmuteActor( - { actor: alice }, - { headers: sc.getHeaders(bob), encoding: 'application/json' }, - ) - }) - - async function updateProfile( - agent: AtpAgent, - did: string, - record: Record, - ) { - return await agent.api.com.atproto.repo.putRecord( - { - repo: did, - collection: ids.AppBskyActorProfile, - rkey: 'self', - record, - }, - { headers: sc.getHeaders(did), encoding: 'application/json' }, - ) - } -}) diff --git a/packages/pds/tests/views/reposts.test.ts b/packages/pds/tests/views/reposts.test.ts deleted file mode 100644 index 1c61215e3e5..00000000000 --- a/packages/pds/tests/views/reposts.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import AtpAgent from '@atproto/api' -import { runTestServer, forSnapshot, CloseFn, paginateAll } from '../_util' -import { SeedClient } from '../seeds/client' -import repostsSeed from '../seeds/reposts' - -describe('pds repost views', () => { - let agent: AtpAgent - let close: CloseFn - let sc: SeedClient - - // account dids, for convenience - let alice: string - let bob: string - - beforeAll(async () => { - const server = await runTestServer({ - dbPostgresSchema: 'views_reposts', - }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await repostsSeed(sc) - alice = sc.dids.alice - bob = sc.dids.bob - await server.processAll() - }) - - afterAll(async () => { - await close() - }) - - it('fetches reposted-by for a post', async () => { - const view = await agent.api.app.bsky.feed.getRepostedBy( - { uri: sc.posts[alice][2].ref.uriStr }, - { headers: sc.getHeaders(alice) }, - ) - expect(view.data.uri).toEqual(sc.posts[sc.dids.alice][2].ref.uriStr) - expect(forSnapshot(view.data.repostedBy)).toMatchSnapshot() - }) - - it('fetches reposted-by for a reply', async () => { - const view = await agent.api.app.bsky.feed.getRepostedBy( - { uri: sc.replies[bob][0].ref.uriStr }, - { headers: sc.getHeaders(alice) }, - ) - expect(view.data.uri).toEqual(sc.replies[sc.dids.bob][0].ref.uriStr) - expect(forSnapshot(view.data.repostedBy)).toMatchSnapshot() - }) - - it('paginates', async () => { - const results = (results) => results.flatMap((res) => res.repostedBy) - const paginator = async (cursor?: string) => { - const res = await agent.api.app.bsky.feed.getRepostedBy( - { - uri: sc.posts[alice][2].ref.uriStr, - cursor, - limit: 2, - }, - { headers: sc.getHeaders(alice) }, - ) - return res.data - } - - const paginatedAll = await paginateAll(paginator) - paginatedAll.forEach((res) => - expect(res.repostedBy.length).toBeLessThanOrEqual(2), - ) - - const full = await agent.api.app.bsky.feed.getRepostedBy( - { uri: sc.posts[alice][2].ref.uriStr }, - { headers: sc.getHeaders(alice) }, - ) - - expect(full.data.repostedBy.length).toEqual(4) - expect(results(paginatedAll)).toEqual(results([full.data])) - }) -}) diff --git a/packages/pds/tests/views/suggestions.test.ts b/packages/pds/tests/views/suggestions.test.ts deleted file mode 100644 index 4bec6dc3dd2..00000000000 --- a/packages/pds/tests/views/suggestions.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import AtpAgent from '@atproto/api' -import { runTestServer, CloseFn } from '../_util' -import { SeedClient } from '../seeds/client' -import basicSeed from '../seeds/basic' - -describe('pds user search views', () => { - let agent: AtpAgent - let close: CloseFn - let sc: SeedClient - - beforeAll(async () => { - const server = await runTestServer({ - dbPostgresSchema: 'views_suggestions', - }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await basicSeed(sc) - await server.processAll() - - const suggestions = [ - { did: sc.dids.bob, order: 1 }, - { did: sc.dids.carol, order: 2 }, - { did: sc.dids.dan, order: 3 }, - ] - await server.ctx.db.db - .insertInto('suggested_follow') - .values(suggestions) - .execute() - }) - - afterAll(async () => { - await close() - }) - - it('actor suggestion gives users', async () => { - const result = await agent.api.app.bsky.actor.getSuggestions( - {}, - { headers: sc.getHeaders(sc.dids.carol) }, - ) - - // does not include carol, because she is requesting - expect(result.data.actors.length).toBe(2) - expect(result.data.actors[0].handle).toEqual('bob.test') - expect(result.data.actors[0].displayName).toEqual('bobby') - expect(result.data.actors[1].handle).toEqual('dan.test') - expect(result.data.actors[1].displayName).toBeUndefined() - }) - - it('does not suggest followed users', async () => { - const result = await agent.api.app.bsky.actor.getSuggestions( - {}, - { headers: sc.getHeaders(sc.dids.alice) }, - ) - - // alice follows everyone - expect(result.data.actors.length).toBe(0) - }) - - it('paginates', async () => { - const result1 = await agent.api.app.bsky.actor.getSuggestions( - { limit: 1 }, - { headers: sc.getHeaders(sc.dids.carol) }, - ) - const result2 = await agent.api.app.bsky.actor.getSuggestions( - { limit: 1, cursor: result1.data.cursor }, - { headers: sc.getHeaders(sc.dids.carol) }, - ) - - expect(result1.data.actors.length).toBe(1) - expect(result1.data.actors[0].handle).toEqual('bob.test') - expect(result2.data.actors.length).toBe(1) - expect(result2.data.actors[0].handle).toEqual('dan.test') - }) -}) diff --git a/packages/pds/tests/views/thread.test.ts b/packages/pds/tests/views/thread.test.ts deleted file mode 100644 index 1efee8b0526..00000000000 --- a/packages/pds/tests/views/thread.test.ts +++ /dev/null @@ -1,456 +0,0 @@ -import assert from 'assert' -import AtpAgent, { AppBskyFeedGetPostThread } from '@atproto/api' -import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' -import { isThreadViewPost } from '@atproto/api/src/client/types/app/bsky/feed/defs' -import { - runTestServer, - forSnapshot, - CloseFn, - adminAuth, - TestServerInfo, -} from '../_util' -import { SeedClient } from '../seeds/client' -import basicSeed from '../seeds/basic' - -describe('pds thread views', () => { - let server: TestServerInfo - let agent: AtpAgent - let close: CloseFn - let sc: SeedClient - - // account dids, for convenience - let alice: string - let bob: string - let carol: string - - beforeAll(async () => { - server = await runTestServer({ - dbPostgresSchema: 'views_thread', - }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await basicSeed(sc) - await server.processAll() - alice = sc.dids.alice - bob = sc.dids.bob - carol = sc.dids.carol - }) - - beforeAll(async () => { - // Add a repost of a reply so that we can confirm viewer state in the thread - await sc.repost(bob, sc.replies[alice][0].ref) - await server.processAll() - }) - - afterAll(async () => { - await close() - }) - - it('fetches deep post thread', async () => { - const thread = await agent.api.app.bsky.feed.getPostThread( - { uri: sc.posts[alice][1].ref.uriStr }, - { headers: sc.getHeaders(bob) }, - ) - - expect(forSnapshot(thread.data.thread)).toMatchSnapshot() - }) - - it('fetches shallow post thread', async () => { - const thread = await agent.api.app.bsky.feed.getPostThread( - { depth: 1, uri: sc.posts[alice][1].ref.uriStr }, - { headers: sc.getHeaders(bob) }, - ) - - expect(forSnapshot(thread.data.thread)).toMatchSnapshot() - }) - - it('fetches ancestors', async () => { - const thread = await agent.api.app.bsky.feed.getPostThread( - { depth: 1, uri: sc.replies[alice][0].ref.uriStr }, - { headers: sc.getHeaders(bob) }, - ) - - expect(forSnapshot(thread.data.thread)).toMatchSnapshot() - }) - - it('fetches shallow ancestors', async () => { - const thread = await agent.api.app.bsky.feed.getPostThread( - { depth: 1, parentHeight: 1, uri: sc.replies[alice][0].ref.uriStr }, - { headers: sc.getHeaders(bob) }, - ) - - expect(forSnapshot(thread.data.thread)).toMatchSnapshot() - }) - - it('fails for an unknown post', async () => { - const promise = agent.api.app.bsky.feed.getPostThread( - { uri: 'at://did:example:fake/does.not.exist/self' }, - { headers: sc.getHeaders(bob) }, - ) - - await expect(promise).rejects.toThrow( - AppBskyFeedGetPostThread.NotFoundError, - ) - }) - - it('includes the muted status of post authors.', async () => { - await agent.api.app.bsky.graph.muteActor( - { actor: alice }, - { headers: sc.getHeaders(bob), encoding: 'application/json' }, - ) - const thread = await agent.api.app.bsky.feed.getPostThread( - { uri: sc.posts[alice][1].ref.uriStr }, - { headers: sc.getHeaders(bob) }, - ) - - expect(forSnapshot(thread.data.thread)).toMatchSnapshot() - - await agent.api.app.bsky.graph.unmuteActor( - { actor: alice }, - { encoding: 'application/json', headers: sc.getHeaders(bob) }, - ) - }) - - it('handles deleted posts correctly', async () => { - const alice = sc.dids.alice - const bob = sc.dids.bob - - const indexes = { - aliceRoot: -1, - bobReply: -1, - aliceReplyReply: -1, - } - - await sc.post(alice, 'Deletion thread') - indexes.aliceRoot = sc.posts[alice].length - 1 - - await sc.reply( - bob, - sc.posts[alice][indexes.aliceRoot].ref, - sc.posts[alice][indexes.aliceRoot].ref, - 'Reply', - ) - indexes.bobReply = sc.replies[bob].length - 1 - await sc.reply( - alice, - sc.posts[alice][indexes.aliceRoot].ref, - sc.replies[bob][indexes.bobReply].ref, - 'Reply reply', - ) - indexes.aliceReplyReply = sc.replies[alice].length - 1 - await server.processAll() - - const thread1 = await agent.api.app.bsky.feed.getPostThread( - { uri: sc.posts[alice][indexes.aliceRoot].ref.uriStr }, - { headers: sc.getHeaders(bob) }, - ) - expect(forSnapshot(thread1.data.thread)).toMatchSnapshot() - - await sc.deletePost(bob, sc.replies[bob][indexes.bobReply].ref.uri) - await server.processAll() - - const thread2 = await agent.api.app.bsky.feed.getPostThread( - { uri: sc.posts[alice][indexes.aliceRoot].ref.uriStr }, - { headers: sc.getHeaders(bob) }, - ) - expect(forSnapshot(thread2.data.thread)).toMatchSnapshot() - - const thread3 = await agent.api.app.bsky.feed.getPostThread( - { uri: sc.replies[alice][indexes.aliceReplyReply].ref.uriStr }, - { headers: sc.getHeaders(bob) }, - ) - expect(forSnapshot(thread3.data.thread)).toMatchSnapshot() - }) - - it('reflects self-labels', async () => { - const { data: thread } = await agent.api.app.bsky.feed.getPostThread( - { uri: sc.posts[alice][0].ref.uriStr }, - { headers: sc.getHeaders(bob) }, - ) - - assert(isThreadViewPost(thread.thread), 'post does not exist') - const post = thread.thread.post - - const postSelfLabels = post.labels - ?.filter((label) => label.src === alice) - .map((label) => label.val) - - expect(postSelfLabels).toEqual(['self-label']) - - const authorSelfLabels = post.author.labels - ?.filter((label) => label.src === alice) - .map((label) => label.val) - .sort() - - expect(authorSelfLabels).toEqual(['self-label-a', 'self-label-b']) - }) - - it('blocks post by actor takedown', async () => { - const { data: modAction } = - await agent.api.com.atproto.admin.takeModerationAction( - { - action: TAKEDOWN, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: alice, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - - // Same as shallow post thread test, minus alice - const promise = agent.api.app.bsky.feed.getPostThread( - { depth: 1, uri: sc.posts[alice][1].ref.uriStr }, - { headers: sc.getHeaders(bob) }, - ) - - await expect(promise).rejects.toThrow( - AppBskyFeedGetPostThread.NotFoundError, - ) - - // Cleanup - await agent.api.com.atproto.admin.reverseModerationAction( - { - id: modAction.id, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - }) - - it('blocks replies by actor takedown', async () => { - const { data: modAction } = - await agent.api.com.atproto.admin.takeModerationAction( - { - action: TAKEDOWN, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: carol, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - - // Same as deep post thread test, minus carol - const thread = await agent.api.app.bsky.feed.getPostThread( - { uri: sc.posts[alice][1].ref.uriStr }, - { headers: sc.getHeaders(bob) }, - ) - - expect(forSnapshot(thread.data.thread)).toMatchSnapshot() - - // Cleanup - await agent.api.com.atproto.admin.reverseModerationAction( - { - id: modAction.id, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - }) - - it('blocks ancestors by actor takedown', async () => { - const { data: modAction } = - await agent.api.com.atproto.admin.takeModerationAction( - { - action: TAKEDOWN, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: bob, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - - // Same as ancestor post thread test, minus bob - const thread = await agent.api.app.bsky.feed.getPostThread( - { depth: 1, uri: sc.replies[alice][0].ref.uriStr }, - { headers: sc.getHeaders(bob) }, - ) - - expect(forSnapshot(thread.data.thread)).toMatchSnapshot() - - // Cleanup - await agent.api.com.atproto.admin.reverseModerationAction( - { - id: modAction.id, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - }) - - it('blocks post by record takedown', async () => { - const postRef = sc.posts[alice][1].ref - const { data: modAction } = - await agent.api.com.atproto.admin.takeModerationAction( - { - action: TAKEDOWN, - subject: { - $type: 'com.atproto.repo.strongRef', - uri: postRef.uriStr, - cid: postRef.cidStr, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - - const promise = agent.api.app.bsky.feed.getPostThread( - { depth: 1, uri: postRef.uriStr }, - { headers: sc.getHeaders(bob) }, - ) - - await expect(promise).rejects.toThrow( - AppBskyFeedGetPostThread.NotFoundError, - ) - - // Cleanup - await agent.api.com.atproto.admin.reverseModerationAction( - { - id: modAction.id, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - }) - - it('blocks ancestors by record takedown', async () => { - const threadPreTakedown = await agent.api.app.bsky.feed.getPostThread( - { depth: 1, uri: sc.replies[alice][0].ref.uriStr }, - { headers: sc.getHeaders(bob) }, - ) - - const parent = threadPreTakedown.data.thread.parent?.['post'] - - const { data: modAction } = - await agent.api.com.atproto.admin.takeModerationAction( - { - action: TAKEDOWN, - subject: { - $type: 'com.atproto.repo.strongRef', - uri: parent.uri, - cid: parent.cid, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - - // Same as ancestor post thread test, minus parent post - const thread = await agent.api.app.bsky.feed.getPostThread( - { depth: 1, uri: sc.replies[alice][0].ref.uriStr }, - { headers: sc.getHeaders(bob) }, - ) - - expect(forSnapshot(thread.data.thread)).toMatchSnapshot() - - // Cleanup - await agent.api.com.atproto.admin.reverseModerationAction( - { - id: modAction.id, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - }) - - it('blocks replies by record takedown', async () => { - const threadPreTakedown = await agent.api.app.bsky.feed.getPostThread( - { uri: sc.posts[alice][1].ref.uriStr }, - { headers: sc.getHeaders(bob) }, - ) - const post1 = threadPreTakedown.data.thread.replies?.[0].post - const post2 = threadPreTakedown.data.thread.replies?.[1].replies[0].post - - const actionResults = await Promise.all( - [post1, post2].map((post) => - agent.api.com.atproto.admin.takeModerationAction( - { - action: TAKEDOWN, - subject: { - $type: 'com.atproto.repo.strongRef', - uri: post.uri, - cid: post.cid, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ), - ), - ) - - // Same as deep post thread test, minus some replies - const thread = await agent.api.app.bsky.feed.getPostThread( - { uri: sc.posts[alice][1].ref.uriStr }, - { headers: sc.getHeaders(bob) }, - ) - - expect(forSnapshot(thread.data.thread)).toMatchSnapshot() - - // Cleanup - await Promise.all( - actionResults.map((result) => - agent.api.com.atproto.admin.reverseModerationAction( - { - id: result.data.id, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ), - ), - ) - }) -}) diff --git a/packages/pds/tests/views/timeline.test.ts b/packages/pds/tests/views/timeline.test.ts deleted file mode 100644 index 5dd9ba4a893..00000000000 --- a/packages/pds/tests/views/timeline.test.ts +++ /dev/null @@ -1,310 +0,0 @@ -import assert from 'assert' -import AtpAgent from '@atproto/api' -import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' -import { FeedViewPost } from '../../src/lexicon/types/app/bsky/feed/defs' -import { - runTestServer, - forSnapshot, - CloseFn, - getOriginator, - paginateAll, - adminAuth, -} from '../_util' -import { SeedClient } from '../seeds/client' -import basicSeed from '../seeds/basic' -import { FeedAlgorithm } from '../../src/app-view/api/app/bsky/util/feed' - -describe('timeline views', () => { - let agent: AtpAgent - let close: CloseFn - let sc: SeedClient - - // account dids, for convenience - let alice: string - let bob: string - let carol: string - let dan: string - - beforeAll(async () => { - const server = await runTestServer({ - dbPostgresSchema: 'views_home_feed', - }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await basicSeed(sc) - alice = sc.dids.alice - bob = sc.dids.bob - carol = sc.dids.carol - dan = sc.dids.dan - // Label posts as "kind" to check labels on embed views - const labelPostA = sc.posts[bob][0].ref - const labelPostB = sc.posts[carol][0].ref - await server.ctx.services.appView - .label(server.ctx.db) - .formatAndCreate( - server.ctx.cfg.labelerDid, - labelPostA.uriStr, - labelPostA.cidStr, - { create: ['kind'] }, - ) - await server.ctx.services.appView - .label(server.ctx.db) - .formatAndCreate( - server.ctx.cfg.labelerDid, - labelPostB.uriStr, - labelPostB.cidStr, - { create: ['kind'] }, - ) - await server.processAll() - }) - - afterAll(async () => { - await close() - }) - - it("fetches authenticated user's home feed w/ reverse-chronological algorithm", async () => { - const expectOriginatorFollowedBy = (did) => (item: FeedViewPost) => { - const originator = getOriginator(item) - // The user expects to see posts & reposts from themselves and follows - if (did !== originator) { - expect(sc.follows[did]).toHaveProperty(originator) - } - } - - const aliceTL = await agent.api.app.bsky.feed.getTimeline( - { algorithm: FeedAlgorithm.ReverseChronological }, - { - headers: sc.getHeaders(alice), - }, - ) - - expect(forSnapshot(aliceTL.data.feed)).toMatchSnapshot() - aliceTL.data.feed.forEach(expectOriginatorFollowedBy(alice)) - - const bobTL = await agent.api.app.bsky.feed.getTimeline( - { algorithm: FeedAlgorithm.ReverseChronological }, - { - headers: sc.getHeaders(bob), - }, - ) - - expect(forSnapshot(bobTL.data.feed)).toMatchSnapshot() - bobTL.data.feed.forEach(expectOriginatorFollowedBy(bob)) - - const carolTL = await agent.api.app.bsky.feed.getTimeline( - { algorithm: FeedAlgorithm.ReverseChronological }, - { - headers: sc.getHeaders(carol), - }, - ) - - expect(forSnapshot(carolTL.data.feed)).toMatchSnapshot() - carolTL.data.feed.forEach(expectOriginatorFollowedBy(carol)) - - const danTL = await agent.api.app.bsky.feed.getTimeline( - { algorithm: FeedAlgorithm.ReverseChronological }, - { - headers: sc.getHeaders(dan), - }, - ) - - expect(forSnapshot(danTL.data.feed)).toMatchSnapshot() - danTL.data.feed.forEach(expectOriginatorFollowedBy(dan)) - }) - - it("fetches authenticated user's home feed w/ default algorithm", async () => { - const defaultTL = await agent.api.app.bsky.feed.getTimeline( - {}, - { - headers: sc.getHeaders(alice), - }, - ) - const reverseChronologicalTL = await agent.api.app.bsky.feed.getTimeline( - { algorithm: FeedAlgorithm.ReverseChronological }, - { - headers: sc.getHeaders(alice), - }, - ) - expect(defaultTL.data.feed).toEqual(reverseChronologicalTL.data.feed) - }) - - it('omits posts and reposts of muted authors.', async () => { - await agent.api.app.bsky.graph.muteActor( - { actor: bob }, - { headers: sc.getHeaders(alice), encoding: 'application/json' }, - ) - await agent.api.app.bsky.graph.muteActor( - { actor: carol }, - { headers: sc.getHeaders(alice), encoding: 'application/json' }, - ) - - const aliceTL = await agent.api.app.bsky.feed.getTimeline( - { algorithm: FeedAlgorithm.ReverseChronological }, - { headers: sc.getHeaders(alice) }, - ) - - expect(forSnapshot(aliceTL.data.feed)).toMatchSnapshot() - - // Cleanup - await agent.api.app.bsky.graph.unmuteActor( - { actor: bob }, - { encoding: 'application/json', headers: sc.getHeaders(alice) }, - ) - await agent.api.app.bsky.graph.unmuteActor( - { actor: carol }, - { encoding: 'application/json', headers: sc.getHeaders(alice) }, - ) - }) - - it('paginates reverse-chronological feed', async () => { - const results = (results) => results.flatMap((res) => res.feed) - const paginator = async (cursor?: string) => { - const res = await agent.api.app.bsky.feed.getTimeline( - { - algorithm: FeedAlgorithm.ReverseChronological, - cursor, - limit: 4, - }, - { headers: sc.getHeaders(carol) }, - ) - return res.data - } - - const paginatedAll = await paginateAll(paginator) - paginatedAll.forEach((res) => - expect(res.feed.length).toBeLessThanOrEqual(4), - ) - - const full = await agent.api.app.bsky.feed.getTimeline( - { - algorithm: FeedAlgorithm.ReverseChronological, - }, - { headers: sc.getHeaders(carol) }, - ) - - expect(full.data.feed.length).toEqual(7) - expect(results(paginatedAll)).toEqual(results([full.data])) - }) - - it('reflects self-labels', async () => { - const carolTL = await agent.api.app.bsky.feed.getTimeline( - {}, - { headers: sc.getHeaders(carol) }, - ) - - const alicePost = carolTL.data.feed.find( - ({ post }) => post.uri === sc.posts[alice][0].ref.uriStr, - )?.post - - assert(alicePost, 'post does not exist') - - const postSelfLabels = alicePost.labels - ?.filter((label) => label.src === alice) - .map((label) => label.val) - - expect(postSelfLabels).toEqual(['self-label']) - - const authorSelfLabels = alicePost.author.labels - ?.filter((label) => label.src === alice) - .map((label) => label.val) - .sort() - - expect(authorSelfLabels).toEqual(['self-label-a', 'self-label-b']) - }) - - it('blocks posts, reposts, replies by actor takedown', async () => { - const actionResults = await Promise.all( - [bob, carol].map((did) => - agent.api.com.atproto.admin.takeModerationAction( - { - action: TAKEDOWN, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ), - ), - ) - - const aliceTL = await agent.api.app.bsky.feed.getTimeline( - { algorithm: FeedAlgorithm.ReverseChronological }, - { headers: sc.getHeaders(alice) }, - ) - - expect(forSnapshot(aliceTL.data.feed)).toMatchSnapshot() - - // Cleanup - await Promise.all( - actionResults.map((result) => - agent.api.com.atproto.admin.reverseModerationAction( - { - id: result.data.id, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ), - ), - ) - }) - - it('blocks posts, reposts, replies by record takedown.', async () => { - const postRef1 = sc.posts[dan][1].ref // Repost - const postRef2 = sc.replies[bob][0].ref // Post and reply parent - const actionResults = await Promise.all( - [postRef1, postRef2].map((postRef) => - agent.api.com.atproto.admin.takeModerationAction( - { - action: TAKEDOWN, - subject: { - $type: 'com.atproto.repo.strongRef', - uri: postRef.uriStr, - cid: postRef.cidStr, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ), - ), - ) - - const aliceTL = await agent.api.app.bsky.feed.getTimeline( - { algorithm: FeedAlgorithm.ReverseChronological }, - { headers: sc.getHeaders(alice) }, - ) - - expect(forSnapshot(aliceTL.data.feed)).toMatchSnapshot() - - // Cleanup - await Promise.all( - actionResults.map((result) => - agent.api.com.atproto.admin.reverseModerationAction( - { - id: result.data.id, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ), - ), - ) - }) -}) diff --git a/services/pds/index.js b/services/pds/index.js index 5c4d8e924a7..4be4833e942 100644 --- a/services/pds/index.js +++ b/services/pds/index.js @@ -12,17 +12,12 @@ require('dd-trace') // Only works with commonjs // Tracer code above must come before anything else const path = require('path') -const { - KmsKeypair, - S3BlobStore, - CloudfrontInvalidator, -} = require('@atproto/aws') +const { KmsKeypair, S3BlobStore } = require('@atproto/aws') const { Database, ServerConfig, PDS, ViewMaintainer, - makeAlgos, PeriodicModerationActionReversal, } = require('@atproto/pds') const { Secp256k1Keypair } = require('@atproto/crypto') @@ -68,19 +63,12 @@ const main = async () => { password: env.smtpPassword, }), }) - const cfInvalidator = new CloudfrontInvalidator({ - distributionId: env.cfDistributionId, - pathPrefix: cfg.imgUriEndpoint && new URL(cfg.imgUriEndpoint).pathname, - }) - const algos = env.feedPublisherDid ? makeAlgos(env.feedPublisherDid) : {} const pds = PDS.create({ db, blobstore: s3Blobstore, repoSigningKey, plcRotationKey, config: cfg, - imgInvalidator: cfInvalidator, - algos, }) const viewMaintainer = new ViewMaintainer(migrateDb) const viewMaintainerRunning = viewMaintainer.run() @@ -151,8 +139,6 @@ const getEnv = () => ({ smtpUsername: process.env.SMTP_USERNAME, smtpPassword: process.env.SMTP_PASSWORD, s3Bucket: process.env.S3_BUCKET_NAME, - cfDistributionId: process.env.CF_DISTRIBUTION_ID, - feedPublisherDid: process.env.FEED_PUBLISHER_DID, }) const maintainXrpcResource = (span, req) => { From 4d60322dacc56f48337610fd9409cc856e85531b Mon Sep 17 00:00:00 2001 From: bnewbold Date: Fri, 22 Sep 2023 19:08:52 -0700 Subject: [PATCH 32/46] auto-moderator tweaks: pass along record URI, create report for takedown action (#1643) * auto-moderator: include record URI in abyss requests * auto-moderator: log attempt at hard takedown; create report as well The motivation is to flag the event to mod team, and to make it easier to confirm that takedown took place. * auto-mod: typo fix * auto-mod: bugfixes * bsky: always create auto-mod report locally, not pushAgent (if possible) * bsky: fix auto-mod build * bsky: URL-encode scanBlob call --- packages/bsky/src/auto-moderator/abyss.ts | 32 ++++++++----- packages/bsky/src/auto-moderator/index.ts | 45 ++++++++++++++++--- .../tests/auto-moderator/takedowns.test.ts | 3 +- 3 files changed, 62 insertions(+), 18 deletions(-) diff --git a/packages/bsky/src/auto-moderator/abyss.ts b/packages/bsky/src/auto-moderator/abyss.ts index fb9ee2c4e98..4799c7067a5 100644 --- a/packages/bsky/src/auto-moderator/abyss.ts +++ b/packages/bsky/src/auto-moderator/abyss.ts @@ -1,5 +1,6 @@ import axios from 'axios' import { CID } from 'multiformats/cid' +import { AtUri } from '@atproto/syntax' import * as ui8 from 'uint8arrays' import { resolveBlob } from '../api/blob-resolver' import { retryHttp } from '../util/retry' @@ -8,7 +9,7 @@ import { IdResolver } from '@atproto/identity' import { labelerLogger as log } from '../logger' export interface ImageFlagger { - scanImage(did: string, cid: CID): Promise + scanImage(did: string, cid: CID, uri: AtUri): Promise } export class Abyss implements ImageFlagger { @@ -22,11 +23,11 @@ export class Abyss implements ImageFlagger { this.auth = basicAuth(this.password) } - async scanImage(did: string, cid: CID): Promise { + async scanImage(did: string, cid: CID, uri: AtUri): Promise { const start = Date.now() const res = await retryHttp(async () => { try { - return await this.makeReq(did, cid) + return await this.makeReq(did, cid, uri) } catch (err) { log.warn({ err, did, cid: cid.toString() }, 'abyss request failed') throw err @@ -39,20 +40,24 @@ export class Abyss implements ImageFlagger { return this.parseRes(res) } - async makeReq(did: string, cid: CID): Promise { + async makeReq(did: string, cid: CID, uri: AtUri): Promise { const { stream, contentType } = await resolveBlob( did, cid, this.ctx.db, this.ctx.idResolver, ) - const { data } = await axios.post(this.getReqUrl({ did }), stream, { - headers: { - 'Content-Type': contentType, - authorization: this.auth, + const { data } = await axios.post( + this.getReqUrl({ did, uri: uri.toString() }), + stream, + { + headers: { + 'Content-Type': contentType, + authorization: this.auth, + }, + timeout: 10000, }, - timeout: 10000, - }) + ) return data } @@ -69,8 +74,11 @@ export class Abyss implements ImageFlagger { return labels } - getReqUrl(params: { did: string }) { - return `${this.endpoint}/xrpc/com.atproto.unspecced.scanBlob?did=${params.did}` + getReqUrl(params: { did: string; uri: string }) { + const search = new URLSearchParams(params) + return `${ + this.endpoint + }/xrpc/com.atproto.unspecced.scanBlob?${search.toString()}` } } diff --git a/packages/bsky/src/auto-moderator/index.ts b/packages/bsky/src/auto-moderator/index.ts index 85cc529bce1..1be099759f1 100644 --- a/packages/bsky/src/auto-moderator/index.ts +++ b/packages/bsky/src/auto-moderator/index.ts @@ -18,7 +18,10 @@ import { ImageUriBuilder } from '../image/uri' import { ImageInvalidator } from '../image/invalidator' import { Abyss } from './abyss' import { FuzzyMatcher, TextFlagger } from './fuzzy-matcher' -import { REASONOTHER } from '../lexicon/types/com/atproto/moderation/defs' +import { + REASONOTHER, + REASONVIOLATION, +} from '../lexicon/types/com/atproto/moderation/defs' export class AutoModerator { public pushAgent?: AtpAgent @@ -172,7 +175,7 @@ export class AutoModerator { async checkImgForTakedown(uri: AtUri, recordCid: CID, imgCids: CID[]) { if (imgCids.length < 0) return const results = await Promise.all( - imgCids.map((cid) => this.imageFlagger?.scanImage(uri.host, cid)), + imgCids.map((cid) => this.imageFlagger?.scanImage(uri.host, cid, uri)), ) const takedownCids: CID[] = [] for (let i = 0; i < results.length; i++) { @@ -207,7 +210,39 @@ export class AutoModerator { takedownCids: CID[], labels: string[], ) { - const reason = `automated takedown for labels: ${labels.join(', ')}` + const reportReason = `automated takedown (${labels.join( + ', ', + )}). account needs review and possibly additional action` + const takedownReason = `automated takedown for labels: ${labels.join(', ')}` + log.warn( + { + uri: uri.toString(), + blobCids: takedownCids, + labels, + }, + 'hard takedown of record (and blobs) based on auto-matching', + ) + + if (this.services.moderation) { + await this.ctx.db.transaction(async (dbTxn) => { + // directly/locally create report, even if we use pushAgent for the takedown. don't have acctual account credentials for pushAgent, only admin auth + if (!this.services.moderation) { + // checked above, outside the transaction + return + } + const modSrvc = this.services.moderation(dbTxn) + await modSrvc.report({ + reportedBy: this.ctx.cfg.labelerDid, + reasonType: REASONVIOLATION, + subject: { + uri: uri, + cid: recordCid, + }, + reason: reportReason, + }) + }) + } + if (this.pushAgent) { await this.pushAgent.com.atproto.admin.takeModerationAction({ action: 'com.atproto.admin.defs#takedown', @@ -217,7 +252,7 @@ export class AutoModerator { cid: recordCid.toString(), }, subjectBlobCids: takedownCids.map((c) => c.toString()), - reason, + reason: takedownReason, createdBy: this.ctx.cfg.labelerDid, }) } else { @@ -230,7 +265,7 @@ export class AutoModerator { action: 'com.atproto.admin.defs#takedown', subject: { uri, cid: recordCid }, subjectBlobCids: takedownCids, - reason, + reason: takedownReason, createdBy: this.ctx.cfg.labelerDid, }) await modSrvc.takedownRecord({ diff --git a/packages/bsky/tests/auto-moderator/takedowns.test.ts b/packages/bsky/tests/auto-moderator/takedowns.test.ts index 27b0c986115..32c5c941642 100644 --- a/packages/bsky/tests/auto-moderator/takedowns.test.ts +++ b/packages/bsky/tests/auto-moderator/takedowns.test.ts @@ -7,6 +7,7 @@ import { TestNetwork } from '@atproto/dev-env' import { ImageRef, SeedClient } from '../seeds/client' import usersSeed from '../seeds/users' import { CID } from 'multiformats/cid' +import { AtUri } from '@atproto/syntax' import { ImageFlagger } from '../../src/auto-moderator/abyss' import { ImageInvalidator } from '../../src/image/invalidator' import { sha256 } from '@atproto/crypto' @@ -157,7 +158,7 @@ class TestInvalidator implements ImageInvalidator { } class TestFlagger implements ImageFlagger { - async scanImage(_did: string, cid: CID): Promise { + async scanImage(_did: string, cid: CID, _uri: AtUri): Promise { if (cid.equals(badCid1)) { return ['kill-it'] } else if (cid.equals(badCid2)) { From d703bc36e6637750ec20df4c8d0c015c7bbd04aa Mon Sep 17 00:00:00 2001 From: devin ivy Date: Mon, 25 Sep 2023 10:27:48 +0200 Subject: [PATCH 33/46] Clear follow viewer state when blocking (#1659) * clear follow viewer state when blocking * tidy --- packages/bsky/src/services/actor/views.ts | 20 +++++++++++++---- .../__snapshots__/block-lists.test.ts.snap | 1 - packages/bsky/tests/views/blocks.test.ts | 22 +++++++++++++++++-- .../tests/views/suggested-follows.test.ts | 2 +- .../proxied/__snapshots__/views.test.ts.snap | 10 +++------ 5 files changed, 40 insertions(+), 15 deletions(-) diff --git a/packages/bsky/src/services/actor/views.ts b/packages/bsky/src/services/actor/views.ts index 80652599f80..ec39805c76d 100644 --- a/packages/bsky/src/services/actor/views.ts +++ b/packages/bsky/src/services/actor/views.ts @@ -194,8 +194,14 @@ export class ActorViews { mutedByList, blockedBy: !!bam.blockedBy([viewer, did]), blocking: bam.blocking([viewer, did]) ?? undefined, - following: prof?.viewerFollowing || undefined, - followedBy: prof?.viewerFollowedBy || undefined, + following: + prof?.viewerFollowing && !bam.block([viewer, did]) + ? prof.viewerFollowing + : undefined, + followedBy: + prof?.viewerFollowedBy && !bam.block([viewer, did]) + ? prof.viewerFollowedBy + : undefined, } : undefined, labels: [...actorLabels, ...selfLabels], @@ -314,8 +320,14 @@ export class ActorViews { mutedByList, blockedBy: !!bam.blockedBy([viewer, did]), blocking: bam.blocking([viewer, did]) ?? undefined, - following: prof?.viewerFollowing || undefined, - followedBy: prof?.viewerFollowedBy || undefined, + following: + prof?.viewerFollowing && !bam.block([viewer, did]) + ? prof.viewerFollowing + : undefined, + followedBy: + prof?.viewerFollowedBy && !bam.block([viewer, did]) + ? prof.viewerFollowedBy + : undefined, } : undefined, labels: [...actorLabels, ...selfLabels], diff --git a/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap b/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap index 009095947c2..364ad4b7d63 100644 --- a/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap @@ -512,7 +512,6 @@ Object { "viewer": Object { "blockedBy": false, "blocking": "record(0)", - "following": "record(4)", "muted": false, }, }, diff --git a/packages/bsky/tests/views/blocks.test.ts b/packages/bsky/tests/views/blocks.test.ts index 30b46e78ab5..0109b93f82a 100644 --- a/packages/bsky/tests/views/blocks.test.ts +++ b/packages/bsky/tests/views/blocks.test.ts @@ -191,17 +191,35 @@ describe('pds views with blocking', () => { { actor: dan }, { headers: await network.serviceHeaders(carol) }, ) - expect(resCarol.data.viewer?.blocking).toBeUndefined + expect(resCarol.data.viewer?.blocking).toBeUndefined() expect(resCarol.data.viewer?.blockedBy).toBe(true) const resDan = await agent.api.app.bsky.actor.getProfile( { actor: carol }, { headers: await network.serviceHeaders(dan) }, ) - expect(resDan.data.viewer?.blocking).toBeDefined + expect(resDan.data.viewer?.blocking).toBeDefined() expect(resDan.data.viewer?.blockedBy).toBe(false) }) + it('unsets viewer follow state when blocked', async () => { + // there are follows between carol and dan + const { data: profile } = await agent.api.app.bsky.actor.getProfile( + { actor: carol }, + { headers: await network.serviceHeaders(dan) }, + ) + expect(profile.viewer?.following).toBeUndefined() + expect(profile.viewer?.followedBy).toBeUndefined() + const { data: result } = await agent.api.app.bsky.graph.getBlocks( + {}, + { headers: await network.serviceHeaders(dan) }, + ) + const blocked = result.blocks.find((block) => block.did === carol) + expect(blocked).toBeDefined() + expect(blocked?.viewer?.following).toBeUndefined() + expect(blocked?.viewer?.followedBy).toBeUndefined() + }) + it('returns block status on getProfiles', async () => { const resCarol = await agent.api.app.bsky.actor.getProfiles( { actors: [alice, dan] }, diff --git a/packages/bsky/tests/views/suggested-follows.test.ts b/packages/bsky/tests/views/suggested-follows.test.ts index 6a2f3ebe1d7..1d8cb5a91ba 100644 --- a/packages/bsky/tests/views/suggested-follows.test.ts +++ b/packages/bsky/tests/views/suggested-follows.test.ts @@ -11,7 +11,7 @@ describe('suggested follows', () => { beforeAll(async () => { network = await TestNetwork.create({ - dbPostgresSchema: 'bsky_views_suggestions', + dbPostgresSchema: 'bsky_views_suggested_follows', }) agent = network.bsky.getClient() pdsAgent = network.pds.getClient() diff --git a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap index d6707292de4..994589ff0b2 100644 --- a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap @@ -2959,8 +2959,6 @@ Object { "viewer": Object { "blockedBy": false, "blocking": "record(0)", - "followedBy": "record(2)", - "following": "record(1)", "muted": false, }, }, @@ -2977,7 +2975,7 @@ Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(1)", - "uri": "record(6)", + "uri": "record(2)", "val": "self-label-a", }, Object { @@ -2985,15 +2983,13 @@ Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(1)", - "uri": "record(6)", + "uri": "record(2)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "blocking": "record(3)", - "followedBy": "record(5)", - "following": "record(4)", + "blocking": "record(1)", "muted": false, }, }, From d96f7d9b84c6fbab9711059c8584a77d892dcedd Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 25 Sep 2023 11:14:57 -0500 Subject: [PATCH 34/46] add `tags` to posts (#1637) * add tags to post lex * kiss * add richtext facet and validation attrs * add tag validation attrs to post * codegen * add maxLength for tags, add description * validate post tags on write * add test * handle tags in indexer * add tags to postView, codegen * return tags on post thread view * format * revert formatting change to docs * use establish validation pattern * add changeset (cherry picked from commit fcb6fe7c26144662f791c7900afcd84c7bf1be6b) * remove tags from postView, codegen * remove tags from thread view * revert unused changes --- .changeset/three-snakes-turn.md | 7 +++ lexicons/app/bsky/feed/post.json | 6 +++ lexicons/app/bsky/richtext/facet.json | 10 ++++- packages/api/src/client/lexicons.ts | 23 ++++++++++ .../src/client/types/app/bsky/feed/post.ts | 2 + .../client/types/app/bsky/richtext/facet.ts | 18 +++++++- .../20230920T213858047Z-add-tags-to-post.ts | 9 ++++ packages/bsky/src/db/migrations/index.ts | 1 + packages/bsky/src/db/tables/post.ts | 1 + packages/bsky/src/lexicon/lexicons.ts | 23 ++++++++++ .../src/lexicon/types/app/bsky/feed/post.ts | 2 + .../lexicon/types/app/bsky/richtext/facet.ts | 18 +++++++- packages/bsky/src/services/feed/index.ts | 1 + .../src/services/indexing/plugins/post.ts | 3 ++ packages/bsky/tests/views/posts.test.ts | 28 +++++++++++- packages/pds/src/lexicon/lexicons.ts | 23 ++++++++++ .../src/lexicon/types/app/bsky/feed/post.ts | 2 + .../lexicon/types/app/bsky/richtext/facet.ts | 18 +++++++- packages/pds/src/repo/prepare.ts | 2 + packages/pds/tests/create-post.test.ts | 45 +++++++++++++++++++ 20 files changed, 236 insertions(+), 6 deletions(-) create mode 100644 .changeset/three-snakes-turn.md create mode 100644 packages/bsky/src/db/migrations/20230920T213858047Z-add-tags-to-post.ts create mode 100644 packages/pds/tests/create-post.test.ts diff --git a/.changeset/three-snakes-turn.md b/.changeset/three-snakes-turn.md new file mode 100644 index 00000000000..cd086115761 --- /dev/null +++ b/.changeset/three-snakes-turn.md @@ -0,0 +1,7 @@ +--- +'@atproto/bsky': patch +'@atproto/api': patch +'@atproto/pds': patch +--- + +Introduce general support for tags on posts diff --git a/lexicons/app/bsky/feed/post.json b/lexicons/app/bsky/feed/post.json index 5622b5cfd50..b21f01b6050 100644 --- a/lexicons/app/bsky/feed/post.json +++ b/lexicons/app/bsky/feed/post.json @@ -38,6 +38,12 @@ "type": "union", "refs": ["com.atproto.label.defs#selfLabels"] }, + "tags": { + "type": "array", + "maxLength": 8, + "items": { "type": "string", "maxLength": 640, "maxGraphemes": 64 }, + "description": "Additional non-inline tags describing this post." + }, "createdAt": { "type": "string", "format": "datetime" } } } diff --git a/lexicons/app/bsky/richtext/facet.json b/lexicons/app/bsky/richtext/facet.json index 9addf2f34b7..ea8f2cba288 100644 --- a/lexicons/app/bsky/richtext/facet.json +++ b/lexicons/app/bsky/richtext/facet.json @@ -9,7 +9,7 @@ "index": { "type": "ref", "ref": "#byteSlice" }, "features": { "type": "array", - "items": { "type": "union", "refs": ["#mention", "#link"] } + "items": { "type": "union", "refs": ["#mention", "#link", "#tag"] } } } }, @@ -29,6 +29,14 @@ "uri": { "type": "string", "format": "uri" } } }, + "tag": { + "type": "object", + "description": "A hashtag.", + "required": ["tag"], + "properties": { + "tag": { "type": "string", "maxLength": 640, "maxGraphemes": 64 } + } + }, "byteSlice": { "type": "object", "description": "A text segment. Start is inclusive, end is exclusive. Indices are for utf8-encoded strings.", diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 6b9b7b7f14f..a5cbf08d608 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -5622,6 +5622,16 @@ export const schemaDict = { type: 'union', refs: ['lex:com.atproto.label.defs#selfLabels'], }, + tags: { + type: 'array', + maxLength: 8, + items: { + type: 'string', + maxLength: 640, + maxGraphemes: 64, + }, + description: 'Additional non-inline tags describing this post.', + }, createdAt: { type: 'string', format: 'datetime', @@ -6761,6 +6771,7 @@ export const schemaDict = { refs: [ 'lex:app.bsky.richtext.facet#mention', 'lex:app.bsky.richtext.facet#link', + 'lex:app.bsky.richtext.facet#tag', ], }, }, @@ -6788,6 +6799,18 @@ export const schemaDict = { }, }, }, + tag: { + type: 'object', + description: 'A hashtag.', + required: ['tag'], + properties: { + tag: { + type: 'string', + maxLength: 640, + maxGraphemes: 64, + }, + }, + }, byteSlice: { type: 'object', description: diff --git a/packages/api/src/client/types/app/bsky/feed/post.ts b/packages/api/src/client/types/app/bsky/feed/post.ts index 1e326692640..a3299e19035 100644 --- a/packages/api/src/client/types/app/bsky/feed/post.ts +++ b/packages/api/src/client/types/app/bsky/feed/post.ts @@ -29,6 +29,8 @@ export interface Record { labels?: | ComAtprotoLabelDefs.SelfLabels | { $type: string; [k: string]: unknown } + /** Additional non-inline tags describing this post. */ + tags?: string[] createdAt: string [k: string]: unknown } diff --git a/packages/api/src/client/types/app/bsky/richtext/facet.ts b/packages/api/src/client/types/app/bsky/richtext/facet.ts index cea86685a0f..96573bb06fe 100644 --- a/packages/api/src/client/types/app/bsky/richtext/facet.ts +++ b/packages/api/src/client/types/app/bsky/richtext/facet.ts @@ -8,7 +8,7 @@ import { CID } from 'multiformats/cid' export interface Main { index: ByteSlice - features: (Mention | Link | { $type: string; [k: string]: unknown })[] + features: (Mention | Link | Tag | { $type: string; [k: string]: unknown })[] [k: string]: unknown } @@ -61,6 +61,22 @@ export function validateLink(v: unknown): ValidationResult { return lexicons.validate('app.bsky.richtext.facet#link', v) } +/** A hashtag. */ +export interface Tag { + tag: string + [k: string]: unknown +} + +export function isTag(v: unknown): v is Tag { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.richtext.facet#tag' + ) +} + +export function validateTag(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.richtext.facet#tag', v) +} + /** A text segment. Start is inclusive, end is exclusive. Indices are for utf8-encoded strings. */ export interface ByteSlice { byteStart: number diff --git a/packages/bsky/src/db/migrations/20230920T213858047Z-add-tags-to-post.ts b/packages/bsky/src/db/migrations/20230920T213858047Z-add-tags-to-post.ts new file mode 100644 index 00000000000..9d4e5bd4cfb --- /dev/null +++ b/packages/bsky/src/db/migrations/20230920T213858047Z-add-tags-to-post.ts @@ -0,0 +1,9 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema.alterTable('post').addColumn('tags', 'jsonb').execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.alterTable('post').dropColumn('tags').execute() +} diff --git a/packages/bsky/src/db/migrations/index.ts b/packages/bsky/src/db/migrations/index.ts index bf18d8dd15b..9e8bfe9cf7f 100644 --- a/packages/bsky/src/db/migrations/index.ts +++ b/packages/bsky/src/db/migrations/index.ts @@ -28,3 +28,4 @@ export * as _20230817T195936007Z from './20230817T195936007Z-native-notification export * as _20230830T205507322Z from './20230830T205507322Z-suggested-feeds' export * as _20230904T211011773Z from './20230904T211011773Z-block-lists' export * as _20230906T222220386Z from './20230906T222220386Z-thread-gating' +export * as _20230920T213858047Z from './20230920T213858047Z-add-tags-to-post' diff --git a/packages/bsky/src/db/tables/post.ts b/packages/bsky/src/db/tables/post.ts index c627efa39e7..6c01b76c8e0 100644 --- a/packages/bsky/src/db/tables/post.ts +++ b/packages/bsky/src/db/tables/post.ts @@ -12,6 +12,7 @@ export interface Post { replyParent: string | null replyParentCid: string | null langs: string[] | null + tags: string[] | null invalidReplyRoot: boolean | null violatesThreadGate: boolean | null createdAt: string diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 6b9b7b7f14f..a5cbf08d608 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -5622,6 +5622,16 @@ export const schemaDict = { type: 'union', refs: ['lex:com.atproto.label.defs#selfLabels'], }, + tags: { + type: 'array', + maxLength: 8, + items: { + type: 'string', + maxLength: 640, + maxGraphemes: 64, + }, + description: 'Additional non-inline tags describing this post.', + }, createdAt: { type: 'string', format: 'datetime', @@ -6761,6 +6771,7 @@ export const schemaDict = { refs: [ 'lex:app.bsky.richtext.facet#mention', 'lex:app.bsky.richtext.facet#link', + 'lex:app.bsky.richtext.facet#tag', ], }, }, @@ -6788,6 +6799,18 @@ export const schemaDict = { }, }, }, + tag: { + type: 'object', + description: 'A hashtag.', + required: ['tag'], + properties: { + tag: { + type: 'string', + maxLength: 640, + maxGraphemes: 64, + }, + }, + }, byteSlice: { type: 'object', description: diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/post.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/post.ts index 8942bc724cd..93870b4452d 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/post.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/post.ts @@ -29,6 +29,8 @@ export interface Record { labels?: | ComAtprotoLabelDefs.SelfLabels | { $type: string; [k: string]: unknown } + /** Additional non-inline tags describing this post. */ + tags?: string[] createdAt: string [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/types/app/bsky/richtext/facet.ts b/packages/bsky/src/lexicon/types/app/bsky/richtext/facet.ts index a7369ee8d57..2c5b2d723a9 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/richtext/facet.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/richtext/facet.ts @@ -8,7 +8,7 @@ import { CID } from 'multiformats/cid' export interface Main { index: ByteSlice - features: (Mention | Link | { $type: string; [k: string]: unknown })[] + features: (Mention | Link | Tag | { $type: string; [k: string]: unknown })[] [k: string]: unknown } @@ -61,6 +61,22 @@ export function validateLink(v: unknown): ValidationResult { return lexicons.validate('app.bsky.richtext.facet#link', v) } +/** A hashtag. */ +export interface Tag { + tag: string + [k: string]: unknown +} + +export function isTag(v: unknown): v is Tag { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.richtext.facet#tag' + ) +} + +export function validateTag(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.richtext.facet#tag', v) +} + /** A text segment. Start is inclusive, end is exclusive. Indices are for utf8-encoded strings. */ export interface ByteSlice { byteStart: number diff --git a/packages/bsky/src/services/feed/index.ts b/packages/bsky/src/services/feed/index.ts index f955979e81e..dab9673d9db 100644 --- a/packages/bsky/src/services/feed/index.ts +++ b/packages/bsky/src/services/feed/index.ts @@ -147,6 +147,7 @@ export class FeedService { 'post_agg.likeCount as likeCount', 'post_agg.repostCount as repostCount', 'post_agg.replyCount as replyCount', + 'post.tags as tags', db .selectFrom('repost') .if(!viewer, (q) => q.where(noMatch)) diff --git a/packages/bsky/src/services/indexing/plugins/post.ts b/packages/bsky/src/services/indexing/plugins/post.ts index f57bc10179b..40835348f01 100644 --- a/packages/bsky/src/services/indexing/plugins/post.ts +++ b/packages/bsky/src/services/indexing/plugins/post.ts @@ -76,6 +76,9 @@ const insertFn = async ( langs: obj.langs?.length ? sql`${JSON.stringify(obj.langs)}` // sidesteps kysely's array serialization, which is non-jsonb : null, + tags: obj.tags?.length + ? sql`${JSON.stringify(obj.tags)}` // sidesteps kysely's array serialization, which is non-jsonb + : null, indexedAt: timestamp, } const [insertedPost] = await Promise.all([ diff --git a/packages/bsky/tests/views/posts.test.ts b/packages/bsky/tests/views/posts.test.ts index 99ec565dc59..6fa12a085df 100644 --- a/packages/bsky/tests/views/posts.test.ts +++ b/packages/bsky/tests/views/posts.test.ts @@ -1,4 +1,4 @@ -import AtpAgent from '@atproto/api' +import AtpAgent, { AppBskyFeedPost } from '@atproto/api' import { TestNetwork } from '@atproto/dev-env' import { forSnapshot, stripViewerFromPost } from '../_util' import { SeedClient } from '../seeds/client' @@ -7,6 +7,7 @@ import basicSeed from '../seeds/basic' describe('pds posts views', () => { let network: TestNetwork let agent: AtpAgent + let pdsAgent: AtpAgent let sc: SeedClient beforeAll(async () => { @@ -14,7 +15,7 @@ describe('pds posts views', () => { dbPostgresSchema: 'bsky_views_posts', }) agent = network.bsky.getClient() - const pdsAgent = network.pds.getClient() + pdsAgent = network.pds.getClient() sc = new SeedClient(pdsAgent) await basicSeed(sc) await network.processAll() @@ -83,4 +84,27 @@ describe('pds posts views', () => { ].sort() expect(receivedUris).toEqual(expected) }) + + it('allows for creating posts with tags', async () => { + const post: AppBskyFeedPost.Record = { + text: 'hello world', + tags: ['javascript', 'hehe'], + createdAt: new Date().toISOString(), + } + + const { uri } = await pdsAgent.api.app.bsky.feed.post.create( + { repo: sc.dids.alice }, + post, + sc.getHeaders(sc.dids.alice), + ) + + await network.processAll() + await network.bsky.processAll() + + const { data } = await agent.api.app.bsky.feed.getPosts({ uris: [uri] }) + + expect(data.posts.length).toBe(1) + // @ts-ignore we know it's a post record + expect(data.posts[0].record.tags).toEqual(['javascript', 'hehe']) + }) }) diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 6b9b7b7f14f..a5cbf08d608 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -5622,6 +5622,16 @@ export const schemaDict = { type: 'union', refs: ['lex:com.atproto.label.defs#selfLabels'], }, + tags: { + type: 'array', + maxLength: 8, + items: { + type: 'string', + maxLength: 640, + maxGraphemes: 64, + }, + description: 'Additional non-inline tags describing this post.', + }, createdAt: { type: 'string', format: 'datetime', @@ -6761,6 +6771,7 @@ export const schemaDict = { refs: [ 'lex:app.bsky.richtext.facet#mention', 'lex:app.bsky.richtext.facet#link', + 'lex:app.bsky.richtext.facet#tag', ], }, }, @@ -6788,6 +6799,18 @@ export const schemaDict = { }, }, }, + tag: { + type: 'object', + description: 'A hashtag.', + required: ['tag'], + properties: { + tag: { + type: 'string', + maxLength: 640, + maxGraphemes: 64, + }, + }, + }, byteSlice: { type: 'object', description: diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/post.ts b/packages/pds/src/lexicon/types/app/bsky/feed/post.ts index 8942bc724cd..93870b4452d 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/post.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/post.ts @@ -29,6 +29,8 @@ export interface Record { labels?: | ComAtprotoLabelDefs.SelfLabels | { $type: string; [k: string]: unknown } + /** Additional non-inline tags describing this post. */ + tags?: string[] createdAt: string [k: string]: unknown } diff --git a/packages/pds/src/lexicon/types/app/bsky/richtext/facet.ts b/packages/pds/src/lexicon/types/app/bsky/richtext/facet.ts index a7369ee8d57..2c5b2d723a9 100644 --- a/packages/pds/src/lexicon/types/app/bsky/richtext/facet.ts +++ b/packages/pds/src/lexicon/types/app/bsky/richtext/facet.ts @@ -8,7 +8,7 @@ import { CID } from 'multiformats/cid' export interface Main { index: ByteSlice - features: (Mention | Link | { $type: string; [k: string]: unknown })[] + features: (Mention | Link | Tag | { $type: string; [k: string]: unknown })[] [k: string]: unknown } @@ -61,6 +61,22 @@ export function validateLink(v: unknown): ValidationResult { return lexicons.validate('app.bsky.richtext.facet#link', v) } +/** A hashtag. */ +export interface Tag { + tag: string + [k: string]: unknown +} + +export function isTag(v: unknown): v is Tag { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.richtext.facet#tag' + ) +} + +export function validateTag(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.richtext.facet#tag', v) +} + /** A text segment. Start is inclusive, end is exclusive. Indices are for utf8-encoded strings. */ export interface ByteSlice { byteStart: number diff --git a/packages/pds/src/repo/prepare.ts b/packages/pds/src/repo/prepare.ts index 581701f1f01..2147ef552b6 100644 --- a/packages/pds/src/repo/prepare.ts +++ b/packages/pds/src/repo/prepare.ts @@ -299,6 +299,8 @@ function assertNoExplicitSlurs(rkey: string, record: RepoRecord) { } else if (isFeedGenerator(record)) { toCheck += ' ' + rkey toCheck += ' ' + record.displayName + } else if (isPost(record)) { + toCheck += record.tags?.join(' ') } if (hasExplicitSlur(toCheck)) { throw new InvalidRecordError('Unacceptable slur in record') diff --git a/packages/pds/tests/create-post.test.ts b/packages/pds/tests/create-post.test.ts new file mode 100644 index 00000000000..e2763981fb0 --- /dev/null +++ b/packages/pds/tests/create-post.test.ts @@ -0,0 +1,45 @@ +import AtpAgent, { AppBskyFeedPost, AtUri } from '@atproto/api' +import { runTestServer, TestServerInfo } from './_util' +import { SeedClient } from './seeds/client' +import basicSeed from './seeds/basic' + +describe('pds posts record creation', () => { + let server: TestServerInfo + let agent: AtpAgent + let sc: SeedClient + + beforeAll(async () => { + server = await runTestServer({ + dbPostgresSchema: 'views_posts', + }) + agent = new AtpAgent({ service: server.url }) + sc = new SeedClient(agent) + await basicSeed(sc) + await server.processAll() + }) + + afterAll(async () => { + await server.close() + }) + + it('allows for creating posts with tags', async () => { + const post: AppBskyFeedPost.Record = { + text: 'hello world', + tags: ['javascript', 'hehe'], + createdAt: new Date().toISOString(), + } + + const res = await agent.api.app.bsky.feed.post.create( + { repo: sc.dids.alice }, + post, + sc.getHeaders(sc.dids.alice), + ) + const { value: record } = await agent.api.app.bsky.feed.post.get({ + repo: sc.dids.alice, + rkey: new AtUri(res.uri).rkey, + }) + + expect(record).toBeTruthy() + expect(record.tags).toEqual(['javascript', 'hehe']) + }) +}) From 3ea11a1a79a7f6a54dba2a529f77903d8008a49d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 11:17:11 -0500 Subject: [PATCH 35/46] Version packages (#1664) Co-authored-by: github-actions[bot] --- .changeset/three-snakes-turn.md | 7 ------- packages/api/CHANGELOG.md | 6 ++++++ packages/api/package.json | 2 +- packages/bsky/CHANGELOG.md | 9 +++++++++ packages/bsky/package.json | 2 +- packages/dev-env/CHANGELOG.md | 9 +++++++++ packages/dev-env/package.json | 2 +- packages/pds/CHANGELOG.md | 9 +++++++++ packages/pds/package.json | 2 +- 9 files changed, 37 insertions(+), 11 deletions(-) delete mode 100644 .changeset/three-snakes-turn.md diff --git a/.changeset/three-snakes-turn.md b/.changeset/three-snakes-turn.md deleted file mode 100644 index cd086115761..00000000000 --- a/.changeset/three-snakes-turn.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@atproto/bsky': patch -'@atproto/api': patch -'@atproto/pds': patch ---- - -Introduce general support for tags on posts diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index 79e6807686c..f438c52525d 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -1,5 +1,11 @@ # @atproto/api +## 0.6.17 + +### Patch Changes + +- [#1637](https://github.com/bluesky-social/atproto/pull/1637) [`d96f7d9b`](https://github.com/bluesky-social/atproto/commit/d96f7d9b84c6fbab9711059c8584a77d892dcedd) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Introduce general support for tags on posts + ## 0.6.16 ### Patch Changes diff --git a/packages/api/package.json b/packages/api/package.json index 76cf7f1b8e7..8165fa63dd2 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.6.16", + "version": "0.6.17", "license": "MIT", "description": "Client library for atproto and Bluesky", "keywords": [ diff --git a/packages/bsky/CHANGELOG.md b/packages/bsky/CHANGELOG.md index 8e449ba7cf0..0d973cc3d50 100644 --- a/packages/bsky/CHANGELOG.md +++ b/packages/bsky/CHANGELOG.md @@ -1,5 +1,14 @@ # @atproto/bsky +## 0.0.8 + +### Patch Changes + +- [#1637](https://github.com/bluesky-social/atproto/pull/1637) [`d96f7d9b`](https://github.com/bluesky-social/atproto/commit/d96f7d9b84c6fbab9711059c8584a77d892dcedd) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Introduce general support for tags on posts + +- Updated dependencies [[`d96f7d9b`](https://github.com/bluesky-social/atproto/commit/d96f7d9b84c6fbab9711059c8584a77d892dcedd)]: + - @atproto/api@0.6.17 + ## 0.0.7 ### Patch Changes diff --git a/packages/bsky/package.json b/packages/bsky/package.json index 8195d27b539..ec203970552 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/bsky", - "version": "0.0.7", + "version": "0.0.8", "license": "MIT", "description": "Reference implementation of app.bsky App View (Bluesky API)", "keywords": [ diff --git a/packages/dev-env/CHANGELOG.md b/packages/dev-env/CHANGELOG.md index 38a56abaa99..130a330007b 100644 --- a/packages/dev-env/CHANGELOG.md +++ b/packages/dev-env/CHANGELOG.md @@ -1,5 +1,14 @@ # @atproto/dev-env +## 0.2.8 + +### Patch Changes + +- Updated dependencies [[`d96f7d9b`](https://github.com/bluesky-social/atproto/commit/d96f7d9b84c6fbab9711059c8584a77d892dcedd)]: + - @atproto/bsky@0.0.8 + - @atproto/api@0.6.17 + - @atproto/pds@0.1.17 + ## 0.2.7 ### Patch Changes diff --git a/packages/dev-env/package.json b/packages/dev-env/package.json index e688319dcb1..d945103437e 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/dev-env", - "version": "0.2.7", + "version": "0.2.8", "license": "MIT", "description": "Local development environment helper for atproto development", "keywords": [ diff --git a/packages/pds/CHANGELOG.md b/packages/pds/CHANGELOG.md index 17f13cf1c85..3a450244149 100644 --- a/packages/pds/CHANGELOG.md +++ b/packages/pds/CHANGELOG.md @@ -1,5 +1,14 @@ # @atproto/pds +## 0.1.17 + +### Patch Changes + +- [#1637](https://github.com/bluesky-social/atproto/pull/1637) [`d96f7d9b`](https://github.com/bluesky-social/atproto/commit/d96f7d9b84c6fbab9711059c8584a77d892dcedd) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Introduce general support for tags on posts + +- Updated dependencies [[`d96f7d9b`](https://github.com/bluesky-social/atproto/commit/d96f7d9b84c6fbab9711059c8584a77d892dcedd)]: + - @atproto/api@0.6.17 + ## 0.1.16 ### Patch Changes diff --git a/packages/pds/package.json b/packages/pds/package.json index e227bd4c4c9..de690dad916 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/pds", - "version": "0.1.16", + "version": "0.1.17", "license": "MIT", "description": "Reference implementation of atproto Personal Data Server (PDS)", "keywords": [ From caef7c183206b2b3384e3fe2dbee6bc953c27d04 Mon Sep 17 00:00:00 2001 From: bnewbold Date: Mon, 25 Sep 2023 11:18:34 -0700 Subject: [PATCH 36/46] Makefile: run code formatting after codegen (#1660) --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index f8c36ce2bb0..f9e90a86a44 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,8 @@ codegen: ## Re-generate packages from lexicon/ files cd packages/api; pnpm run codegen cd packages/pds; pnpm run codegen cd packages/bsky; pnpm run codegen + # clean up codegen output + pnpm format .PHONY: lint lint: ## Run style checks and verify syntax From 11bf4d302e8ac9a5c5d627268433b44ecf2870b6 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 25 Sep 2023 14:17:44 -0500 Subject: [PATCH 37/46] Reverse order of blocks from sync.getRepo (#1665) * reverse order of blocks from sync.getRepo * write to car while fetching next page --- packages/pds/src/sql-repo-storage.ts | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/pds/src/sql-repo-storage.ts b/packages/pds/src/sql-repo-storage.ts index a7b6a5ae1ea..7522e325bfa 100644 --- a/packages/pds/src/sql-repo-storage.ts +++ b/packages/pds/src/sql-repo-storage.ts @@ -213,8 +213,22 @@ export class SqlRepoStorage extends ReadableBlockstore implements RepoStorage { } return writeCarStream(root, async (car) => { let cursor: RevCursor | undefined = undefined + const writeRows = async ( + rows: { cid: string; content: Uint8Array }[], + ) => { + for (const row of rows) { + await car.put({ + cid: CID.parse(row.cid), + bytes: row.content, + }) + } + } + // allow us to write to car while fetching the next page + let writePromise: Promise = Promise.resolve() do { const res = await this.getBlockRange(since, cursor) + await writePromise + writePromise = writeRows(res) for (const row of res) { await car.put({ cid: CID.parse(row.cid), @@ -231,6 +245,8 @@ export class SqlRepoStorage extends ReadableBlockstore implements RepoStorage { cursor = undefined } } while (cursor) + // ensure we flush the last page of blocks + await writePromise }) } @@ -240,17 +256,18 @@ export class SqlRepoStorage extends ReadableBlockstore implements RepoStorage { .selectFrom('ipld_block') .where('creator', '=', this.did) .select(['cid', 'repoRev', 'content']) - .orderBy('repoRev', 'asc') - .orderBy('cid', 'asc') + .orderBy('repoRev', 'desc') + .orderBy('cid', 'desc') .limit(500) if (cursor) { // use this syntax to ensure we hit the index builder = builder.where( - sql`((${ref('repoRev')}, ${ref('cid')}) > (${ + sql`((${ref('repoRev')}, ${ref('cid')}) < (${ cursor.rev }, ${cursor.cid.toString()}))`, ) - } else if (since) { + } + if (since) { builder = builder.where('repoRev', '>', since) } return builder.execute() From 2ce8a11b8daf5d39027488c5dde8c47b0eb937bf Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 25 Sep 2023 17:45:57 -0500 Subject: [PATCH 38/46] Add hashtag detection to richtext (#1651) * add tag detection to richtext * fix duplicate tag index error * add utils * fix leading space index failures, test for them * add changeset --- .changeset/moody-wombats-live.md | 5 + packages/api/src/rich-text/detection.ts | 27 +++++ packages/api/src/rich-text/rich-text.ts | 13 +++ .../api/tests/rich-text-detection.test.ts | 104 ++++++++++++++++++ 4 files changed, 149 insertions(+) create mode 100644 .changeset/moody-wombats-live.md diff --git a/.changeset/moody-wombats-live.md b/.changeset/moody-wombats-live.md new file mode 100644 index 00000000000..c146f008d5c --- /dev/null +++ b/.changeset/moody-wombats-live.md @@ -0,0 +1,5 @@ +--- +'@atproto/api': patch +--- + +Adds support for hashtags in the `RichText.detectFacets` method. diff --git a/packages/api/src/rich-text/detection.ts b/packages/api/src/rich-text/detection.ts index 910804ca0db..503866d7df8 100644 --- a/packages/api/src/rich-text/detection.ts +++ b/packages/api/src/rich-text/detection.ts @@ -69,6 +69,33 @@ export function detectFacets(text: UnicodeString): Facet[] | undefined { }) } } + { + const re = /(?:^|\s)(#[^\d\s]\S*)(?=\s)?/g + while ((match = re.exec(text.utf16))) { + let [tag] = match + const hasLeadingSpace = /^\s/.test(tag) + + tag = tag.trim().replace(/\p{P}+$/gu, '') // strip ending punctuation + + // inclusive of #, max of 64 chars + if (tag.length > 66) continue + + const index = match.index + (hasLeadingSpace ? 1 : 0) + + facets.push({ + index: { + byteStart: text.utf16IndexToUtf8Index(index), + byteEnd: text.utf16IndexToUtf8Index(index + tag.length), // inclusive of last char + }, + features: [ + { + $type: 'app.bsky.richtext.facet#tag', + tag, + }, + ], + }) + } + } return facets.length > 0 ? facets : undefined } diff --git a/packages/api/src/rich-text/rich-text.ts b/packages/api/src/rich-text/rich-text.ts index 46ccc7dfef1..4c041b8bb5f 100644 --- a/packages/api/src/rich-text/rich-text.ts +++ b/packages/api/src/rich-text/rich-text.ts @@ -100,6 +100,7 @@ import { detectFacets } from './detection' export type Facet = AppBskyRichtextFacet.Main export type FacetLink = AppBskyRichtextFacet.Link export type FacetMention = AppBskyRichtextFacet.Mention +export type FacetTag = AppBskyRichtextFacet.Tag export type Entity = AppBskyFeedPost.Entity export interface RichTextProps { @@ -141,6 +142,18 @@ export class RichTextSegment { isMention() { return !!this.mention } + + get tag(): FacetTag | undefined { + const tag = this.facet?.features.find(AppBskyRichtextFacet.isTag) + if (AppBskyRichtextFacet.isTag(tag)) { + return tag + } + return undefined + } + + isTag() { + return !!this.tag + } } export class RichText { diff --git a/packages/api/tests/rich-text-detection.test.ts b/packages/api/tests/rich-text-detection.test.ts index da81fe415b1..df2aed84889 100644 --- a/packages/api/tests/rich-text-detection.test.ts +++ b/packages/api/tests/rich-text-detection.test.ts @@ -1,4 +1,5 @@ import { AtpAgent, RichText, RichTextSegment } from '../src' +import { isTag } from '../src/client/types/app/bsky/richtext/facet' describe('detectFacets', () => { const agent = new AtpAgent({ service: 'http://localhost' }) @@ -208,6 +209,109 @@ describe('detectFacets', () => { expect(Array.from(rt.segments(), segmentToOutput)).toEqual(outputs[i]) } }) + + it('correctly detects tags inline', async () => { + const inputs: [ + string, + string[], + { byteStart: number; byteEnd: number }[], + ][] = [ + ['#a', ['#a'], [{ byteStart: 0, byteEnd: 2 }]], + [ + '#a #b', + ['#a', '#b'], + [ + { byteStart: 0, byteEnd: 2 }, + { byteStart: 3, byteEnd: 5 }, + ], + ], + ['#1', [], []], + ['#tag', ['#tag'], [{ byteStart: 0, byteEnd: 4 }]], + ['body #tag', ['#tag'], [{ byteStart: 5, byteEnd: 9 }]], + ['#tag body', ['#tag'], [{ byteStart: 0, byteEnd: 4 }]], + ['body #tag body', ['#tag'], [{ byteStart: 5, byteEnd: 9 }]], + ['body #1', [], []], + ['body #a1', ['#a1'], [{ byteStart: 5, byteEnd: 8 }]], + ['#', [], []], + ['text #', [], []], + ['text # text', [], []], + [ + 'body #thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + ['#thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'], + [{ byteStart: 5, byteEnd: 71 }], + ], + [ + 'body #thisisa65characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab', + [], + [], + ], + [ + 'its a #double#rainbow', + ['#double#rainbow'], + [{ byteStart: 6, byteEnd: 21 }], + ], + ['##hashash', ['##hashash'], [{ byteStart: 0, byteEnd: 9 }]], + ['some #n0n3s@n5e!', ['#n0n3s@n5e'], [{ byteStart: 5, byteEnd: 15 }]], + [ + 'works #with,punctuation', + ['#with,punctuation'], + [{ byteStart: 6, byteEnd: 23 }], + ], + [ + 'strips trailing #punctuation, #like. #this!', + ['#punctuation', '#like', '#this'], + [ + { byteStart: 16, byteEnd: 28 }, + { byteStart: 30, byteEnd: 35 }, + { byteStart: 37, byteEnd: 42 }, + ], + ], + [ + 'strips #multi_trailing___...', + ['#multi_trailing'], + [{ byteStart: 7, byteEnd: 22 }], + ], + [ + 'works with #🦋 emoji, and #butter🦋fly', + ['#🦋', '#butter🦋fly'], + [ + { byteStart: 11, byteEnd: 16 }, + { byteStart: 28, byteEnd: 42 }, + ], + ], + [ + '#same #same #but #diff', + ['#same', '#same', '#but', '#diff'], + [ + { byteStart: 0, byteEnd: 5 }, + { byteStart: 6, byteEnd: 11 }, + { byteStart: 12, byteEnd: 16 }, + { byteStart: 17, byteEnd: 22 }, + ], + ], + ] + + for (const [input, tags, indices] of inputs) { + const rt = new RichText({ text: input }) + await rt.detectFacets(agent) + + let detectedTags: string[] = [] + let detectedIndices: { byteStart: number; byteEnd: number }[] = [] + + for (const { facet } of rt.segments()) { + if (!facet) continue + for (const feature of facet.features) { + if (isTag(feature)) { + detectedTags.push(feature.tag) + } + } + detectedIndices.push(facet.index) + } + + expect(detectedTags).toEqual(tags) + expect(detectedIndices).toEqual(indices) + } + }) }) function segmentToOutput(segment: RichTextSegment): string[] { From 4a64944783e9b2a530df2288833af520bc89412f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 17:48:05 -0500 Subject: [PATCH 39/46] Version packages (#1669) Co-authored-by: github-actions[bot] --- .changeset/moody-wombats-live.md | 5 ----- packages/api/CHANGELOG.md | 6 ++++++ packages/api/package.json | 2 +- packages/bsky/CHANGELOG.md | 7 +++++++ packages/bsky/package.json | 2 +- packages/dev-env/CHANGELOG.md | 9 +++++++++ packages/dev-env/package.json | 2 +- packages/pds/CHANGELOG.md | 7 +++++++ packages/pds/package.json | 2 +- 9 files changed, 33 insertions(+), 9 deletions(-) delete mode 100644 .changeset/moody-wombats-live.md diff --git a/.changeset/moody-wombats-live.md b/.changeset/moody-wombats-live.md deleted file mode 100644 index c146f008d5c..00000000000 --- a/.changeset/moody-wombats-live.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@atproto/api': patch ---- - -Adds support for hashtags in the `RichText.detectFacets` method. diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index f438c52525d..1d6d7788839 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -1,5 +1,11 @@ # @atproto/api +## 0.6.18 + +### Patch Changes + +- [#1651](https://github.com/bluesky-social/atproto/pull/1651) [`2ce8a11b`](https://github.com/bluesky-social/atproto/commit/2ce8a11b8daf5d39027488c5dde8c47b0eb937bf) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Adds support for hashtags in the `RichText.detectFacets` method. + ## 0.6.17 ### Patch Changes diff --git a/packages/api/package.json b/packages/api/package.json index 8165fa63dd2..13624588c0c 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.6.17", + "version": "0.6.18", "license": "MIT", "description": "Client library for atproto and Bluesky", "keywords": [ diff --git a/packages/bsky/CHANGELOG.md b/packages/bsky/CHANGELOG.md index 0d973cc3d50..9d85cfa4a70 100644 --- a/packages/bsky/CHANGELOG.md +++ b/packages/bsky/CHANGELOG.md @@ -1,5 +1,12 @@ # @atproto/bsky +## 0.0.9 + +### Patch Changes + +- Updated dependencies [[`2ce8a11b`](https://github.com/bluesky-social/atproto/commit/2ce8a11b8daf5d39027488c5dde8c47b0eb937bf)]: + - @atproto/api@0.6.18 + ## 0.0.8 ### Patch Changes diff --git a/packages/bsky/package.json b/packages/bsky/package.json index ec203970552..33cafe53947 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/bsky", - "version": "0.0.8", + "version": "0.0.9", "license": "MIT", "description": "Reference implementation of app.bsky App View (Bluesky API)", "keywords": [ diff --git a/packages/dev-env/CHANGELOG.md b/packages/dev-env/CHANGELOG.md index 130a330007b..67523be96e0 100644 --- a/packages/dev-env/CHANGELOG.md +++ b/packages/dev-env/CHANGELOG.md @@ -1,5 +1,14 @@ # @atproto/dev-env +## 0.2.9 + +### Patch Changes + +- Updated dependencies [[`2ce8a11b`](https://github.com/bluesky-social/atproto/commit/2ce8a11b8daf5d39027488c5dde8c47b0eb937bf)]: + - @atproto/api@0.6.18 + - @atproto/bsky@0.0.9 + - @atproto/pds@0.1.18 + ## 0.2.8 ### Patch Changes diff --git a/packages/dev-env/package.json b/packages/dev-env/package.json index d945103437e..926ff0b412d 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/dev-env", - "version": "0.2.8", + "version": "0.2.9", "license": "MIT", "description": "Local development environment helper for atproto development", "keywords": [ diff --git a/packages/pds/CHANGELOG.md b/packages/pds/CHANGELOG.md index 3a450244149..2680326d054 100644 --- a/packages/pds/CHANGELOG.md +++ b/packages/pds/CHANGELOG.md @@ -1,5 +1,12 @@ # @atproto/pds +## 0.1.18 + +### Patch Changes + +- Updated dependencies [[`2ce8a11b`](https://github.com/bluesky-social/atproto/commit/2ce8a11b8daf5d39027488c5dde8c47b0eb937bf)]: + - @atproto/api@0.6.18 + ## 0.1.17 ### Patch Changes diff --git a/packages/pds/package.json b/packages/pds/package.json index de690dad916..833b016f4ef 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/pds", - "version": "0.1.17", + "version": "0.1.18", "license": "MIT", "description": "Reference implementation of atproto Personal Data Server (PDS)", "keywords": [ From 233a132c11764c90c7edabe44d74aef02a922cc7 Mon Sep 17 00:00:00 2001 From: bnewbold Date: Mon, 25 Sep 2023 16:56:00 -0700 Subject: [PATCH 40/46] proposed new search lexicons (#1594) * proposed new search lexicons * lexicons: lint * lexicons: fix actors typo * lexicons: camelCase bites again, ssssss * lexicons: add 'q' and mark 'term' as deprecated for search endpoints * codegen for search lexicon updates * bsky: prefer 'q' over 'term' in existing search endpoints * search: bugfix * lexicons: make unspecced search endpoints return skeleton obj * re-codegen for search skeleton obj --- lexicons/app/bsky/actor/searchActors.json | 13 +- .../app/bsky/actor/searchActorsTypeahead.json | 11 +- lexicons/app/bsky/feed/searchPosts.json | 52 ++++ lexicons/app/bsky/unspecced/defs.json | 20 ++ .../bsky/unspecced/searchActorsSkeleton.json | 56 ++++ .../bsky/unspecced/searchPostsSkeleton.json | 52 ++++ lexicons/com/atproto/admin/searchRepos.json | 6 +- packages/api/src/client/index.ts | 41 +++ packages/api/src/client/lexicons.ts | 239 +++++++++++++++++- .../types/app/bsky/actor/searchActors.ts | 3 + .../app/bsky/actor/searchActorsTypeahead.ts | 3 + .../client/types/app/bsky/feed/searchPosts.ts | 50 ++++ .../client/types/app/bsky/unspecced/defs.ts | 41 +++ .../bsky/unspecced/searchActorsSkeleton.ts | 52 ++++ .../app/bsky/unspecced/searchPostsSkeleton.ts | 50 ++++ .../types/com/atproto/admin/searchRepos.ts | 2 + .../src/api/app/bsky/actor/searchActors.ts | 8 +- .../app/bsky/actor/searchActorsTypeahead.ts | 8 +- .../src/api/com/atproto/admin/searchRepos.ts | 5 + packages/bsky/src/lexicon/index.ts | 36 +++ packages/bsky/src/lexicon/lexicons.ts | 239 +++++++++++++++++- .../types/app/bsky/actor/searchActors.ts | 3 + .../app/bsky/actor/searchActorsTypeahead.ts | 3 + .../types/app/bsky/feed/searchPosts.ts | 54 ++++ .../lexicon/types/app/bsky/unspecced/defs.ts | 41 +++ .../bsky/unspecced/searchActorsSkeleton.ts | 56 ++++ .../app/bsky/unspecced/searchPostsSkeleton.ts | 54 ++++ .../types/com/atproto/admin/searchRepos.ts | 2 + packages/pds/src/lexicon/index.ts | 36 +++ packages/pds/src/lexicon/lexicons.ts | 239 +++++++++++++++++- .../types/app/bsky/actor/searchActors.ts | 3 + .../app/bsky/actor/searchActorsTypeahead.ts | 3 + .../types/app/bsky/feed/searchPosts.ts | 54 ++++ .../lexicon/types/app/bsky/unspecced/defs.ts | 41 +++ .../bsky/unspecced/searchActorsSkeleton.ts | 56 ++++ .../app/bsky/unspecced/searchPostsSkeleton.ts | 54 ++++ .../types/com/atproto/admin/searchRepos.ts | 2 + 37 files changed, 1671 insertions(+), 17 deletions(-) create mode 100644 lexicons/app/bsky/feed/searchPosts.json create mode 100644 lexicons/app/bsky/unspecced/defs.json create mode 100644 lexicons/app/bsky/unspecced/searchActorsSkeleton.json create mode 100644 lexicons/app/bsky/unspecced/searchPostsSkeleton.json create mode 100644 packages/api/src/client/types/app/bsky/feed/searchPosts.ts create mode 100644 packages/api/src/client/types/app/bsky/unspecced/defs.ts create mode 100644 packages/api/src/client/types/app/bsky/unspecced/searchActorsSkeleton.ts create mode 100644 packages/api/src/client/types/app/bsky/unspecced/searchPostsSkeleton.ts create mode 100644 packages/bsky/src/lexicon/types/app/bsky/feed/searchPosts.ts create mode 100644 packages/bsky/src/lexicon/types/app/bsky/unspecced/defs.ts create mode 100644 packages/bsky/src/lexicon/types/app/bsky/unspecced/searchActorsSkeleton.ts create mode 100644 packages/bsky/src/lexicon/types/app/bsky/unspecced/searchPostsSkeleton.ts create mode 100644 packages/pds/src/lexicon/types/app/bsky/feed/searchPosts.ts create mode 100644 packages/pds/src/lexicon/types/app/bsky/unspecced/defs.ts create mode 100644 packages/pds/src/lexicon/types/app/bsky/unspecced/searchActorsSkeleton.ts create mode 100644 packages/pds/src/lexicon/types/app/bsky/unspecced/searchPostsSkeleton.ts diff --git a/lexicons/app/bsky/actor/searchActors.json b/lexicons/app/bsky/actor/searchActors.json index dc76ad8fc39..f65e2fc953b 100644 --- a/lexicons/app/bsky/actor/searchActors.json +++ b/lexicons/app/bsky/actor/searchActors.json @@ -4,16 +4,23 @@ "defs": { "main": { "type": "query", - "description": "Find actors matching search criteria.", + "description": "Find actors (profiles) matching search criteria.", "parameters": { "type": "params", "properties": { - "term": { "type": "string" }, + "term": { + "type": "string", + "description": "DEPRECATED: use 'q' instead" + }, + "q": { + "type": "string", + "description": "search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended" + }, "limit": { "type": "integer", "minimum": 1, "maximum": 100, - "default": 50 + "default": 25 }, "cursor": { "type": "string" } } diff --git a/lexicons/app/bsky/actor/searchActorsTypeahead.json b/lexicons/app/bsky/actor/searchActorsTypeahead.json index 7065f3d7117..f94dd6c3f69 100644 --- a/lexicons/app/bsky/actor/searchActorsTypeahead.json +++ b/lexicons/app/bsky/actor/searchActorsTypeahead.json @@ -8,12 +8,19 @@ "parameters": { "type": "params", "properties": { - "term": { "type": "string" }, + "term": { + "type": "string", + "description": "DEPRECATED: use 'q' instead" + }, + "q": { + "type": "string", + "description": "search query prefix; not a full query string" + }, "limit": { "type": "integer", "minimum": 1, "maximum": 100, - "default": 50 + "default": 10 } } }, diff --git a/lexicons/app/bsky/feed/searchPosts.json b/lexicons/app/bsky/feed/searchPosts.json new file mode 100644 index 00000000000..34eb38f686e --- /dev/null +++ b/lexicons/app/bsky/feed/searchPosts.json @@ -0,0 +1,52 @@ +{ + "lexicon": 1, + "id": "app.bsky.feed.searchPosts", + "defs": { + "main": { + "type": "query", + "description": "Find posts matching search criteria", + "parameters": { + "type": "params", + "required": ["q"], + "properties": { + "q": { + "type": "string", + "description": "search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended" + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + }, + "cursor": { + "type": "string", + "description": "optional pagination mechanism; may not necessarily allow scrolling through entire result set" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["posts"], + "properties": { + "cursor": { "type": "string" }, + "hitsTotal": { + "type": "integer", + "description": "count of search hits. optional, may be rounded/truncated, and may not be possible to paginate through all hits" + }, + "posts": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.feed.defs#postView" + } + } + } + } + }, + "errors": [{ "name": "BadQueryString" }] + } + } +} diff --git a/lexicons/app/bsky/unspecced/defs.json b/lexicons/app/bsky/unspecced/defs.json new file mode 100644 index 00000000000..e9925922a3e --- /dev/null +++ b/lexicons/app/bsky/unspecced/defs.json @@ -0,0 +1,20 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.defs", + "defs": { + "skeletonSearchPost": { + "type": "object", + "required": ["uri"], + "properties": { + "uri": { "type": "string", "format": "at-uri" } + } + }, + "skeletonSearchActor": { + "type": "object", + "required": ["did"], + "properties": { + "did": { "type": "string", "format": "did" } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/searchActorsSkeleton.json b/lexicons/app/bsky/unspecced/searchActorsSkeleton.json new file mode 100644 index 00000000000..108dacf9b14 --- /dev/null +++ b/lexicons/app/bsky/unspecced/searchActorsSkeleton.json @@ -0,0 +1,56 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.searchActorsSkeleton", + "defs": { + "main": { + "type": "query", + "description": "Backend Actors (profile) search, returning only skeleton", + "parameters": { + "type": "params", + "required": ["q"], + "properties": { + "q": { + "type": "string", + "description": "search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. For typeahead search, only simple term match is supported, not full syntax" + }, + "typeahead": { + "type": "boolean", + "description": "if true, acts as fast/simple 'typeahead' query" + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + }, + "cursor": { + "type": "string", + "description": "optional pagination mechanism; may not necessarily allow scrolling through entire result set" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["actors"], + "properties": { + "cursor": { "type": "string" }, + "hitsTotal": { + "type": "integer", + "description": "count of search hits. optional, may be rounded/truncated, and may not be possible to paginate through all hits" + }, + "actors": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.unspecced.defs#skeletonSearchActor" + } + } + } + } + }, + "errors": [{ "name": "BadQueryString" }] + } + } +} diff --git a/lexicons/app/bsky/unspecced/searchPostsSkeleton.json b/lexicons/app/bsky/unspecced/searchPostsSkeleton.json new file mode 100644 index 00000000000..532bfea79f9 --- /dev/null +++ b/lexicons/app/bsky/unspecced/searchPostsSkeleton.json @@ -0,0 +1,52 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.searchPostsSkeleton", + "defs": { + "main": { + "type": "query", + "description": "Backend Posts search, returning only skeleton", + "parameters": { + "type": "params", + "required": ["q"], + "properties": { + "q": { + "type": "string", + "description": "search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended" + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + }, + "cursor": { + "type": "string", + "description": "optional pagination mechanism; may not necessarily allow scrolling through entire result set" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["posts"], + "properties": { + "cursor": { "type": "string" }, + "hitsTotal": { + "type": "integer", + "description": "count of search hits. optional, may be rounded/truncated, and may not be possible to paginate through all hits" + }, + "posts": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.unspecced.defs#skeletonSearchPost" + } + } + } + } + }, + "errors": [{ "name": "BadQueryString" }] + } + } +} diff --git a/lexicons/com/atproto/admin/searchRepos.json b/lexicons/com/atproto/admin/searchRepos.json index fb9c90f343c..85cc6fd482a 100644 --- a/lexicons/com/atproto/admin/searchRepos.json +++ b/lexicons/com/atproto/admin/searchRepos.json @@ -8,7 +8,11 @@ "parameters": { "type": "params", "properties": { - "term": { "type": "string" }, + "term": { + "type": "string", + "description": "DEPRECATED: use 'q' instead" + }, + "q": { "type": "string" }, "invitedBy": { "type": "string" }, "limit": { "type": "integer", diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index d72fe659e50..4d9ae10936b 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -103,6 +103,7 @@ import * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline' import * as AppBskyFeedLike from './types/app/bsky/feed/like' import * as AppBskyFeedPost from './types/app/bsky/feed/post' import * as AppBskyFeedRepost from './types/app/bsky/feed/repost' +import * as AppBskyFeedSearchPosts from './types/app/bsky/feed/searchPosts' import * as AppBskyFeedThreadgate from './types/app/bsky/feed/threadgate' import * as AppBskyGraphBlock from './types/app/bsky/graph/block' import * as AppBskyGraphDefs from './types/app/bsky/graph/defs' @@ -129,9 +130,12 @@ import * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/ import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen' import * as AppBskyRichtextFacet from './types/app/bsky/richtext/facet' import * as AppBskyUnspeccedApplyLabels from './types/app/bsky/unspecced/applyLabels' +import * as AppBskyUnspeccedDefs from './types/app/bsky/unspecced/defs' import * as AppBskyUnspeccedGetPopular from './types/app/bsky/unspecced/getPopular' import * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators' import * as AppBskyUnspeccedGetTimelineSkeleton from './types/app/bsky/unspecced/getTimelineSkeleton' +import * as AppBskyUnspeccedSearchActorsSkeleton from './types/app/bsky/unspecced/searchActorsSkeleton' +import * as AppBskyUnspeccedSearchPostsSkeleton from './types/app/bsky/unspecced/searchPostsSkeleton' export * as ComAtprotoAdminDefs from './types/com/atproto/admin/defs' export * as ComAtprotoAdminDisableAccountInvites from './types/com/atproto/admin/disableAccountInvites' @@ -229,6 +233,7 @@ export * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline' export * as AppBskyFeedLike from './types/app/bsky/feed/like' export * as AppBskyFeedPost from './types/app/bsky/feed/post' export * as AppBskyFeedRepost from './types/app/bsky/feed/repost' +export * as AppBskyFeedSearchPosts from './types/app/bsky/feed/searchPosts' export * as AppBskyFeedThreadgate from './types/app/bsky/feed/threadgate' export * as AppBskyGraphBlock from './types/app/bsky/graph/block' export * as AppBskyGraphDefs from './types/app/bsky/graph/defs' @@ -255,9 +260,12 @@ export * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/ export * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen' export * as AppBskyRichtextFacet from './types/app/bsky/richtext/facet' export * as AppBskyUnspeccedApplyLabels from './types/app/bsky/unspecced/applyLabels' +export * as AppBskyUnspeccedDefs from './types/app/bsky/unspecced/defs' export * as AppBskyUnspeccedGetPopular from './types/app/bsky/unspecced/getPopular' export * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators' export * as AppBskyUnspeccedGetTimelineSkeleton from './types/app/bsky/unspecced/getTimelineSkeleton' +export * as AppBskyUnspeccedSearchActorsSkeleton from './types/app/bsky/unspecced/searchActorsSkeleton' +export * as AppBskyUnspeccedSearchPostsSkeleton from './types/app/bsky/unspecced/searchPostsSkeleton' export const COM_ATPROTO_ADMIN = { DefsTakedown: 'com.atproto.admin.defs#takedown', @@ -1381,6 +1389,17 @@ export class FeedNS { throw AppBskyFeedGetTimeline.toKnownErr(e) }) } + + searchPosts( + params?: AppBskyFeedSearchPosts.QueryParams, + opts?: AppBskyFeedSearchPosts.CallOptions, + ): Promise { + return this._service.xrpc + .call('app.bsky.feed.searchPosts', params, undefined, opts) + .catch((e) => { + throw AppBskyFeedSearchPosts.toKnownErr(e) + }) + } } export class GeneratorRecord { @@ -2282,4 +2301,26 @@ export class UnspeccedNS { throw AppBskyUnspeccedGetTimelineSkeleton.toKnownErr(e) }) } + + searchActorsSkeleton( + params?: AppBskyUnspeccedSearchActorsSkeleton.QueryParams, + opts?: AppBskyUnspeccedSearchActorsSkeleton.CallOptions, + ): Promise { + return this._service.xrpc + .call('app.bsky.unspecced.searchActorsSkeleton', params, undefined, opts) + .catch((e) => { + throw AppBskyUnspeccedSearchActorsSkeleton.toKnownErr(e) + }) + } + + searchPostsSkeleton( + params?: AppBskyUnspeccedSearchPostsSkeleton.QueryParams, + opts?: AppBskyUnspeccedSearchPostsSkeleton.CallOptions, + ): Promise { + return this._service.xrpc + .call('app.bsky.unspecced.searchPostsSkeleton', params, undefined, opts) + .catch((e) => { + throw AppBskyUnspeccedSearchPostsSkeleton.toKnownErr(e) + }) + } } diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index a5cbf08d608..b624ce335ed 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -1113,6 +1113,10 @@ export const schemaDict = { properties: { term: { type: 'string', + description: "DEPRECATED: use 'q' instead", + }, + q: { + type: 'string', }, invitedBy: { type: 'string', @@ -3998,18 +4002,24 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Find actors matching search criteria.', + description: 'Find actors (profiles) matching search criteria.', parameters: { type: 'params', properties: { term: { type: 'string', + description: "DEPRECATED: use 'q' instead", + }, + q: { + type: 'string', + description: + 'search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended', }, limit: { type: 'integer', minimum: 1, maximum: 100, - default: 50, + default: 25, }, cursor: { type: 'string', @@ -4050,12 +4060,17 @@ export const schemaDict = { properties: { term: { type: 'string', + description: "DEPRECATED: use 'q' instead", + }, + q: { + type: 'string', + description: 'search query prefix; not a full query string', }, limit: { type: 'integer', minimum: 1, maximum: 100, - default: 50, + default: 10, }, }, }, @@ -5713,6 +5728,67 @@ export const schemaDict = { }, }, }, + AppBskyFeedSearchPosts: { + lexicon: 1, + id: 'app.bsky.feed.searchPosts', + defs: { + main: { + type: 'query', + description: 'Find posts matching search criteria', + parameters: { + type: 'params', + required: ['q'], + properties: { + q: { + type: 'string', + description: + 'search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 25, + }, + cursor: { + type: 'string', + description: + 'optional pagination mechanism; may not necessarily allow scrolling through entire result set', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['posts'], + properties: { + cursor: { + type: 'string', + }, + hitsTotal: { + type: 'integer', + description: + 'count of search hits. optional, may be rounded/truncated, and may not be possible to paginate through all hits', + }, + posts: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#postView', + }, + }, + }, + }, + }, + errors: [ + { + name: 'BadQueryString', + }, + ], + }, + }, + }, AppBskyFeedThreadgate: { lexicon: 1, id: 'app.bsky.feed.threadgate', @@ -6855,6 +6931,32 @@ export const schemaDict = { }, }, }, + AppBskyUnspeccedDefs: { + lexicon: 1, + id: 'app.bsky.unspecced.defs', + defs: { + skeletonSearchPost: { + type: 'object', + required: ['uri'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + }, + }, + skeletonSearchActor: { + type: 'object', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + }, + }, + }, + }, + }, AppBskyUnspeccedGetPopular: { lexicon: 1, id: 'app.bsky.unspecced.getPopular', @@ -6997,6 +7099,132 @@ export const schemaDict = { }, }, }, + AppBskyUnspeccedSearchActorsSkeleton: { + lexicon: 1, + id: 'app.bsky.unspecced.searchActorsSkeleton', + defs: { + main: { + type: 'query', + description: 'Backend Actors (profile) search, returning only skeleton', + parameters: { + type: 'params', + required: ['q'], + properties: { + q: { + type: 'string', + description: + 'search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. For typeahead search, only simple term match is supported, not full syntax', + }, + typeahead: { + type: 'boolean', + description: "if true, acts as fast/simple 'typeahead' query", + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 25, + }, + cursor: { + type: 'string', + description: + 'optional pagination mechanism; may not necessarily allow scrolling through entire result set', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['actors'], + properties: { + cursor: { + type: 'string', + }, + hitsTotal: { + type: 'integer', + description: + 'count of search hits. optional, may be rounded/truncated, and may not be possible to paginate through all hits', + }, + actors: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.unspecced.defs#skeletonSearchActor', + }, + }, + }, + }, + }, + errors: [ + { + name: 'BadQueryString', + }, + ], + }, + }, + }, + AppBskyUnspeccedSearchPostsSkeleton: { + lexicon: 1, + id: 'app.bsky.unspecced.searchPostsSkeleton', + defs: { + main: { + type: 'query', + description: 'Backend Posts search, returning only skeleton', + parameters: { + type: 'params', + required: ['q'], + properties: { + q: { + type: 'string', + description: + 'search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 25, + }, + cursor: { + type: 'string', + description: + 'optional pagination mechanism; may not necessarily allow scrolling through entire result set', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['posts'], + properties: { + cursor: { + type: 'string', + }, + hitsTotal: { + type: 'integer', + description: + 'count of search hits. optional, may be rounded/truncated, and may not be possible to paginate through all hits', + }, + posts: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.unspecced.defs#skeletonSearchPost', + }, + }, + }, + }, + }, + errors: [ + { + name: 'BadQueryString', + }, + ], + }, + }, + }, } export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[] export const lexicons: Lexicons = new Lexicons(schemas) @@ -7103,6 +7331,7 @@ export const ids = { AppBskyFeedLike: 'app.bsky.feed.like', AppBskyFeedPost: 'app.bsky.feed.post', AppBskyFeedRepost: 'app.bsky.feed.repost', + AppBskyFeedSearchPosts: 'app.bsky.feed.searchPosts', AppBskyFeedThreadgate: 'app.bsky.feed.threadgate', AppBskyGraphBlock: 'app.bsky.graph.block', AppBskyGraphDefs: 'app.bsky.graph.defs', @@ -7131,8 +7360,12 @@ export const ids = { AppBskyNotificationUpdateSeen: 'app.bsky.notification.updateSeen', AppBskyRichtextFacet: 'app.bsky.richtext.facet', AppBskyUnspeccedApplyLabels: 'app.bsky.unspecced.applyLabels', + AppBskyUnspeccedDefs: 'app.bsky.unspecced.defs', AppBskyUnspeccedGetPopular: 'app.bsky.unspecced.getPopular', AppBskyUnspeccedGetPopularFeedGenerators: 'app.bsky.unspecced.getPopularFeedGenerators', AppBskyUnspeccedGetTimelineSkeleton: 'app.bsky.unspecced.getTimelineSkeleton', + AppBskyUnspeccedSearchActorsSkeleton: + 'app.bsky.unspecced.searchActorsSkeleton', + AppBskyUnspeccedSearchPostsSkeleton: 'app.bsky.unspecced.searchPostsSkeleton', } diff --git a/packages/api/src/client/types/app/bsky/actor/searchActors.ts b/packages/api/src/client/types/app/bsky/actor/searchActors.ts index 526e012910f..5e6527606d8 100644 --- a/packages/api/src/client/types/app/bsky/actor/searchActors.ts +++ b/packages/api/src/client/types/app/bsky/actor/searchActors.ts @@ -9,7 +9,10 @@ import { CID } from 'multiformats/cid' import * as AppBskyActorDefs from './defs' export interface QueryParams { + /** DEPRECATED: use 'q' instead */ term?: string + /** search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended */ + q?: string limit?: number cursor?: string } diff --git a/packages/api/src/client/types/app/bsky/actor/searchActorsTypeahead.ts b/packages/api/src/client/types/app/bsky/actor/searchActorsTypeahead.ts index 5bb1557b406..5818d6f64ad 100644 --- a/packages/api/src/client/types/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/api/src/client/types/app/bsky/actor/searchActorsTypeahead.ts @@ -9,7 +9,10 @@ import { CID } from 'multiformats/cid' import * as AppBskyActorDefs from './defs' export interface QueryParams { + /** DEPRECATED: use 'q' instead */ term?: string + /** search query prefix; not a full query string */ + q?: string limit?: number } diff --git a/packages/api/src/client/types/app/bsky/feed/searchPosts.ts b/packages/api/src/client/types/app/bsky/feed/searchPosts.ts new file mode 100644 index 00000000000..6b8613a2e1f --- /dev/null +++ b/packages/api/src/client/types/app/bsky/feed/searchPosts.ts @@ -0,0 +1,50 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { Headers, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as AppBskyFeedDefs from './defs' + +export interface QueryParams { + /** search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended */ + q: string + limit?: number + /** optional pagination mechanism; may not necessarily allow scrolling through entire result set */ + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + /** count of search hits. optional, may be rounded/truncated, and may not be possible to paginate through all hits */ + hitsTotal?: number + posts: AppBskyFeedDefs.PostView[] + [k: string]: unknown +} + +export interface CallOptions { + headers?: Headers +} + +export interface Response { + success: boolean + headers: Headers + data: OutputSchema +} + +export class BadQueryStringError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message, src.headers) + } +} + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + if (e.error === 'BadQueryString') return new BadQueryStringError(e) + } + return e +} diff --git a/packages/api/src/client/types/app/bsky/unspecced/defs.ts b/packages/api/src/client/types/app/bsky/unspecced/defs.ts new file mode 100644 index 00000000000..ecee03578af --- /dev/null +++ b/packages/api/src/client/types/app/bsky/unspecced/defs.ts @@ -0,0 +1,41 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface SkeletonSearchPost { + uri: string + [k: string]: unknown +} + +export function isSkeletonSearchPost(v: unknown): v is SkeletonSearchPost { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.unspecced.defs#skeletonSearchPost' + ) +} + +export function validateSkeletonSearchPost(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.unspecced.defs#skeletonSearchPost', v) +} + +export interface SkeletonSearchActor { + did: string + [k: string]: unknown +} + +export function isSkeletonSearchActor(v: unknown): v is SkeletonSearchActor { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.unspecced.defs#skeletonSearchActor' + ) +} + +export function validateSkeletonSearchActor(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.unspecced.defs#skeletonSearchActor', v) +} diff --git a/packages/api/src/client/types/app/bsky/unspecced/searchActorsSkeleton.ts b/packages/api/src/client/types/app/bsky/unspecced/searchActorsSkeleton.ts new file mode 100644 index 00000000000..7cc2729620e --- /dev/null +++ b/packages/api/src/client/types/app/bsky/unspecced/searchActorsSkeleton.ts @@ -0,0 +1,52 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { Headers, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as AppBskyUnspeccedDefs from './defs' + +export interface QueryParams { + /** search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. For typeahead search, only simple term match is supported, not full syntax */ + q: string + /** if true, acts as fast/simple 'typeahead' query */ + typeahead?: boolean + limit?: number + /** optional pagination mechanism; may not necessarily allow scrolling through entire result set */ + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + /** count of search hits. optional, may be rounded/truncated, and may not be possible to paginate through all hits */ + hitsTotal?: number + actors: AppBskyUnspeccedDefs.SkeletonSearchActor[] + [k: string]: unknown +} + +export interface CallOptions { + headers?: Headers +} + +export interface Response { + success: boolean + headers: Headers + data: OutputSchema +} + +export class BadQueryStringError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message, src.headers) + } +} + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + if (e.error === 'BadQueryString') return new BadQueryStringError(e) + } + return e +} diff --git a/packages/api/src/client/types/app/bsky/unspecced/searchPostsSkeleton.ts b/packages/api/src/client/types/app/bsky/unspecced/searchPostsSkeleton.ts new file mode 100644 index 00000000000..07391886f8f --- /dev/null +++ b/packages/api/src/client/types/app/bsky/unspecced/searchPostsSkeleton.ts @@ -0,0 +1,50 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { Headers, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as AppBskyUnspeccedDefs from './defs' + +export interface QueryParams { + /** search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended */ + q: string + limit?: number + /** optional pagination mechanism; may not necessarily allow scrolling through entire result set */ + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + /** count of search hits. optional, may be rounded/truncated, and may not be possible to paginate through all hits */ + hitsTotal?: number + posts: AppBskyUnspeccedDefs.SkeletonSearchPost[] + [k: string]: unknown +} + +export interface CallOptions { + headers?: Headers +} + +export interface Response { + success: boolean + headers: Headers + data: OutputSchema +} + +export class BadQueryStringError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message, src.headers) + } +} + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + if (e.error === 'BadQueryString') return new BadQueryStringError(e) + } + return e +} diff --git a/packages/api/src/client/types/com/atproto/admin/searchRepos.ts b/packages/api/src/client/types/com/atproto/admin/searchRepos.ts index a43e0ee7322..372cc98ff13 100644 --- a/packages/api/src/client/types/com/atproto/admin/searchRepos.ts +++ b/packages/api/src/client/types/com/atproto/admin/searchRepos.ts @@ -9,7 +9,9 @@ import { CID } from 'multiformats/cid' import * as ComAtprotoAdminDefs from './defs' export interface QueryParams { + /** DEPRECATED: use 'q' instead */ term?: string + q?: string invitedBy?: string limit?: number cursor?: string diff --git a/packages/bsky/src/api/app/bsky/actor/searchActors.ts b/packages/bsky/src/api/app/bsky/actor/searchActors.ts index df5821a03f9..d4ae0a8d264 100644 --- a/packages/bsky/src/api/app/bsky/actor/searchActors.ts +++ b/packages/bsky/src/api/app/bsky/actor/searchActors.ts @@ -11,8 +11,14 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.searchActors({ auth: ctx.authOptionalVerifier, handler: async ({ auth, params }) => { - const { cursor, limit, term: rawTerm } = params + let { cursor, limit, term: rawTerm, q: rawQ } = params const requester = auth.credentials.did + + // prefer new 'q' query param over deprecated 'term' + if (rawQ) { + rawTerm = rawQ + } + const term = cleanTerm(rawTerm || '') const db = ctx.db.getReplica('search') diff --git a/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts b/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts index 64bcd811d02..c438c4d2324 100644 --- a/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts @@ -9,8 +9,14 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.searchActorsTypeahead({ auth: ctx.authOptionalVerifier, handler: async ({ params, auth }) => { - const { limit, term: rawTerm } = params + let { limit, term: rawTerm, q: rawQ } = params const requester = auth.credentials.did + + // prefer new 'q' query param over deprecated 'term' + if (rawQ) { + rawTerm = rawQ + } + const term = cleanTerm(rawTerm || '') const db = ctx.db.getReplica('search') diff --git a/packages/bsky/src/api/com/atproto/admin/searchRepos.ts b/packages/bsky/src/api/com/atproto/admin/searchRepos.ts index 9945b27fcb4..a17421e90cd 100644 --- a/packages/bsky/src/api/com/atproto/admin/searchRepos.ts +++ b/packages/bsky/src/api/com/atproto/admin/searchRepos.ts @@ -12,6 +12,11 @@ export default function (server: Server, ctx: AppContext) { if (invitedBy) { throw new InvalidRequestError('The invitedBy parameter is unsupported') } + // prefer new 'q' query param over deprecated 'term' + const { q } = params + if (q) { + params.term = q + } const { results, cursor } = await ctx.services .actor(db) diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index 52635132470..3ec69a3503f 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -89,6 +89,7 @@ import * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts' import * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy' import * as AppBskyFeedGetSuggestedFeeds from './types/app/bsky/feed/getSuggestedFeeds' import * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline' +import * as AppBskyFeedSearchPosts from './types/app/bsky/feed/searchPosts' import * as AppBskyGraphGetBlocks from './types/app/bsky/graph/getBlocks' import * as AppBskyGraphGetFollowers from './types/app/bsky/graph/getFollowers' import * as AppBskyGraphGetFollows from './types/app/bsky/graph/getFollows' @@ -110,6 +111,8 @@ import * as AppBskyUnspeccedApplyLabels from './types/app/bsky/unspecced/applyLa import * as AppBskyUnspeccedGetPopular from './types/app/bsky/unspecced/getPopular' import * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators' import * as AppBskyUnspeccedGetTimelineSkeleton from './types/app/bsky/unspecced/getTimelineSkeleton' +import * as AppBskyUnspeccedSearchActorsSkeleton from './types/app/bsky/unspecced/searchActorsSkeleton' +import * as AppBskyUnspeccedSearchPostsSkeleton from './types/app/bsky/unspecced/searchPostsSkeleton' export const COM_ATPROTO_ADMIN = { DefsTakedown: 'com.atproto.admin.defs#takedown', @@ -1168,6 +1171,17 @@ export class FeedNS { const nsid = 'app.bsky.feed.getTimeline' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } + + searchPosts( + cfg: ConfigOf< + AV, + AppBskyFeedSearchPosts.Handler>, + AppBskyFeedSearchPosts.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.feed.searchPosts' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } } export class GraphNS { @@ -1431,6 +1445,28 @@ export class UnspeccedNS { const nsid = 'app.bsky.unspecced.getTimelineSkeleton' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } + + searchActorsSkeleton( + cfg: ConfigOf< + AV, + AppBskyUnspeccedSearchActorsSkeleton.Handler>, + AppBskyUnspeccedSearchActorsSkeleton.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.unspecced.searchActorsSkeleton' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + searchPostsSkeleton( + cfg: ConfigOf< + AV, + AppBskyUnspeccedSearchPostsSkeleton.Handler>, + AppBskyUnspeccedSearchPostsSkeleton.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.unspecced.searchPostsSkeleton' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } } type SharedRateLimitOpts = { diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index a5cbf08d608..b624ce335ed 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -1113,6 +1113,10 @@ export const schemaDict = { properties: { term: { type: 'string', + description: "DEPRECATED: use 'q' instead", + }, + q: { + type: 'string', }, invitedBy: { type: 'string', @@ -3998,18 +4002,24 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Find actors matching search criteria.', + description: 'Find actors (profiles) matching search criteria.', parameters: { type: 'params', properties: { term: { type: 'string', + description: "DEPRECATED: use 'q' instead", + }, + q: { + type: 'string', + description: + 'search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended', }, limit: { type: 'integer', minimum: 1, maximum: 100, - default: 50, + default: 25, }, cursor: { type: 'string', @@ -4050,12 +4060,17 @@ export const schemaDict = { properties: { term: { type: 'string', + description: "DEPRECATED: use 'q' instead", + }, + q: { + type: 'string', + description: 'search query prefix; not a full query string', }, limit: { type: 'integer', minimum: 1, maximum: 100, - default: 50, + default: 10, }, }, }, @@ -5713,6 +5728,67 @@ export const schemaDict = { }, }, }, + AppBskyFeedSearchPosts: { + lexicon: 1, + id: 'app.bsky.feed.searchPosts', + defs: { + main: { + type: 'query', + description: 'Find posts matching search criteria', + parameters: { + type: 'params', + required: ['q'], + properties: { + q: { + type: 'string', + description: + 'search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 25, + }, + cursor: { + type: 'string', + description: + 'optional pagination mechanism; may not necessarily allow scrolling through entire result set', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['posts'], + properties: { + cursor: { + type: 'string', + }, + hitsTotal: { + type: 'integer', + description: + 'count of search hits. optional, may be rounded/truncated, and may not be possible to paginate through all hits', + }, + posts: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#postView', + }, + }, + }, + }, + }, + errors: [ + { + name: 'BadQueryString', + }, + ], + }, + }, + }, AppBskyFeedThreadgate: { lexicon: 1, id: 'app.bsky.feed.threadgate', @@ -6855,6 +6931,32 @@ export const schemaDict = { }, }, }, + AppBskyUnspeccedDefs: { + lexicon: 1, + id: 'app.bsky.unspecced.defs', + defs: { + skeletonSearchPost: { + type: 'object', + required: ['uri'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + }, + }, + skeletonSearchActor: { + type: 'object', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + }, + }, + }, + }, + }, AppBskyUnspeccedGetPopular: { lexicon: 1, id: 'app.bsky.unspecced.getPopular', @@ -6997,6 +7099,132 @@ export const schemaDict = { }, }, }, + AppBskyUnspeccedSearchActorsSkeleton: { + lexicon: 1, + id: 'app.bsky.unspecced.searchActorsSkeleton', + defs: { + main: { + type: 'query', + description: 'Backend Actors (profile) search, returning only skeleton', + parameters: { + type: 'params', + required: ['q'], + properties: { + q: { + type: 'string', + description: + 'search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. For typeahead search, only simple term match is supported, not full syntax', + }, + typeahead: { + type: 'boolean', + description: "if true, acts as fast/simple 'typeahead' query", + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 25, + }, + cursor: { + type: 'string', + description: + 'optional pagination mechanism; may not necessarily allow scrolling through entire result set', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['actors'], + properties: { + cursor: { + type: 'string', + }, + hitsTotal: { + type: 'integer', + description: + 'count of search hits. optional, may be rounded/truncated, and may not be possible to paginate through all hits', + }, + actors: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.unspecced.defs#skeletonSearchActor', + }, + }, + }, + }, + }, + errors: [ + { + name: 'BadQueryString', + }, + ], + }, + }, + }, + AppBskyUnspeccedSearchPostsSkeleton: { + lexicon: 1, + id: 'app.bsky.unspecced.searchPostsSkeleton', + defs: { + main: { + type: 'query', + description: 'Backend Posts search, returning only skeleton', + parameters: { + type: 'params', + required: ['q'], + properties: { + q: { + type: 'string', + description: + 'search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 25, + }, + cursor: { + type: 'string', + description: + 'optional pagination mechanism; may not necessarily allow scrolling through entire result set', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['posts'], + properties: { + cursor: { + type: 'string', + }, + hitsTotal: { + type: 'integer', + description: + 'count of search hits. optional, may be rounded/truncated, and may not be possible to paginate through all hits', + }, + posts: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.unspecced.defs#skeletonSearchPost', + }, + }, + }, + }, + }, + errors: [ + { + name: 'BadQueryString', + }, + ], + }, + }, + }, } export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[] export const lexicons: Lexicons = new Lexicons(schemas) @@ -7103,6 +7331,7 @@ export const ids = { AppBskyFeedLike: 'app.bsky.feed.like', AppBskyFeedPost: 'app.bsky.feed.post', AppBskyFeedRepost: 'app.bsky.feed.repost', + AppBskyFeedSearchPosts: 'app.bsky.feed.searchPosts', AppBskyFeedThreadgate: 'app.bsky.feed.threadgate', AppBskyGraphBlock: 'app.bsky.graph.block', AppBskyGraphDefs: 'app.bsky.graph.defs', @@ -7131,8 +7360,12 @@ export const ids = { AppBskyNotificationUpdateSeen: 'app.bsky.notification.updateSeen', AppBskyRichtextFacet: 'app.bsky.richtext.facet', AppBskyUnspeccedApplyLabels: 'app.bsky.unspecced.applyLabels', + AppBskyUnspeccedDefs: 'app.bsky.unspecced.defs', AppBskyUnspeccedGetPopular: 'app.bsky.unspecced.getPopular', AppBskyUnspeccedGetPopularFeedGenerators: 'app.bsky.unspecced.getPopularFeedGenerators', AppBskyUnspeccedGetTimelineSkeleton: 'app.bsky.unspecced.getTimelineSkeleton', + AppBskyUnspeccedSearchActorsSkeleton: + 'app.bsky.unspecced.searchActorsSkeleton', + AppBskyUnspeccedSearchPostsSkeleton: 'app.bsky.unspecced.searchPostsSkeleton', } diff --git a/packages/bsky/src/lexicon/types/app/bsky/actor/searchActors.ts b/packages/bsky/src/lexicon/types/app/bsky/actor/searchActors.ts index f620a463cff..0222f3658da 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/searchActors.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/searchActors.ts @@ -10,7 +10,10 @@ import { HandlerAuth } from '@atproto/xrpc-server' import * as AppBskyActorDefs from './defs' export interface QueryParams { + /** DEPRECATED: use 'q' instead */ term?: string + /** search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended */ + q?: string limit: number cursor?: string } diff --git a/packages/bsky/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts b/packages/bsky/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts index 4f5bbb7c23c..ba0d62444ce 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts @@ -10,7 +10,10 @@ import { HandlerAuth } from '@atproto/xrpc-server' import * as AppBskyActorDefs from './defs' export interface QueryParams { + /** DEPRECATED: use 'q' instead */ term?: string + /** search query prefix; not a full query string */ + q?: string limit: number } diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/searchPosts.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/searchPosts.ts new file mode 100644 index 00000000000..6b5fe08e467 --- /dev/null +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/searchPosts.ts @@ -0,0 +1,54 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyFeedDefs from './defs' + +export interface QueryParams { + /** search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended */ + q: string + limit: number + /** optional pagination mechanism; may not necessarily allow scrolling through entire result set */ + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + /** count of search hits. optional, may be rounded/truncated, and may not be possible to paginate through all hits */ + hitsTotal?: number + posts: AppBskyFeedDefs.PostView[] + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'BadQueryString' +} + +export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/unspecced/defs.ts b/packages/bsky/src/lexicon/types/app/bsky/unspecced/defs.ts new file mode 100644 index 00000000000..59a6b38064c --- /dev/null +++ b/packages/bsky/src/lexicon/types/app/bsky/unspecced/defs.ts @@ -0,0 +1,41 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' + +export interface SkeletonSearchPost { + uri: string + [k: string]: unknown +} + +export function isSkeletonSearchPost(v: unknown): v is SkeletonSearchPost { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.unspecced.defs#skeletonSearchPost' + ) +} + +export function validateSkeletonSearchPost(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.unspecced.defs#skeletonSearchPost', v) +} + +export interface SkeletonSearchActor { + did: string + [k: string]: unknown +} + +export function isSkeletonSearchActor(v: unknown): v is SkeletonSearchActor { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.unspecced.defs#skeletonSearchActor' + ) +} + +export function validateSkeletonSearchActor(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.unspecced.defs#skeletonSearchActor', v) +} diff --git a/packages/bsky/src/lexicon/types/app/bsky/unspecced/searchActorsSkeleton.ts b/packages/bsky/src/lexicon/types/app/bsky/unspecced/searchActorsSkeleton.ts new file mode 100644 index 00000000000..2cf59bf86a9 --- /dev/null +++ b/packages/bsky/src/lexicon/types/app/bsky/unspecced/searchActorsSkeleton.ts @@ -0,0 +1,56 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyUnspeccedDefs from './defs' + +export interface QueryParams { + /** search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. For typeahead search, only simple term match is supported, not full syntax */ + q: string + /** if true, acts as fast/simple 'typeahead' query */ + typeahead?: boolean + limit: number + /** optional pagination mechanism; may not necessarily allow scrolling through entire result set */ + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + /** count of search hits. optional, may be rounded/truncated, and may not be possible to paginate through all hits */ + hitsTotal?: number + actors: AppBskyUnspeccedDefs.SkeletonSearchActor[] + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'BadQueryString' +} + +export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/unspecced/searchPostsSkeleton.ts b/packages/bsky/src/lexicon/types/app/bsky/unspecced/searchPostsSkeleton.ts new file mode 100644 index 00000000000..df990d2c5c6 --- /dev/null +++ b/packages/bsky/src/lexicon/types/app/bsky/unspecced/searchPostsSkeleton.ts @@ -0,0 +1,54 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyUnspeccedDefs from './defs' + +export interface QueryParams { + /** search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended */ + q: string + limit: number + /** optional pagination mechanism; may not necessarily allow scrolling through entire result set */ + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + /** count of search hits. optional, may be rounded/truncated, and may not be possible to paginate through all hits */ + hitsTotal?: number + posts: AppBskyUnspeccedDefs.SkeletonSearchPost[] + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'BadQueryString' +} + +export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/searchRepos.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/searchRepos.ts index c79cd046ca0..32266fd66fd 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/searchRepos.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/searchRepos.ts @@ -10,7 +10,9 @@ import { HandlerAuth } from '@atproto/xrpc-server' import * as ComAtprotoAdminDefs from './defs' export interface QueryParams { + /** DEPRECATED: use 'q' instead */ term?: string + q?: string invitedBy?: string limit: number cursor?: string diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 52635132470..3ec69a3503f 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -89,6 +89,7 @@ import * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts' import * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy' import * as AppBskyFeedGetSuggestedFeeds from './types/app/bsky/feed/getSuggestedFeeds' import * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline' +import * as AppBskyFeedSearchPosts from './types/app/bsky/feed/searchPosts' import * as AppBskyGraphGetBlocks from './types/app/bsky/graph/getBlocks' import * as AppBskyGraphGetFollowers from './types/app/bsky/graph/getFollowers' import * as AppBskyGraphGetFollows from './types/app/bsky/graph/getFollows' @@ -110,6 +111,8 @@ import * as AppBskyUnspeccedApplyLabels from './types/app/bsky/unspecced/applyLa import * as AppBskyUnspeccedGetPopular from './types/app/bsky/unspecced/getPopular' import * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators' import * as AppBskyUnspeccedGetTimelineSkeleton from './types/app/bsky/unspecced/getTimelineSkeleton' +import * as AppBskyUnspeccedSearchActorsSkeleton from './types/app/bsky/unspecced/searchActorsSkeleton' +import * as AppBskyUnspeccedSearchPostsSkeleton from './types/app/bsky/unspecced/searchPostsSkeleton' export const COM_ATPROTO_ADMIN = { DefsTakedown: 'com.atproto.admin.defs#takedown', @@ -1168,6 +1171,17 @@ export class FeedNS { const nsid = 'app.bsky.feed.getTimeline' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } + + searchPosts( + cfg: ConfigOf< + AV, + AppBskyFeedSearchPosts.Handler>, + AppBskyFeedSearchPosts.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.feed.searchPosts' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } } export class GraphNS { @@ -1431,6 +1445,28 @@ export class UnspeccedNS { const nsid = 'app.bsky.unspecced.getTimelineSkeleton' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } + + searchActorsSkeleton( + cfg: ConfigOf< + AV, + AppBskyUnspeccedSearchActorsSkeleton.Handler>, + AppBskyUnspeccedSearchActorsSkeleton.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.unspecced.searchActorsSkeleton' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + searchPostsSkeleton( + cfg: ConfigOf< + AV, + AppBskyUnspeccedSearchPostsSkeleton.Handler>, + AppBskyUnspeccedSearchPostsSkeleton.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.unspecced.searchPostsSkeleton' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } } type SharedRateLimitOpts = { diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index a5cbf08d608..b624ce335ed 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -1113,6 +1113,10 @@ export const schemaDict = { properties: { term: { type: 'string', + description: "DEPRECATED: use 'q' instead", + }, + q: { + type: 'string', }, invitedBy: { type: 'string', @@ -3998,18 +4002,24 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Find actors matching search criteria.', + description: 'Find actors (profiles) matching search criteria.', parameters: { type: 'params', properties: { term: { type: 'string', + description: "DEPRECATED: use 'q' instead", + }, + q: { + type: 'string', + description: + 'search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended', }, limit: { type: 'integer', minimum: 1, maximum: 100, - default: 50, + default: 25, }, cursor: { type: 'string', @@ -4050,12 +4060,17 @@ export const schemaDict = { properties: { term: { type: 'string', + description: "DEPRECATED: use 'q' instead", + }, + q: { + type: 'string', + description: 'search query prefix; not a full query string', }, limit: { type: 'integer', minimum: 1, maximum: 100, - default: 50, + default: 10, }, }, }, @@ -5713,6 +5728,67 @@ export const schemaDict = { }, }, }, + AppBskyFeedSearchPosts: { + lexicon: 1, + id: 'app.bsky.feed.searchPosts', + defs: { + main: { + type: 'query', + description: 'Find posts matching search criteria', + parameters: { + type: 'params', + required: ['q'], + properties: { + q: { + type: 'string', + description: + 'search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 25, + }, + cursor: { + type: 'string', + description: + 'optional pagination mechanism; may not necessarily allow scrolling through entire result set', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['posts'], + properties: { + cursor: { + type: 'string', + }, + hitsTotal: { + type: 'integer', + description: + 'count of search hits. optional, may be rounded/truncated, and may not be possible to paginate through all hits', + }, + posts: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#postView', + }, + }, + }, + }, + }, + errors: [ + { + name: 'BadQueryString', + }, + ], + }, + }, + }, AppBskyFeedThreadgate: { lexicon: 1, id: 'app.bsky.feed.threadgate', @@ -6855,6 +6931,32 @@ export const schemaDict = { }, }, }, + AppBskyUnspeccedDefs: { + lexicon: 1, + id: 'app.bsky.unspecced.defs', + defs: { + skeletonSearchPost: { + type: 'object', + required: ['uri'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + }, + }, + skeletonSearchActor: { + type: 'object', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + }, + }, + }, + }, + }, AppBskyUnspeccedGetPopular: { lexicon: 1, id: 'app.bsky.unspecced.getPopular', @@ -6997,6 +7099,132 @@ export const schemaDict = { }, }, }, + AppBskyUnspeccedSearchActorsSkeleton: { + lexicon: 1, + id: 'app.bsky.unspecced.searchActorsSkeleton', + defs: { + main: { + type: 'query', + description: 'Backend Actors (profile) search, returning only skeleton', + parameters: { + type: 'params', + required: ['q'], + properties: { + q: { + type: 'string', + description: + 'search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. For typeahead search, only simple term match is supported, not full syntax', + }, + typeahead: { + type: 'boolean', + description: "if true, acts as fast/simple 'typeahead' query", + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 25, + }, + cursor: { + type: 'string', + description: + 'optional pagination mechanism; may not necessarily allow scrolling through entire result set', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['actors'], + properties: { + cursor: { + type: 'string', + }, + hitsTotal: { + type: 'integer', + description: + 'count of search hits. optional, may be rounded/truncated, and may not be possible to paginate through all hits', + }, + actors: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.unspecced.defs#skeletonSearchActor', + }, + }, + }, + }, + }, + errors: [ + { + name: 'BadQueryString', + }, + ], + }, + }, + }, + AppBskyUnspeccedSearchPostsSkeleton: { + lexicon: 1, + id: 'app.bsky.unspecced.searchPostsSkeleton', + defs: { + main: { + type: 'query', + description: 'Backend Posts search, returning only skeleton', + parameters: { + type: 'params', + required: ['q'], + properties: { + q: { + type: 'string', + description: + 'search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 25, + }, + cursor: { + type: 'string', + description: + 'optional pagination mechanism; may not necessarily allow scrolling through entire result set', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['posts'], + properties: { + cursor: { + type: 'string', + }, + hitsTotal: { + type: 'integer', + description: + 'count of search hits. optional, may be rounded/truncated, and may not be possible to paginate through all hits', + }, + posts: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.unspecced.defs#skeletonSearchPost', + }, + }, + }, + }, + }, + errors: [ + { + name: 'BadQueryString', + }, + ], + }, + }, + }, } export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[] export const lexicons: Lexicons = new Lexicons(schemas) @@ -7103,6 +7331,7 @@ export const ids = { AppBskyFeedLike: 'app.bsky.feed.like', AppBskyFeedPost: 'app.bsky.feed.post', AppBskyFeedRepost: 'app.bsky.feed.repost', + AppBskyFeedSearchPosts: 'app.bsky.feed.searchPosts', AppBskyFeedThreadgate: 'app.bsky.feed.threadgate', AppBskyGraphBlock: 'app.bsky.graph.block', AppBskyGraphDefs: 'app.bsky.graph.defs', @@ -7131,8 +7360,12 @@ export const ids = { AppBskyNotificationUpdateSeen: 'app.bsky.notification.updateSeen', AppBskyRichtextFacet: 'app.bsky.richtext.facet', AppBskyUnspeccedApplyLabels: 'app.bsky.unspecced.applyLabels', + AppBskyUnspeccedDefs: 'app.bsky.unspecced.defs', AppBskyUnspeccedGetPopular: 'app.bsky.unspecced.getPopular', AppBskyUnspeccedGetPopularFeedGenerators: 'app.bsky.unspecced.getPopularFeedGenerators', AppBskyUnspeccedGetTimelineSkeleton: 'app.bsky.unspecced.getTimelineSkeleton', + AppBskyUnspeccedSearchActorsSkeleton: + 'app.bsky.unspecced.searchActorsSkeleton', + AppBskyUnspeccedSearchPostsSkeleton: 'app.bsky.unspecced.searchPostsSkeleton', } diff --git a/packages/pds/src/lexicon/types/app/bsky/actor/searchActors.ts b/packages/pds/src/lexicon/types/app/bsky/actor/searchActors.ts index f620a463cff..0222f3658da 100644 --- a/packages/pds/src/lexicon/types/app/bsky/actor/searchActors.ts +++ b/packages/pds/src/lexicon/types/app/bsky/actor/searchActors.ts @@ -10,7 +10,10 @@ import { HandlerAuth } from '@atproto/xrpc-server' import * as AppBskyActorDefs from './defs' export interface QueryParams { + /** DEPRECATED: use 'q' instead */ term?: string + /** search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended */ + q?: string limit: number cursor?: string } diff --git a/packages/pds/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts b/packages/pds/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts index 4f5bbb7c23c..ba0d62444ce 100644 --- a/packages/pds/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/pds/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts @@ -10,7 +10,10 @@ import { HandlerAuth } from '@atproto/xrpc-server' import * as AppBskyActorDefs from './defs' export interface QueryParams { + /** DEPRECATED: use 'q' instead */ term?: string + /** search query prefix; not a full query string */ + q?: string limit: number } diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/searchPosts.ts b/packages/pds/src/lexicon/types/app/bsky/feed/searchPosts.ts new file mode 100644 index 00000000000..6b5fe08e467 --- /dev/null +++ b/packages/pds/src/lexicon/types/app/bsky/feed/searchPosts.ts @@ -0,0 +1,54 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyFeedDefs from './defs' + +export interface QueryParams { + /** search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended */ + q: string + limit: number + /** optional pagination mechanism; may not necessarily allow scrolling through entire result set */ + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + /** count of search hits. optional, may be rounded/truncated, and may not be possible to paginate through all hits */ + hitsTotal?: number + posts: AppBskyFeedDefs.PostView[] + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'BadQueryString' +} + +export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/unspecced/defs.ts b/packages/pds/src/lexicon/types/app/bsky/unspecced/defs.ts new file mode 100644 index 00000000000..59a6b38064c --- /dev/null +++ b/packages/pds/src/lexicon/types/app/bsky/unspecced/defs.ts @@ -0,0 +1,41 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' + +export interface SkeletonSearchPost { + uri: string + [k: string]: unknown +} + +export function isSkeletonSearchPost(v: unknown): v is SkeletonSearchPost { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.unspecced.defs#skeletonSearchPost' + ) +} + +export function validateSkeletonSearchPost(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.unspecced.defs#skeletonSearchPost', v) +} + +export interface SkeletonSearchActor { + did: string + [k: string]: unknown +} + +export function isSkeletonSearchActor(v: unknown): v is SkeletonSearchActor { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.unspecced.defs#skeletonSearchActor' + ) +} + +export function validateSkeletonSearchActor(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.unspecced.defs#skeletonSearchActor', v) +} diff --git a/packages/pds/src/lexicon/types/app/bsky/unspecced/searchActorsSkeleton.ts b/packages/pds/src/lexicon/types/app/bsky/unspecced/searchActorsSkeleton.ts new file mode 100644 index 00000000000..2cf59bf86a9 --- /dev/null +++ b/packages/pds/src/lexicon/types/app/bsky/unspecced/searchActorsSkeleton.ts @@ -0,0 +1,56 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyUnspeccedDefs from './defs' + +export interface QueryParams { + /** search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. For typeahead search, only simple term match is supported, not full syntax */ + q: string + /** if true, acts as fast/simple 'typeahead' query */ + typeahead?: boolean + limit: number + /** optional pagination mechanism; may not necessarily allow scrolling through entire result set */ + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + /** count of search hits. optional, may be rounded/truncated, and may not be possible to paginate through all hits */ + hitsTotal?: number + actors: AppBskyUnspeccedDefs.SkeletonSearchActor[] + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'BadQueryString' +} + +export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/unspecced/searchPostsSkeleton.ts b/packages/pds/src/lexicon/types/app/bsky/unspecced/searchPostsSkeleton.ts new file mode 100644 index 00000000000..df990d2c5c6 --- /dev/null +++ b/packages/pds/src/lexicon/types/app/bsky/unspecced/searchPostsSkeleton.ts @@ -0,0 +1,54 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyUnspeccedDefs from './defs' + +export interface QueryParams { + /** search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended */ + q: string + limit: number + /** optional pagination mechanism; may not necessarily allow scrolling through entire result set */ + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + /** count of search hits. optional, may be rounded/truncated, and may not be possible to paginate through all hits */ + hitsTotal?: number + posts: AppBskyUnspeccedDefs.SkeletonSearchPost[] + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'BadQueryString' +} + +export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/searchRepos.ts b/packages/pds/src/lexicon/types/com/atproto/admin/searchRepos.ts index c79cd046ca0..32266fd66fd 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/searchRepos.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/searchRepos.ts @@ -10,7 +10,9 @@ import { HandlerAuth } from '@atproto/xrpc-server' import * as ComAtprotoAdminDefs from './defs' export interface QueryParams { + /** DEPRECATED: use 'q' instead */ term?: string + q?: string invitedBy?: string limit: number cursor?: string From 2fa9088639c1e17e49e524bb8fa2db2d4e9680a7 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 25 Sep 2023 20:09:52 -0500 Subject: [PATCH 41/46] Disable pds appview indexing (#1645) * rm indexing service * remove message queue & refactor background queue * wip * remove all canProxyReadc * finish cleanup * clean up tests * fix up tests * fix api tests * fix build * fix compression test * update image tests * fix dev envs * build branch * wip - removing labeler * fix service file * remove kysely tables * re-enable getPopular * format * cleaning up tests * rm unused sharp code * rm pds build * clean up tests * fix build * fix build * migration * tidy * build branch * tidy * build branch * small tidy * dont build --- lexicons/app/bsky/unspecced/applyLabels.json | 23 - packages/api/src/client/index.ts | 13 - packages/api/src/client/lexicons.ts | 27 -- .../types/app/bsky/unspecced/applyLabels.ts | 33 -- packages/bsky/package.json | 1 - packages/bsky/src/auto-moderator/index.ts | 18 +- packages/bsky/src/lexicon/index.ts | 12 - packages/bsky/src/lexicon/lexicons.ts | 27 -- .../types/app/bsky/unspecced/applyLabels.ts | 39 -- .../src/services/indexing/plugins/block.ts | 2 +- .../indexing/plugins/feed-generator.ts | 2 +- .../src/services/indexing/plugins/follow.ts | 2 +- .../src/services/indexing/plugins/like.ts | 2 +- .../services/indexing/plugins/list-block.ts | 2 +- .../services/indexing/plugins/list-item.ts | 2 +- .../src/services/indexing/plugins/list.ts | 2 +- .../src/services/indexing/plugins/post.ts | 2 +- .../src/services/indexing/plugins/repost.ts | 2 +- .../services/indexing/plugins/thread-gate.ts | 2 +- packages/bsky/src/services/label/index.ts | 2 +- packages/common/package.json | 1 + .../indexing/util.ts => common/src/dates.ts} | 0 packages/common/src/index.ts | 1 + packages/dev-env/build.js | 2 +- packages/dev-env/package.json | 3 +- packages/dev-env/src/bin-network.ts | 41 -- packages/dev-env/src/bin.ts | 10 +- packages/dev-env/src/mock/index.ts | 46 +- packages/dev-env/src/pds.ts | 12 - packages/dev-env/src/types.ts | 2 - packages/pds/package.json | 1 - .../atproto/admin/reverseModerationAction.ts | 31 -- .../com/atproto/admin/takeModerationAction.ts | 21 - .../src/app-view/api/app/bsky/unspecced.ts | 19 +- packages/pds/src/app-view/db/index.ts | 38 -- .../pds/src/app-view/db/tables/actor-block.ts | 11 - packages/pds/src/app-view/db/tables/algo.ts | 12 - .../app-view/db/tables/duplicate-record.ts | 12 - .../src/app-view/db/tables/feed-generator.ts | 18 - .../pds/src/app-view/db/tables/feed-item.ts | 12 - packages/pds/src/app-view/db/tables/follow.ts | 11 - packages/pds/src/app-view/db/tables/like.ts | 13 - .../pds/src/app-view/db/tables/list-item.ts | 13 - packages/pds/src/app-view/db/tables/list.ts | 16 - .../pds/src/app-view/db/tables/post-agg.ts | 14 - .../pds/src/app-view/db/tables/post-embed.ts | 30 -- packages/pds/src/app-view/db/tables/post.ts | 18 - .../pds/src/app-view/db/tables/profile-agg.ts | 14 - .../pds/src/app-view/db/tables/profile.ts | 13 - packages/pds/src/app-view/db/tables/repost.ts | 13 - .../src/app-view/db/tables/subscription.ts | 9 - .../app-view/db/tables/suggested-follow.ts | 10 - .../pds/src/app-view/db/tables/view-param.ts | 12 - .../src/app-view/event-stream/consumers.ts | 38 -- .../src/app-view/services/indexing/index.ts | 136 ------ .../services/indexing/plugins/block.ts | 97 ----- .../indexing/plugins/feed-generator.ts | 89 ---- .../services/indexing/plugins/follow.ts | 138 ------ .../services/indexing/plugins/like.ts | 128 ------ .../services/indexing/plugins/list-item.ts | 102 ----- .../services/indexing/plugins/list.ts | 86 ---- .../services/indexing/plugins/post.ts | 326 -------------- .../services/indexing/plugins/profile.ts | 82 ---- .../services/indexing/plugins/repost.ts | 153 ------- .../app-view/services/indexing/processor.ts | 259 ----------- .../src/app-view/services/indexing/util.ts | 16 - .../pds/src/app-view/services/label/index.ts | 171 -------- .../background-queue.ts => background.ts} | 4 +- packages/pds/src/config.ts | 32 -- packages/pds/src/context.ts | 20 +- packages/pds/src/db/database-schema.ts | 6 +- .../20230922T033938477Z-remove-appview.ts | 29 ++ packages/pds/src/db/migrations/index.ts | 1 + .../db/periodic-moderation-action-reversal.ts | 25 -- packages/pds/src/db/tables/label.ts | 12 - .../pds/src/event-stream/message-queue.ts | 48 --- packages/pds/src/event-stream/messages.ts | 51 --- packages/pds/src/event-stream/types.ts | 38 -- packages/pds/src/index.ts | 37 +- packages/pds/src/label-cache.ts | 90 ---- packages/pds/src/labeler/base.ts | 72 ---- packages/pds/src/labeler/hive.ts | 191 --------- packages/pds/src/labeler/index.ts | 3 - packages/pds/src/labeler/keyword.ts | 29 -- packages/pds/src/labeler/util.ts | 109 ----- packages/pds/src/lexicon/index.ts | 12 - packages/pds/src/lexicon/lexicons.ts | 27 -- .../types/app/bsky/unspecced/applyLabels.ts | 39 -- packages/pds/src/services/account/index.ts | 30 +- packages/pds/src/services/index.ts | 27 +- packages/pds/src/services/moderation/index.ts | 18 +- packages/pds/src/services/moderation/views.ts | 45 +- packages/pds/src/services/record/index.ts | 30 +- packages/pds/src/services/repo/blobs.ts | 2 +- packages/pds/src/services/repo/index.ts | 31 +- packages/pds/src/services/util/search.ts | 105 +---- packages/pds/tests/_util.ts | 7 - packages/pds/tests/account-deletion.test.ts | 147 +------ .../__snapshots__/get-record.test.ts.snap | 173 -------- .../admin/__snapshots__/get-repo.test.ts.snap | 118 ------ packages/pds/tests/admin/get-record.test.ts | 25 -- packages/pds/tests/admin/get-repo.test.ts | 22 - packages/pds/tests/admin/repo-search.test.ts | 309 +------------- packages/pds/tests/duplicate-records.test.ts | 179 -------- .../pds/tests/labeler/apply-labels.test.ts | 180 -------- .../labeler/fixtures/hiveai_resp_example.json | 401 ------------------ packages/pds/tests/labeler/hive.test.ts | 16 - packages/pds/tests/labeler/labeler.test.ts | 178 -------- packages/pds/tests/moderation.test.ts | 205 +-------- packages/pds/tests/proxied/admin.test.ts | 21 +- pnpm-lock.yaml | 9 +- 111 files changed, 174 insertions(+), 5494 deletions(-) delete mode 100644 lexicons/app/bsky/unspecced/applyLabels.json delete mode 100644 packages/api/src/client/types/app/bsky/unspecced/applyLabels.ts delete mode 100644 packages/bsky/src/lexicon/types/app/bsky/unspecced/applyLabels.ts rename packages/{bsky/src/services/indexing/util.ts => common/src/dates.ts} (100%) delete mode 100644 packages/dev-env/src/bin-network.ts delete mode 100644 packages/pds/src/app-view/db/index.ts delete mode 100644 packages/pds/src/app-view/db/tables/actor-block.ts delete mode 100644 packages/pds/src/app-view/db/tables/algo.ts delete mode 100644 packages/pds/src/app-view/db/tables/duplicate-record.ts delete mode 100644 packages/pds/src/app-view/db/tables/feed-generator.ts delete mode 100644 packages/pds/src/app-view/db/tables/feed-item.ts delete mode 100644 packages/pds/src/app-view/db/tables/follow.ts delete mode 100644 packages/pds/src/app-view/db/tables/like.ts delete mode 100644 packages/pds/src/app-view/db/tables/list-item.ts delete mode 100644 packages/pds/src/app-view/db/tables/list.ts delete mode 100644 packages/pds/src/app-view/db/tables/post-agg.ts delete mode 100644 packages/pds/src/app-view/db/tables/post-embed.ts delete mode 100644 packages/pds/src/app-view/db/tables/post.ts delete mode 100644 packages/pds/src/app-view/db/tables/profile-agg.ts delete mode 100644 packages/pds/src/app-view/db/tables/profile.ts delete mode 100644 packages/pds/src/app-view/db/tables/repost.ts delete mode 100644 packages/pds/src/app-view/db/tables/subscription.ts delete mode 100644 packages/pds/src/app-view/db/tables/suggested-follow.ts delete mode 100644 packages/pds/src/app-view/db/tables/view-param.ts delete mode 100644 packages/pds/src/app-view/event-stream/consumers.ts delete mode 100644 packages/pds/src/app-view/services/indexing/index.ts delete mode 100644 packages/pds/src/app-view/services/indexing/plugins/block.ts delete mode 100644 packages/pds/src/app-view/services/indexing/plugins/feed-generator.ts delete mode 100644 packages/pds/src/app-view/services/indexing/plugins/follow.ts delete mode 100644 packages/pds/src/app-view/services/indexing/plugins/like.ts delete mode 100644 packages/pds/src/app-view/services/indexing/plugins/list-item.ts delete mode 100644 packages/pds/src/app-view/services/indexing/plugins/list.ts delete mode 100644 packages/pds/src/app-view/services/indexing/plugins/post.ts delete mode 100644 packages/pds/src/app-view/services/indexing/plugins/profile.ts delete mode 100644 packages/pds/src/app-view/services/indexing/plugins/repost.ts delete mode 100644 packages/pds/src/app-view/services/indexing/processor.ts delete mode 100644 packages/pds/src/app-view/services/indexing/util.ts delete mode 100644 packages/pds/src/app-view/services/label/index.ts rename packages/pds/src/{event-stream/background-queue.ts => background.ts} (92%) create mode 100644 packages/pds/src/db/migrations/20230922T033938477Z-remove-appview.ts delete mode 100644 packages/pds/src/db/tables/label.ts delete mode 100644 packages/pds/src/event-stream/message-queue.ts delete mode 100644 packages/pds/src/event-stream/messages.ts delete mode 100644 packages/pds/src/event-stream/types.ts delete mode 100644 packages/pds/src/label-cache.ts delete mode 100644 packages/pds/src/labeler/base.ts delete mode 100644 packages/pds/src/labeler/hive.ts delete mode 100644 packages/pds/src/labeler/index.ts delete mode 100644 packages/pds/src/labeler/keyword.ts delete mode 100644 packages/pds/src/labeler/util.ts delete mode 100644 packages/pds/src/lexicon/types/app/bsky/unspecced/applyLabels.ts delete mode 100644 packages/pds/tests/duplicate-records.test.ts delete mode 100644 packages/pds/tests/labeler/apply-labels.test.ts delete mode 100644 packages/pds/tests/labeler/fixtures/hiveai_resp_example.json delete mode 100644 packages/pds/tests/labeler/hive.test.ts delete mode 100644 packages/pds/tests/labeler/labeler.test.ts diff --git a/lexicons/app/bsky/unspecced/applyLabels.json b/lexicons/app/bsky/unspecced/applyLabels.json deleted file mode 100644 index 24c9e716ad5..00000000000 --- a/lexicons/app/bsky/unspecced/applyLabels.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "lexicon": 1, - "id": "app.bsky.unspecced.applyLabels", - "defs": { - "main": { - "type": "procedure", - "description": "Allow a labeler to apply labels directly.", - "input": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": ["labels"], - "properties": { - "labels": { - "type": "array", - "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } - } - } - } - } - } - } -} diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index 4d9ae10936b..982117cef02 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -129,7 +129,6 @@ import * as AppBskyNotificationListNotifications from './types/app/bsky/notifica import * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/registerPush' import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen' import * as AppBskyRichtextFacet from './types/app/bsky/richtext/facet' -import * as AppBskyUnspeccedApplyLabels from './types/app/bsky/unspecced/applyLabels' import * as AppBskyUnspeccedDefs from './types/app/bsky/unspecced/defs' import * as AppBskyUnspeccedGetPopular from './types/app/bsky/unspecced/getPopular' import * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators' @@ -259,7 +258,6 @@ export * as AppBskyNotificationListNotifications from './types/app/bsky/notifica export * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/registerPush' export * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen' export * as AppBskyRichtextFacet from './types/app/bsky/richtext/facet' -export * as AppBskyUnspeccedApplyLabels from './types/app/bsky/unspecced/applyLabels' export * as AppBskyUnspeccedDefs from './types/app/bsky/unspecced/defs' export * as AppBskyUnspeccedGetPopular from './types/app/bsky/unspecced/getPopular' export * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators' @@ -2253,17 +2251,6 @@ export class UnspeccedNS { this._service = service } - applyLabels( - data?: AppBskyUnspeccedApplyLabels.InputSchema, - opts?: AppBskyUnspeccedApplyLabels.CallOptions, - ): Promise { - return this._service.xrpc - .call('app.bsky.unspecced.applyLabels', opts?.qp, data, opts) - .catch((e) => { - throw AppBskyUnspeccedApplyLabels.toKnownErr(e) - }) - } - getPopular( params?: AppBskyUnspeccedGetPopular.QueryParams, opts?: AppBskyUnspeccedGetPopular.CallOptions, diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index b624ce335ed..177b63808f4 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -6905,32 +6905,6 @@ export const schemaDict = { }, }, }, - AppBskyUnspeccedApplyLabels: { - lexicon: 1, - id: 'app.bsky.unspecced.applyLabels', - defs: { - main: { - type: 'procedure', - description: 'Allow a labeler to apply labels directly.', - input: { - encoding: 'application/json', - schema: { - type: 'object', - required: ['labels'], - properties: { - labels: { - type: 'array', - items: { - type: 'ref', - ref: 'lex:com.atproto.label.defs#label', - }, - }, - }, - }, - }, - }, - }, - }, AppBskyUnspeccedDefs: { lexicon: 1, id: 'app.bsky.unspecced.defs', @@ -7359,7 +7333,6 @@ export const ids = { AppBskyNotificationRegisterPush: 'app.bsky.notification.registerPush', AppBskyNotificationUpdateSeen: 'app.bsky.notification.updateSeen', AppBskyRichtextFacet: 'app.bsky.richtext.facet', - AppBskyUnspeccedApplyLabels: 'app.bsky.unspecced.applyLabels', AppBskyUnspeccedDefs: 'app.bsky.unspecced.defs', AppBskyUnspeccedGetPopular: 'app.bsky.unspecced.getPopular', AppBskyUnspeccedGetPopularFeedGenerators: diff --git a/packages/api/src/client/types/app/bsky/unspecced/applyLabels.ts b/packages/api/src/client/types/app/bsky/unspecced/applyLabels.ts deleted file mode 100644 index c8e72746a42..00000000000 --- a/packages/api/src/client/types/app/bsky/unspecced/applyLabels.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * GENERATED CODE - DO NOT MODIFY - */ -import { Headers, XRPCError } from '@atproto/xrpc' -import { ValidationResult, BlobRef } from '@atproto/lexicon' -import { isObj, hasProp } from '../../../../util' -import { lexicons } from '../../../../lexicons' -import { CID } from 'multiformats/cid' -import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' - -export interface QueryParams {} - -export interface InputSchema { - labels: ComAtprotoLabelDefs.Label[] - [k: string]: unknown -} - -export interface CallOptions { - headers?: Headers - qp?: QueryParams - encoding: 'application/json' -} - -export interface Response { - success: boolean - headers: Headers -} - -export function toKnownErr(e: any) { - if (e instanceof XRPCError) { - } - return e -} diff --git a/packages/bsky/package.json b/packages/bsky/package.json index 33cafe53947..5281805279f 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -50,7 +50,6 @@ "http-errors": "^2.0.0", "http-terminator": "^3.2.0", "ioredis": "^5.3.2", - "iso-datestring-validator": "^2.2.2", "kysely": "^0.22.0", "multiformats": "^9.9.0", "p-queue": "^6.6.2", diff --git a/packages/bsky/src/auto-moderator/index.ts b/packages/bsky/src/auto-moderator/index.ts index 1be099759f1..30befc19110 100644 --- a/packages/bsky/src/auto-moderator/index.ts +++ b/packages/bsky/src/auto-moderator/index.ts @@ -280,28 +280,12 @@ export class AutoModerator { async storeLabels(uri: AtUri, cid: CID, labels: string[]): Promise { if (labels.length < 1) return const labelSrvc = this.services.label(this.ctx.db) - const formatted = await labelSrvc.formatAndCreate( + await labelSrvc.formatAndCreate( this.ctx.cfg.labelerDid, uri.toString(), cid.toString(), { create: labels }, ) - if (this.pushAgent) { - const agent = this.pushAgent - try { - await agent.api.app.bsky.unspecced.applyLabels({ labels: formatted }) - } catch (err) { - log.error( - { - err, - uri: uri.toString(), - labels, - receiver: agent.service.toString(), - }, - 'failed to push labels', - ) - } - } } async processAll() { diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index 3ec69a3503f..3dd2b5104cc 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -107,7 +107,6 @@ import * as AppBskyNotificationGetUnreadCount from './types/app/bsky/notificatio import * as AppBskyNotificationListNotifications from './types/app/bsky/notification/listNotifications' import * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/registerPush' import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen' -import * as AppBskyUnspeccedApplyLabels from './types/app/bsky/unspecced/applyLabels' import * as AppBskyUnspeccedGetPopular from './types/app/bsky/unspecced/getPopular' import * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators' import * as AppBskyUnspeccedGetTimelineSkeleton from './types/app/bsky/unspecced/getTimelineSkeleton' @@ -1402,17 +1401,6 @@ export class UnspeccedNS { this._server = server } - applyLabels( - cfg: ConfigOf< - AV, - AppBskyUnspeccedApplyLabels.Handler>, - AppBskyUnspeccedApplyLabels.HandlerReqCtx> - >, - ) { - const nsid = 'app.bsky.unspecced.applyLabels' // @ts-ignore - return this._server.xrpc.method(nsid, cfg) - } - getPopular( cfg: ConfigOf< AV, diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index b624ce335ed..177b63808f4 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -6905,32 +6905,6 @@ export const schemaDict = { }, }, }, - AppBskyUnspeccedApplyLabels: { - lexicon: 1, - id: 'app.bsky.unspecced.applyLabels', - defs: { - main: { - type: 'procedure', - description: 'Allow a labeler to apply labels directly.', - input: { - encoding: 'application/json', - schema: { - type: 'object', - required: ['labels'], - properties: { - labels: { - type: 'array', - items: { - type: 'ref', - ref: 'lex:com.atproto.label.defs#label', - }, - }, - }, - }, - }, - }, - }, - }, AppBskyUnspeccedDefs: { lexicon: 1, id: 'app.bsky.unspecced.defs', @@ -7359,7 +7333,6 @@ export const ids = { AppBskyNotificationRegisterPush: 'app.bsky.notification.registerPush', AppBskyNotificationUpdateSeen: 'app.bsky.notification.updateSeen', AppBskyRichtextFacet: 'app.bsky.richtext.facet', - AppBskyUnspeccedApplyLabels: 'app.bsky.unspecced.applyLabels', AppBskyUnspeccedDefs: 'app.bsky.unspecced.defs', AppBskyUnspeccedGetPopular: 'app.bsky.unspecced.getPopular', AppBskyUnspeccedGetPopularFeedGenerators: diff --git a/packages/bsky/src/lexicon/types/app/bsky/unspecced/applyLabels.ts b/packages/bsky/src/lexicon/types/app/bsky/unspecced/applyLabels.ts deleted file mode 100644 index 1d359a9547d..00000000000 --- a/packages/bsky/src/lexicon/types/app/bsky/unspecced/applyLabels.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * GENERATED CODE - DO NOT MODIFY - */ -import express from 'express' -import { ValidationResult, BlobRef } from '@atproto/lexicon' -import { lexicons } from '../../../../lexicons' -import { isObj, hasProp } from '../../../../util' -import { CID } from 'multiformats/cid' -import { HandlerAuth } from '@atproto/xrpc-server' -import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' - -export interface QueryParams {} - -export interface InputSchema { - labels: ComAtprotoLabelDefs.Label[] - [k: string]: unknown -} - -export interface HandlerInput { - encoding: 'application/json' - body: InputSchema -} - -export interface HandlerError { - status: number - message?: string -} - -export type HandlerOutput = HandlerError | void -export type HandlerReqCtx = { - auth: HA - params: QueryParams - input: HandlerInput - req: express.Request - res: express.Response -} -export type Handler = ( - ctx: HandlerReqCtx, -) => Promise | HandlerOutput diff --git a/packages/bsky/src/services/indexing/plugins/block.ts b/packages/bsky/src/services/indexing/plugins/block.ts index c0c8fe9d770..bf8ae9e5029 100644 --- a/packages/bsky/src/services/indexing/plugins/block.ts +++ b/packages/bsky/src/services/indexing/plugins/block.ts @@ -1,11 +1,11 @@ import { Selectable } from 'kysely' import { AtUri } from '@atproto/syntax' +import { toSimplifiedISOSafe } from '@atproto/common' import { CID } from 'multiformats/cid' import * as Block from '../../../lexicon/types/app/bsky/graph/block' import * as lex from '../../../lexicon/lexicons' import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' import RecordProcessor from '../processor' -import { toSimplifiedISOSafe } from '../util' import { PrimaryDatabase } from '../../../db' import { BackgroundQueue } from '../../../background' import { NotificationServer } from '../../../notifications' diff --git a/packages/bsky/src/services/indexing/plugins/feed-generator.ts b/packages/bsky/src/services/indexing/plugins/feed-generator.ts index a0c32ff9129..e4ae5eb4f5a 100644 --- a/packages/bsky/src/services/indexing/plugins/feed-generator.ts +++ b/packages/bsky/src/services/indexing/plugins/feed-generator.ts @@ -1,5 +1,6 @@ import { Selectable } from 'kysely' import { AtUri } from '@atproto/syntax' +import { toSimplifiedISOSafe } from '@atproto/common' import { CID } from 'multiformats/cid' import * as FeedGenerator from '../../../lexicon/types/app/bsky/feed/generator' import * as lex from '../../../lexicon/lexicons' @@ -7,7 +8,6 @@ import { PrimaryDatabase } from '../../../db' import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' import { BackgroundQueue } from '../../../background' import RecordProcessor from '../processor' -import { toSimplifiedISOSafe } from '../util' import { NotificationServer } from '../../../notifications' const lexId = lex.ids.AppBskyFeedGenerator diff --git a/packages/bsky/src/services/indexing/plugins/follow.ts b/packages/bsky/src/services/indexing/plugins/follow.ts index d6cf1996f98..e9a344db2fd 100644 --- a/packages/bsky/src/services/indexing/plugins/follow.ts +++ b/packages/bsky/src/services/indexing/plugins/follow.ts @@ -1,11 +1,11 @@ import { Selectable } from 'kysely' import { AtUri } from '@atproto/syntax' +import { toSimplifiedISOSafe } from '@atproto/common' import { CID } from 'multiformats/cid' import * as Follow from '../../../lexicon/types/app/bsky/graph/follow' import * as lex from '../../../lexicon/lexicons' import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' import RecordProcessor from '../processor' -import { toSimplifiedISOSafe } from '../util' import { PrimaryDatabase } from '../../../db' import { countAll, excluded } from '../../../db/util' import { BackgroundQueue } from '../../../background' diff --git a/packages/bsky/src/services/indexing/plugins/like.ts b/packages/bsky/src/services/indexing/plugins/like.ts index 7899b48b1fd..01e0fa5c4fd 100644 --- a/packages/bsky/src/services/indexing/plugins/like.ts +++ b/packages/bsky/src/services/indexing/plugins/like.ts @@ -1,11 +1,11 @@ import { Selectable } from 'kysely' import { AtUri } from '@atproto/syntax' +import { toSimplifiedISOSafe } from '@atproto/common' import { CID } from 'multiformats/cid' import * as Like from '../../../lexicon/types/app/bsky/feed/like' import * as lex from '../../../lexicon/lexicons' import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' import RecordProcessor from '../processor' -import { toSimplifiedISOSafe } from '../util' import { countAll, excluded } from '../../../db/util' import { PrimaryDatabase } from '../../../db' import { BackgroundQueue } from '../../../background' diff --git a/packages/bsky/src/services/indexing/plugins/list-block.ts b/packages/bsky/src/services/indexing/plugins/list-block.ts index 4285ca8d4bc..33dc7cfc51a 100644 --- a/packages/bsky/src/services/indexing/plugins/list-block.ts +++ b/packages/bsky/src/services/indexing/plugins/list-block.ts @@ -1,5 +1,6 @@ import { Selectable } from 'kysely' import { AtUri } from '@atproto/syntax' +import { toSimplifiedISOSafe } from '@atproto/common' import { CID } from 'multiformats/cid' import * as ListBlock from '../../../lexicon/types/app/bsky/graph/listblock' import * as lex from '../../../lexicon/lexicons' @@ -8,7 +9,6 @@ import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' import RecordProcessor from '../processor' import { BackgroundQueue } from '../../../background' import { NotificationServer } from '../../../notifications' -import { toSimplifiedISOSafe } from '../util' const lexId = lex.ids.AppBskyGraphListblock type IndexedListBlock = Selectable diff --git a/packages/bsky/src/services/indexing/plugins/list-item.ts b/packages/bsky/src/services/indexing/plugins/list-item.ts index 231fb761e16..2ab125062a7 100644 --- a/packages/bsky/src/services/indexing/plugins/list-item.ts +++ b/packages/bsky/src/services/indexing/plugins/list-item.ts @@ -1,11 +1,11 @@ import { Selectable } from 'kysely' import { AtUri } from '@atproto/syntax' +import { toSimplifiedISOSafe } from '@atproto/common' import { CID } from 'multiformats/cid' import * as ListItem from '../../../lexicon/types/app/bsky/graph/listitem' import * as lex from '../../../lexicon/lexicons' import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' import RecordProcessor from '../processor' -import { toSimplifiedISOSafe } from '../util' import { InvalidRequestError } from '@atproto/xrpc-server' import { PrimaryDatabase } from '../../../db' import { BackgroundQueue } from '../../../background' diff --git a/packages/bsky/src/services/indexing/plugins/list.ts b/packages/bsky/src/services/indexing/plugins/list.ts index c74c09c274f..293c457c4fb 100644 --- a/packages/bsky/src/services/indexing/plugins/list.ts +++ b/packages/bsky/src/services/indexing/plugins/list.ts @@ -1,11 +1,11 @@ import { Selectable } from 'kysely' import { AtUri } from '@atproto/syntax' +import { toSimplifiedISOSafe } from '@atproto/common' import { CID } from 'multiformats/cid' import * as List from '../../../lexicon/types/app/bsky/graph/list' import * as lex from '../../../lexicon/lexicons' import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' import RecordProcessor from '../processor' -import { toSimplifiedISOSafe } from '../util' import { PrimaryDatabase } from '../../../db' import { BackgroundQueue } from '../../../background' import { NotificationServer } from '../../../notifications' diff --git a/packages/bsky/src/services/indexing/plugins/post.ts b/packages/bsky/src/services/indexing/plugins/post.ts index 40835348f01..7173c04a991 100644 --- a/packages/bsky/src/services/indexing/plugins/post.ts +++ b/packages/bsky/src/services/indexing/plugins/post.ts @@ -1,6 +1,7 @@ import { Insertable, Selectable, sql } from 'kysely' import { CID } from 'multiformats/cid' import { AtUri } from '@atproto/syntax' +import { toSimplifiedISOSafe } from '@atproto/common' import { jsonStringToLex } from '@atproto/lexicon' import { Record as PostRecord, @@ -19,7 +20,6 @@ import * as lex from '../../../lexicon/lexicons' import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' import RecordProcessor from '../processor' import { Notification } from '../../../db/tables/notification' -import { toSimplifiedISOSafe } from '../util' import { PrimaryDatabase } from '../../../db' import { countAll, excluded } from '../../../db/util' import { BackgroundQueue } from '../../../background' diff --git a/packages/bsky/src/services/indexing/plugins/repost.ts b/packages/bsky/src/services/indexing/plugins/repost.ts index aa93d7b0f61..9c46b9b3376 100644 --- a/packages/bsky/src/services/indexing/plugins/repost.ts +++ b/packages/bsky/src/services/indexing/plugins/repost.ts @@ -1,11 +1,11 @@ import { Selectable } from 'kysely' import { CID } from 'multiformats/cid' import { AtUri } from '@atproto/syntax' +import { toSimplifiedISOSafe } from '@atproto/common' import * as Repost from '../../../lexicon/types/app/bsky/feed/repost' import * as lex from '../../../lexicon/lexicons' import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' import RecordProcessor from '../processor' -import { toSimplifiedISOSafe } from '../util' import { PrimaryDatabase } from '../../../db' import { countAll, excluded } from '../../../db/util' import { BackgroundQueue } from '../../../background' diff --git a/packages/bsky/src/services/indexing/plugins/thread-gate.ts b/packages/bsky/src/services/indexing/plugins/thread-gate.ts index fb0928f2459..37f3ddb062e 100644 --- a/packages/bsky/src/services/indexing/plugins/thread-gate.ts +++ b/packages/bsky/src/services/indexing/plugins/thread-gate.ts @@ -1,11 +1,11 @@ import { AtUri } from '@atproto/syntax' import { InvalidRequestError } from '@atproto/xrpc-server' +import { toSimplifiedISOSafe } from '@atproto/common' import { CID } from 'multiformats/cid' import * as Threadgate from '../../../lexicon/types/app/bsky/feed/threadgate' import * as lex from '../../../lexicon/lexicons' import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' import RecordProcessor from '../processor' -import { toSimplifiedISOSafe } from '../util' import { PrimaryDatabase } from '../../../db' import { BackgroundQueue } from '../../../background' import { NotificationServer } from '../../../notifications' diff --git a/packages/bsky/src/services/label/index.ts b/packages/bsky/src/services/label/index.ts index 855038ab14c..7d351b95011 100644 --- a/packages/bsky/src/services/label/index.ts +++ b/packages/bsky/src/services/label/index.ts @@ -1,9 +1,9 @@ import { sql } from 'kysely' import { AtUri } from '@atproto/syntax' +import { toSimplifiedISOSafe } from '@atproto/common' import { Database } from '../../db' import { Label, isSelfLabels } from '../../lexicon/types/com/atproto/label/defs' import { ids } from '../../lexicon/lexicons' -import { toSimplifiedISOSafe } from '../indexing/util' import { LabelCache } from '../../label-cache' export type Labels = Record diff --git a/packages/common/package.json b/packages/common/package.json index 4794bdae698..5b88803035f 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -27,6 +27,7 @@ "@atproto/common-web": "workspace:^", "@ipld/dag-cbor": "^7.0.3", "cbor-x": "^1.5.1", + "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.15.0", "zod": "3.21.4" diff --git a/packages/bsky/src/services/indexing/util.ts b/packages/common/src/dates.ts similarity index 100% rename from packages/bsky/src/services/indexing/util.ts rename to packages/common/src/dates.ts diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index fd049fc2e5d..524a090c5ab 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1,4 +1,5 @@ export * from '@atproto/common-web' +export * from './dates' export * from './fs' export * from './ipld' export * from './ipld-multi' diff --git a/packages/dev-env/build.js b/packages/dev-env/build.js index 98bb2df6133..60634cf4503 100644 --- a/packages/dev-env/build.js +++ b/packages/dev-env/build.js @@ -6,7 +6,7 @@ const buildShallow = require('esbuild').build({ logLevel: 'info', - entryPoints: ['src/index.ts', 'src/bin.ts', 'src/bin-network.ts'], + entryPoints: ['src/index.ts', 'src/bin.ts'], bundle: true, sourcemap: true, outdir: 'dist', diff --git a/packages/dev-env/package.json b/packages/dev-env/package.json index 926ff0b412d..6ae8cc5d151 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -22,8 +22,7 @@ "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", "update-main-to-dist": "node ../../update-main-to-dist.js packages/dev-env", - "start": "node dist/bin.js", - "start:network": "../dev-infra/with-test-redis-and-db.sh node dist/bin-network.js" + "start": "../dev-infra/with-test-redis-and-db.sh node dist/bin.js" }, "dependencies": { "@atproto/api": "workspace:^", diff --git a/packages/dev-env/src/bin-network.ts b/packages/dev-env/src/bin-network.ts deleted file mode 100644 index 193c7ea968a..00000000000 --- a/packages/dev-env/src/bin-network.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { generateMockSetup } from './mock' -import { TestNetwork } from './network' - -const run = async () => { - console.log(` -██████╗ -██╔═══██╗ -██║██╗██║ -██║██║██║ -╚█║████╔╝ - ╚╝╚═══╝ protocol - -[ created by Bluesky ]`) - - const network = await TestNetwork.create({ - pds: { - port: 2583, - publicUrl: 'http://localhost:2583', - enableLabelsCache: true, - dbPostgresSchema: 'pds', - }, - bsky: { - dbPostgresSchema: 'bsky', - }, - plc: { port: 2582 }, - }) - await generateMockSetup(network) - - console.log( - `👤 DID Placeholder server started http://localhost:${network.plc.port}`, - ) - console.log( - `🌞 Personal Data server started http://localhost:${network.pds.port}`, - ) - console.log(`🌅 Bsky Appview started http://localhost:${network.bsky.port}`) - for (const fg of network.feedGens) { - console.log(`🤖 Feed Generator started http://localhost:${fg.port}`) - } -} - -run() diff --git a/packages/dev-env/src/bin.ts b/packages/dev-env/src/bin.ts index 532265e18ec..56f0138b3b2 100644 --- a/packages/dev-env/src/bin.ts +++ b/packages/dev-env/src/bin.ts @@ -1,5 +1,5 @@ import { generateMockSetup } from './mock' -import { TestNetworkNoAppView } from './network-no-appview' +import { TestNetwork } from './network' const run = async () => { console.log(` @@ -12,11 +12,14 @@ const run = async () => { [ created by Bluesky ]`) - const network = await TestNetworkNoAppView.create({ + const network = await TestNetwork.create({ pds: { port: 2583, - enableLabelsCache: true, publicUrl: 'http://localhost:2583', + dbPostgresSchema: 'pds', + }, + bsky: { + dbPostgresSchema: 'bsky', }, plc: { port: 2582 }, }) @@ -28,6 +31,7 @@ const run = async () => { console.log( `🌞 Personal Data server started http://localhost:${network.pds.port}`, ) + console.log(`🌅 Bsky Appview started http://localhost:${network.bsky.port}`) for (const fg of network.feedGens) { console.log(`🤖 Feed Generator started http://localhost:${fg.port}`) } diff --git a/packages/dev-env/src/mock/index.ts b/packages/dev-env/src/mock/index.ts index afa661f7ecb..10f76b1c259 100644 --- a/packages/dev-env/src/mock/index.ts +++ b/packages/dev-env/src/mock/index.ts @@ -4,7 +4,7 @@ import { REASONSPAM, REASONOTHER, } from '@atproto/api/src/client/types/com/atproto/moderation/defs' -import { TestNetworkNoAppView } from '../index' +import { TestNetwork } from '../index' import { postTexts, replyTexts } from './data' import labeledImgB64 from './img/labeled-img-b64' import blurHashB64 from './img/blur-hash-avatar-b64' @@ -23,7 +23,7 @@ function* dateGen() { return '' } -export async function generateMockSetup(env: TestNetworkNoAppView) { +export async function generateMockSetup(env: TestNetwork) { const date = dateGen() const rand = (n: number) => Math.floor(Math.random() * n) @@ -186,29 +186,27 @@ export async function generateMockSetup(env: TestNetworkNoAppView) { }, ) - const ctx = env.pds.ctx + const ctx = env.bsky.ctx if (ctx) { - await ctx.db.db - .insertInto('label') - .values([ - { - src: ctx.cfg.labelerDid, - uri: labeledPost.uri, - cid: labeledPost.cid, - val: 'nudity', - neg: 0, - cts: new Date().toISOString(), - }, - { - src: ctx.cfg.labelerDid, - uri: filteredPost.uri, - cid: filteredPost.cid, - val: 'dmca-violation', - neg: 0, - cts: new Date().toISOString(), - }, - ]) - .execute() + const labelSrvc = ctx.services.label(ctx.db.getPrimary()) + await labelSrvc.createLabels([ + { + src: ctx.cfg.labelerDid, + uri: labeledPost.uri, + cid: labeledPost.cid, + val: 'nudity', + neg: false, + cts: new Date().toISOString(), + }, + { + src: ctx.cfg.labelerDid, + uri: filteredPost.uri, + cid: filteredPost.cid, + val: 'dmca-violation', + neg: false, + cts: new Date().toISOString(), + }, + ]) } // a set of replies diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index e94130f41ff..2cdcadfe70c 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -2,7 +2,6 @@ import getPort from 'get-port' import * as ui8 from 'uint8arrays' import * as pds from '@atproto/pds' import { Secp256k1Keypair, randomStr } from '@atproto/crypto' -import { MessageDispatcher } from '@atproto/pds/src/event-stream/message-queue' import { AtpAgent } from '@atproto/api' import { Client as PlcClient } from '@did-plc/lib' import { DAY, HOUR } from '@atproto/common-web' @@ -60,9 +59,6 @@ export class TestPds { maxSubscriptionBuffer: 200, repoBackfillLimitMs: 1000 * 60 * 60, // 1hr sequencerLeaderLockId: uniqueLockId(), - labelerDid: 'did:example:labeler', - labelerKeywords: { label_me: 'test-label', label_me_2: 'test-label-2' }, - feedGenDid: 'did:example:feedGen', dbTxLockNonce: await randomStr(32, 'base32'), bskyAppViewEndpoint: cfg.bskyAppViewEndpoint ?? 'http://fake_address', bskyAppViewDid: cfg.bskyAppViewDid ?? 'did:example:fake', @@ -80,11 +76,6 @@ export class TestPds { : pds.Database.memory() await db.migrateToLatestOrThrow() - if (cfg.bskyAppViewEndpoint && !cfg.enableInProcessAppView) { - // Disable communication to app view within pds - MessageDispatcher.prototype.send = async () => {} - } - const server = pds.PDS.create({ db, blobstore, @@ -95,8 +86,6 @@ export class TestPds { await server.start() - // we refresh label cache by hand in `processAll` instead of on a timer - if (!cfg.enableLabelsCache) server.ctx.labelCache.stop() return new TestPds(url, port, server) } @@ -129,7 +118,6 @@ export class TestPds { async processAll() { await this.ctx.backgroundQueue.processAll() - await this.ctx.labelCache.fullRefresh() } async close() { diff --git a/packages/dev-env/src/types.ts b/packages/dev-env/src/types.ts index d87e78a679a..a1642f4751c 100644 --- a/packages/dev-env/src/types.ts +++ b/packages/dev-env/src/types.ts @@ -10,8 +10,6 @@ export type PlcConfig = { export type PdsConfig = Partial & { plcUrl: string migration?: string - enableInProcessAppView?: boolean - enableLabelsCache?: boolean } export type BskyConfig = Partial & { diff --git a/packages/pds/package.json b/packages/pds/package.json index 833b016f4ef..a7d98578f35 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -56,7 +56,6 @@ "http-errors": "^2.0.0", "http-terminator": "^3.2.0", "ioredis": "^5.3.2", - "iso-datestring-validator": "^2.2.2", "jsonwebtoken": "^8.5.1", "kysely": "^0.22.0", "multiformats": "^9.9.0", diff --git a/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts b/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts index d652b501166..b0d1e149790 100644 --- a/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts +++ b/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts @@ -26,7 +26,6 @@ export default function (server: Server, ctx: AppContext) { const transact = db.transaction(async (dbTxn) => { const moderationTxn = services.moderation(dbTxn) - const labelTxn = services.appView.label(dbTxn) // reverse takedowns if (result.action === TAKEDOWN && isRepoRef(result.subject)) { await moderationTxn.reverseTakedownRepo({ @@ -38,18 +37,6 @@ export default function (server: Server, ctx: AppContext) { uri: new AtUri(result.subject.uri), }) } - // invert label creation & negations - const reverseLabels = (uri: string, cid: string | null) => - labelTxn.formatAndCreate(ctx.cfg.labelerDid, uri, cid, { - create: result.negateLabelVals, - negate: result.createLabelVals, - }) - if (isRepoRef(result.subject)) { - await reverseLabels(result.subject.did, null) - } - if (isStrongRef(result.subject)) { - await reverseLabels(result.subject.uri, result.subject.cid) - } }) try { @@ -72,7 +59,6 @@ export default function (server: Server, ctx: AppContext) { const moderationAction = await db.transaction(async (dbTxn) => { const moderationTxn = services.moderation(dbTxn) - const labelTxn = services.appView.label(dbTxn) const now = new Date() const existing = await moderationTxn.getAction(id) @@ -114,23 +100,6 @@ export default function (server: Server, ctx: AppContext) { reason, }) - // invert creates & negates - const { createLabelVals, negateLabelVals } = result - const negate = - createLabelVals && createLabelVals.length > 0 - ? createLabelVals.split(' ') - : undefined - const create = - negateLabelVals && negateLabelVals.length > 0 - ? negateLabelVals.split(' ') - : undefined - await labelTxn.formatAndCreate( - ctx.cfg.labelerDid, - result.subjectUri ?? result.subjectDid, - result.subjectCid, - { create, negate }, - ) - return result }) diff --git a/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts b/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts index 8a81245a9e8..e9fb9b7e043 100644 --- a/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts +++ b/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts @@ -29,7 +29,6 @@ export default function (server: Server, ctx: AppContext) { const transact = db.transaction(async (dbTxn) => { const authTxn = services.auth(dbTxn) const moderationTxn = services.moderation(dbTxn) - const labelTxn = services.appView.label(dbTxn) // perform takedowns if (result.action === TAKEDOWN && isRepoRef(result.subject)) { await authTxn.revokeRefreshTokensByDid(result.subject.did) @@ -45,18 +44,6 @@ export default function (server: Server, ctx: AppContext) { blobCids: result.subjectBlobCids.map((cid) => CID.parse(cid)), }) } - // apply label creation & negations - const applyLabels = (uri: string, cid: string | null) => - labelTxn.formatAndCreate(ctx.cfg.labelerDid, uri, cid, { - create: result.createLabelVals, - negate: result.negateLabelVals, - }) - if (isRepoRef(result.subject)) { - await applyLabels(result.subject.did, null) - } - if (isStrongRef(result.subject)) { - await applyLabels(result.subject.uri, result.subject.cid) - } }) try { @@ -113,7 +100,6 @@ export default function (server: Server, ctx: AppContext) { const moderationAction = await db.transaction(async (dbTxn) => { const authTxn = services.auth(dbTxn) const moderationTxn = services.moderation(dbTxn) - const labelTxn = services.appView.label(dbTxn) const result = await moderationTxn.logAction({ action: getAction(action), @@ -150,13 +136,6 @@ export default function (server: Server, ctx: AppContext) { }) } - await labelTxn.formatAndCreate( - ctx.cfg.labelerDid, - result.subjectUri ?? result.subjectDid, - result.subjectCid, - { create: createLabelVals, negate: negateLabelVals }, - ) - return result }) diff --git a/packages/pds/src/app-view/api/app/bsky/unspecced.ts b/packages/pds/src/app-view/api/app/bsky/unspecced.ts index 482adb3dbd8..8708b8fe92f 100644 --- a/packages/pds/src/app-view/api/app/bsky/unspecced.ts +++ b/packages/pds/src/app-view/api/app/bsky/unspecced.ts @@ -1,4 +1,4 @@ -import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' +import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import { GenericKeyset } from '../../../../db/pagination' import AppContext from '../../../../context' @@ -20,11 +20,6 @@ export default function (server: Server, ctx: AppContext) { encoding: 'application/json', body: res.data, } - - return { - encoding: 'application/json', - body: { feed: [] }, - } }, }) @@ -43,18 +38,6 @@ export default function (server: Server, ctx: AppContext) { } }, }) - - server.app.bsky.unspecced.applyLabels({ - auth: ctx.roleVerifier, - handler: async ({ auth, input }) => { - if (!auth.credentials.admin) { - throw new AuthRequiredError('Insufficient privileges') - } - const { services, db } = ctx - const { labels } = input.body - await services.appView.label(db).createLabels(labels) - }, - }) } type Result = { likeCount: number; cid: string } diff --git a/packages/pds/src/app-view/db/index.ts b/packages/pds/src/app-view/db/index.ts deleted file mode 100644 index 21edd709a7a..00000000000 --- a/packages/pds/src/app-view/db/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as duplicateRecords from './tables/duplicate-record' -import * as profile from './tables/profile' -import * as profileAgg from './tables/profile-agg' -import * as post from './tables/post' -import * as postAgg from './tables/post-agg' -import * as postEmbed from './tables/post-embed' -import * as repost from './tables/repost' -import * as feedItem from './tables/feed-item' -import * as follow from './tables/follow' -import * as list from './tables/list' -import * as listItem from './tables/list-item' -import * as actorBlock from './tables/actor-block' -import * as like from './tables/like' -import * as feedGenerator from './tables/feed-generator' -import * as subscription from './tables/subscription' -import * as algo from './tables/algo' -import * as viewParam from './tables/view-param' -import * as suggestedFollow from './tables/suggested-follow' - -// @NOTE app-view also shares did-handle, record, and repo-root tables w/ main pds -export type DatabaseSchemaType = duplicateRecords.PartialDB & - profile.PartialDB & - profileAgg.PartialDB & - post.PartialDB & - postAgg.PartialDB & - postEmbed.PartialDB & - repost.PartialDB & - feedItem.PartialDB & - follow.PartialDB & - list.PartialDB & - listItem.PartialDB & - actorBlock.PartialDB & - like.PartialDB & - feedGenerator.PartialDB & - subscription.PartialDB & - algo.PartialDB & - viewParam.PartialDB & - suggestedFollow.PartialDB diff --git a/packages/pds/src/app-view/db/tables/actor-block.ts b/packages/pds/src/app-view/db/tables/actor-block.ts deleted file mode 100644 index 8923e921071..00000000000 --- a/packages/pds/src/app-view/db/tables/actor-block.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const tableName = 'actor_block' -export interface ActorBlock { - uri: string - cid: string - creator: string - subjectDid: string - createdAt: string - indexedAt: string -} - -export type PartialDB = { [tableName]: ActorBlock } diff --git a/packages/pds/src/app-view/db/tables/algo.ts b/packages/pds/src/app-view/db/tables/algo.ts deleted file mode 100644 index e38a64c4a69..00000000000 --- a/packages/pds/src/app-view/db/tables/algo.ts +++ /dev/null @@ -1,12 +0,0 @@ -// @NOTE postgres-only -export const whatsHotViewTableName = 'algo_whats_hot_view' - -export interface AlgoWhatsHotView { - uri: string - cid: string - score: number -} - -export type PartialDB = { - [whatsHotViewTableName]: AlgoWhatsHotView -} diff --git a/packages/pds/src/app-view/db/tables/duplicate-record.ts b/packages/pds/src/app-view/db/tables/duplicate-record.ts deleted file mode 100644 index 3cad0cd148b..00000000000 --- a/packages/pds/src/app-view/db/tables/duplicate-record.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface DuplicateRecord { - uri: string - cid: string - duplicateOf: string - indexedAt: string -} - -export const tableName = 'duplicate_record' - -export type PartialDB = { - [tableName]: DuplicateRecord -} diff --git a/packages/pds/src/app-view/db/tables/feed-generator.ts b/packages/pds/src/app-view/db/tables/feed-generator.ts deleted file mode 100644 index 0b33f05cb12..00000000000 --- a/packages/pds/src/app-view/db/tables/feed-generator.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const tableName = 'feed_generator' - -export interface FeedGenerator { - uri: string - cid: string - creator: string - feedDid: string - displayName: string - description: string | null - descriptionFacets: string | null - avatarCid: string | null - createdAt: string - indexedAt: string -} - -export type PartialDB = { - [tableName]: FeedGenerator -} diff --git a/packages/pds/src/app-view/db/tables/feed-item.ts b/packages/pds/src/app-view/db/tables/feed-item.ts deleted file mode 100644 index 2fb7f867311..00000000000 --- a/packages/pds/src/app-view/db/tables/feed-item.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const tableName = 'feed_item' - -export interface FeedItem { - uri: string - cid: string - type: 'post' | 'repost' - postUri: string - originatorDid: string - sortAt: string -} - -export type PartialDB = { [tableName]: FeedItem } diff --git a/packages/pds/src/app-view/db/tables/follow.ts b/packages/pds/src/app-view/db/tables/follow.ts deleted file mode 100644 index b6ba34aea43..00000000000 --- a/packages/pds/src/app-view/db/tables/follow.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const tableName = 'follow' -export interface Follow { - uri: string - cid: string - creator: string - subjectDid: string - createdAt: string - indexedAt: string -} - -export type PartialDB = { [tableName]: Follow } diff --git a/packages/pds/src/app-view/db/tables/like.ts b/packages/pds/src/app-view/db/tables/like.ts deleted file mode 100644 index 48edac21fde..00000000000 --- a/packages/pds/src/app-view/db/tables/like.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface Like { - uri: string - cid: string - creator: string - subject: string - subjectCid: string - createdAt: string - indexedAt: string -} - -const tableName = 'like' - -export type PartialDB = { [tableName]: Like } diff --git a/packages/pds/src/app-view/db/tables/list-item.ts b/packages/pds/src/app-view/db/tables/list-item.ts deleted file mode 100644 index 7e70e718169..00000000000 --- a/packages/pds/src/app-view/db/tables/list-item.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const tableName = 'list_item' - -export interface ListItem { - uri: string - cid: string - creator: string - subjectDid: string - listUri: string - createdAt: string - indexedAt: string -} - -export type PartialDB = { [tableName]: ListItem } diff --git a/packages/pds/src/app-view/db/tables/list.ts b/packages/pds/src/app-view/db/tables/list.ts deleted file mode 100644 index d2db17574d2..00000000000 --- a/packages/pds/src/app-view/db/tables/list.ts +++ /dev/null @@ -1,16 +0,0 @@ -export const tableName = 'list' - -export interface List { - uri: string - cid: string - creator: string - name: string - purpose: string - description: string | null - descriptionFacets: string | null - avatarCid: string | null - createdAt: string - indexedAt: string -} - -export type PartialDB = { [tableName]: List } diff --git a/packages/pds/src/app-view/db/tables/post-agg.ts b/packages/pds/src/app-view/db/tables/post-agg.ts deleted file mode 100644 index 5341347403d..00000000000 --- a/packages/pds/src/app-view/db/tables/post-agg.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Generated } from 'kysely' - -export const tableName = 'post_agg' - -export interface PostAgg { - uri: string - likeCount: Generated - replyCount: Generated - repostCount: Generated -} - -export type PartialDB = { - [tableName]: PostAgg -} diff --git a/packages/pds/src/app-view/db/tables/post-embed.ts b/packages/pds/src/app-view/db/tables/post-embed.ts deleted file mode 100644 index 69c44efe718..00000000000 --- a/packages/pds/src/app-view/db/tables/post-embed.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const imageTableName = 'post_embed_image' -export const externalTableName = 'post_embed_external' -export const recordTableName = 'post_embed_record' - -export interface PostEmbedImage { - postUri: string - position: number - imageCid: string - alt: string -} - -export interface PostEmbedExternal { - postUri: string - uri: string - title: string - description: string - thumbCid: string | null -} - -export interface PostEmbedRecord { - postUri: string - embedUri: string - embedCid: string -} - -export type PartialDB = { - [imageTableName]: PostEmbedImage - [externalTableName]: PostEmbedExternal - [recordTableName]: PostEmbedRecord -} diff --git a/packages/pds/src/app-view/db/tables/post.ts b/packages/pds/src/app-view/db/tables/post.ts deleted file mode 100644 index ce7c2b7acbe..00000000000 --- a/packages/pds/src/app-view/db/tables/post.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const tableName = 'post' - -export interface Post { - uri: string - cid: string - creator: string - text: string - replyRoot: string | null - replyRootCid: string | null - replyParent: string | null - replyParentCid: string | null - createdAt: string - indexedAt: string -} - -export type PartialDB = { - [tableName]: Post -} diff --git a/packages/pds/src/app-view/db/tables/profile-agg.ts b/packages/pds/src/app-view/db/tables/profile-agg.ts deleted file mode 100644 index 333a5136461..00000000000 --- a/packages/pds/src/app-view/db/tables/profile-agg.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Generated } from 'kysely' - -export const tableName = 'profile_agg' - -export interface ProfileAgg { - did: string - followersCount: Generated - followsCount: Generated - postsCount: Generated -} - -export type PartialDB = { - [tableName]: ProfileAgg -} diff --git a/packages/pds/src/app-view/db/tables/profile.ts b/packages/pds/src/app-view/db/tables/profile.ts deleted file mode 100644 index 6f8fac50aa5..00000000000 --- a/packages/pds/src/app-view/db/tables/profile.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const tableName = 'profile' - -export interface Profile { - uri: string - cid: string - creator: string - displayName: string | null - description: string | null - avatarCid: string | null - bannerCid: string | null - indexedAt: string -} -export type PartialDB = { [tableName]: Profile } diff --git a/packages/pds/src/app-view/db/tables/repost.ts b/packages/pds/src/app-view/db/tables/repost.ts deleted file mode 100644 index 8a0fadd6611..00000000000 --- a/packages/pds/src/app-view/db/tables/repost.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const tableName = 'repost' - -export interface Repost { - uri: string - cid: string - creator: string - subject: string - subjectCid: string - createdAt: string - indexedAt: string -} - -export type PartialDB = { [tableName]: Repost } diff --git a/packages/pds/src/app-view/db/tables/subscription.ts b/packages/pds/src/app-view/db/tables/subscription.ts deleted file mode 100644 index 62a49d0b29b..00000000000 --- a/packages/pds/src/app-view/db/tables/subscription.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const tableName = 'subscription' - -export interface Subscription { - service: string - method: string - state: string -} - -export type PartialDB = { [tableName]: Subscription } diff --git a/packages/pds/src/app-view/db/tables/suggested-follow.ts b/packages/pds/src/app-view/db/tables/suggested-follow.ts deleted file mode 100644 index 6ad10f43203..00000000000 --- a/packages/pds/src/app-view/db/tables/suggested-follow.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const tableName = 'suggested_follow' - -export interface SuggestedFollow { - did: string - order: number -} - -export type PartialDB = { - [tableName]: SuggestedFollow -} diff --git a/packages/pds/src/app-view/db/tables/view-param.ts b/packages/pds/src/app-view/db/tables/view-param.ts deleted file mode 100644 index 07e3b08f0cf..00000000000 --- a/packages/pds/src/app-view/db/tables/view-param.ts +++ /dev/null @@ -1,12 +0,0 @@ -// @NOTE postgres-only -export const tableName = 'view_param' - -// materialized views are difficult to change, -// so we parameterize them at runtime with contents of this table. -// its contents are set in migrations, available param names are static. -export interface ViewParam { - name: string - value: string -} - -export type PartialDB = { [tableName]: ViewParam } 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/services/indexing/index.ts b/packages/pds/src/app-view/services/indexing/index.ts deleted file mode 100644 index 346c31e83ea..00000000000 --- a/packages/pds/src/app-view/services/indexing/index.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { CID } from 'multiformats/cid' -import { WriteOpAction } from '@atproto/repo' -import { AtUri } from '@atproto/syntax' -import { ids } from '../../../lexicon/lexicons' -import Database from '../../../db' -import { BackgroundQueue } from '../../../event-stream/background-queue' -import { NoopProcessor } from './processor' -import * as Post from './plugins/post' -import * as Like from './plugins/like' -import * as Repost from './plugins/repost' -import * as Follow from './plugins/follow' -import * as Block from './plugins/block' -import * as List from './plugins/list' -import * as ListItem from './plugins/list-item' -import * as Profile from './plugins/profile' -import * as FeedGenerator from './plugins/feed-generator' - -export class IndexingService { - records: { - post: Post.PluginType - like: Like.PluginType - repost: Repost.PluginType - follow: Follow.PluginType - block: Block.PluginType - list: List.PluginType - listItem: ListItem.PluginType - listBlock: NoopProcessor - profile: Profile.PluginType - feedGenerator: FeedGenerator.PluginType - } - - constructor(public db: Database, public backgroundQueue: BackgroundQueue) { - this.records = { - post: Post.makePlugin(this.db, backgroundQueue), - like: Like.makePlugin(this.db, backgroundQueue), - repost: Repost.makePlugin(this.db, backgroundQueue), - follow: Follow.makePlugin(this.db, backgroundQueue), - block: Block.makePlugin(this.db, backgroundQueue), - list: List.makePlugin(this.db, backgroundQueue), - listItem: ListItem.makePlugin(this.db, backgroundQueue), - listBlock: new NoopProcessor( - ids.AppBskyGraphListblock, - this.db, - backgroundQueue, - ), - profile: Profile.makePlugin(this.db, backgroundQueue), - feedGenerator: FeedGenerator.makePlugin(this.db, backgroundQueue), - } - } - - static creator(backgroundQueue: BackgroundQueue) { - return (db: Database) => new IndexingService(db, backgroundQueue) - } - - async indexRecord( - uri: AtUri, - cid: CID, - obj: unknown, - action: WriteOpAction.Create | WriteOpAction.Update, - timestamp: string, - ) { - this.db.assertTransaction() - const indexer = this.findIndexerForCollection(uri.collection) - if (action === WriteOpAction.Create) { - await indexer.insertRecord(uri, cid, obj, timestamp) - } else { - await indexer.updateRecord(uri, cid, obj, timestamp) - } - } - - async deleteRecord(uri: AtUri, cascading = false) { - this.db.assertTransaction() - const indexer = this.findIndexerForCollection(uri.collection) - await indexer.deleteRecord(uri, cascading) - } - - findIndexerForCollection(collection: string) { - const found = Object.values(this.records).find( - (plugin) => plugin.collection === collection, - ) - if (!found) { - throw new Error('Could not find indexer for collection') - } - return found - } - - async deleteForUser(did: string) { - // Not done in transaction because it would be too long, prone to contention. - // Also, this can safely be run multiple times if it fails. - // Omitting updates to profile_agg and post_agg since it's expensive - // and they'll organically update themselves over time. - - const postByUser = (qb) => - qb - .selectFrom('post') - .where('post.creator', '=', did) - .select('post.uri as uri') - - await this.db.db - .deleteFrom('post_embed_image') - .where('post_embed_image.postUri', 'in', postByUser) - .execute() - await this.db.db - .deleteFrom('post_embed_external') - .where('post_embed_external.postUri', 'in', postByUser) - .execute() - await this.db.db - .deleteFrom('post_embed_record') - .where('post_embed_record.postUri', 'in', postByUser) - .execute() - await this.db.db - .deleteFrom('duplicate_record') - .where('duplicate_record.duplicateOf', 'in', (qb) => - // @TODO remove dependency on record table from app view - qb - .selectFrom('record') - .where('record.did', '=', did) - .select('record.uri as uri'), - ) - .execute() - await this.db.db - .deleteFrom('actor_block') - .where('creator', '=', did) - .execute() - await this.db.db.deleteFrom('list').where('creator', '=', did).execute() - await this.db.db - .deleteFrom('list_item') - .where('creator', '=', did) - .execute() - await this.db.db.deleteFrom('follow').where('creator', '=', did).execute() - await this.db.db.deleteFrom('post').where('creator', '=', did).execute() - await this.db.db.deleteFrom('profile').where('creator', '=', did).execute() - await this.db.db.deleteFrom('repost').where('creator', '=', did).execute() - await this.db.db.deleteFrom('like').where('creator', '=', did).execute() - } -} diff --git a/packages/pds/src/app-view/services/indexing/plugins/block.ts b/packages/pds/src/app-view/services/indexing/plugins/block.ts deleted file mode 100644 index bb30071c4d1..00000000000 --- a/packages/pds/src/app-view/services/indexing/plugins/block.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { AtUri } from '@atproto/syntax' -import { CID } from 'multiformats/cid' -import * as Block from '../../../../lexicon/types/app/bsky/graph/block' -import * as lex from '../../../../lexicon/lexicons' -import Database from '../../../../db' -import { - DatabaseSchema, - DatabaseSchemaType, -} from '../../../../db/database-schema' -import { BackgroundQueue } from '../../../../event-stream/background-queue' -import RecordProcessor from '../processor' -import { toSimplifiedISOSafe } from '../util' - -const lexId = lex.ids.AppBskyGraphBlock -type IndexedBlock = DatabaseSchemaType['actor_block'] - -const insertFn = async ( - db: DatabaseSchema, - uri: AtUri, - cid: CID, - obj: Block.Record, - timestamp: string, -): Promise => { - const inserted = await db - .insertInto('actor_block') - .values({ - uri: uri.toString(), - cid: cid.toString(), - creator: uri.host, - subjectDid: obj.subject, - createdAt: toSimplifiedISOSafe(obj.createdAt), - indexedAt: timestamp, - }) - .onConflict((oc) => oc.doNothing()) - .returningAll() - .executeTakeFirst() - return inserted || null -} - -const findDuplicate = async ( - db: DatabaseSchema, - uri: AtUri, - obj: Block.Record, -): Promise => { - const found = await db - .selectFrom('actor_block') - .where('creator', '=', uri.host) - .where('subjectDid', '=', obj.subject) - .selectAll() - .executeTakeFirst() - return found ? new AtUri(found.uri) : null -} - -const notifsForInsert = () => { - return [] -} - -const deleteFn = async ( - db: DatabaseSchema, - uri: AtUri, -): Promise => { - const deleted = await db - .deleteFrom('actor_block') - .where('uri', '=', uri.toString()) - .returningAll() - .executeTakeFirst() - return deleted || null -} - -const notifsForDelete = ( - deleted: IndexedBlock, - replacedBy: IndexedBlock | null, -) => { - const toDelete = replacedBy ? [] : [deleted.uri] - return { notifs: [], toDelete } -} - -const updateAggregates = async () => {} - -export type PluginType = RecordProcessor - -export const makePlugin = ( - db: Database, - backgroundQueue: BackgroundQueue, -): PluginType => { - return new RecordProcessor(db, backgroundQueue, { - lexId, - insertFn, - findDuplicate, - deleteFn, - notifsForInsert, - notifsForDelete, - updateAggregates, - }) -} - -export default makePlugin diff --git a/packages/pds/src/app-view/services/indexing/plugins/feed-generator.ts b/packages/pds/src/app-view/services/indexing/plugins/feed-generator.ts deleted file mode 100644 index c18da08d51c..00000000000 --- a/packages/pds/src/app-view/services/indexing/plugins/feed-generator.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { AtUri } from '@atproto/syntax' -import { CID } from 'multiformats/cid' -import * as FeedGenerator from '../../../../lexicon/types/app/bsky/feed/generator' -import * as lex from '../../../../lexicon/lexicons' -import Database from '../../../../db' -import { - DatabaseSchema, - DatabaseSchemaType, -} from '../../../../db/database-schema' -import { BackgroundQueue } from '../../../../event-stream/background-queue' -import RecordProcessor from '../processor' -import { toSimplifiedISOSafe } from '../util' - -const lexId = lex.ids.AppBskyFeedGenerator -type IndexedFeedGenerator = DatabaseSchemaType['feed_generator'] - -const insertFn = async ( - db: DatabaseSchema, - uri: AtUri, - cid: CID, - obj: FeedGenerator.Record, - timestamp: string, -): Promise => { - const inserted = await db - .insertInto('feed_generator') - .values({ - uri: uri.toString(), - cid: cid.toString(), - creator: uri.host, - feedDid: obj.did, - displayName: obj.displayName, - description: obj.description, - descriptionFacets: obj.descriptionFacets - ? JSON.stringify(obj.descriptionFacets) - : undefined, - avatarCid: obj.avatar?.ref.toString(), - createdAt: toSimplifiedISOSafe(obj.createdAt), - indexedAt: timestamp, - }) - .onConflict((oc) => oc.doNothing()) - .returningAll() - .executeTakeFirst() - return inserted || null -} - -const findDuplicate = async (): Promise => { - return null -} - -const notifsForInsert = () => { - return [] -} - -const deleteFn = async ( - db: DatabaseSchema, - uri: AtUri, -): Promise => { - const deleted = await db - .deleteFrom('feed_generator') - .where('uri', '=', uri.toString()) - .returningAll() - .executeTakeFirst() - return deleted || null -} - -const notifsForDelete = () => { - return { notifs: [], toDelete: [] } -} - -export type PluginType = RecordProcessor< - FeedGenerator.Record, - IndexedFeedGenerator -> - -export const makePlugin = ( - db: Database, - backgroundQueue: BackgroundQueue, -): PluginType => { - return new RecordProcessor(db, backgroundQueue, { - lexId, - insertFn, - findDuplicate, - deleteFn, - notifsForInsert, - notifsForDelete, - }) -} - -export default makePlugin diff --git a/packages/pds/src/app-view/services/indexing/plugins/follow.ts b/packages/pds/src/app-view/services/indexing/plugins/follow.ts deleted file mode 100644 index c405f288b76..00000000000 --- a/packages/pds/src/app-view/services/indexing/plugins/follow.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { AtUri } from '@atproto/syntax' -import { CID } from 'multiformats/cid' -import * as Follow from '../../../../lexicon/types/app/bsky/graph/follow' -import * as lex from '../../../../lexicon/lexicons' -import Database from '../../../../db' -import { - DatabaseSchema, - DatabaseSchemaType, -} from '../../../../db/database-schema' -import { countAll, excluded } from '../../../../db/util' -import { BackgroundQueue } from '../../../../event-stream/background-queue' -import RecordProcessor from '../processor' -import { toSimplifiedISOSafe } from '../util' - -const lexId = lex.ids.AppBskyGraphFollow -type IndexedFollow = DatabaseSchemaType['follow'] - -const insertFn = async ( - db: DatabaseSchema, - uri: AtUri, - cid: CID, - obj: Follow.Record, - timestamp: string, -): Promise => { - const inserted = await db - .insertInto('follow') - .values({ - uri: uri.toString(), - cid: cid.toString(), - creator: uri.host, - subjectDid: obj.subject, - createdAt: toSimplifiedISOSafe(obj.createdAt), - indexedAt: timestamp, - }) - .onConflict((oc) => oc.doNothing()) - .returningAll() - .executeTakeFirst() - return inserted || null -} - -const findDuplicate = async ( - db: DatabaseSchema, - uri: AtUri, - obj: Follow.Record, -): Promise => { - const found = await db - .selectFrom('follow') - .where('creator', '=', uri.host) - .where('subjectDid', '=', obj.subject) - .selectAll() - .executeTakeFirst() - return found ? new AtUri(found.uri) : null -} - -const notifsForInsert = (obj: IndexedFollow) => { - return [ - { - userDid: obj.subjectDid, - author: obj.creator, - recordUri: obj.uri, - recordCid: obj.cid, - reason: 'follow' as const, - reasonSubject: null, - indexedAt: obj.indexedAt, - }, - ] -} - -const deleteFn = async ( - db: DatabaseSchema, - uri: AtUri, -): Promise => { - const deleted = await db - .deleteFrom('follow') - .where('uri', '=', uri.toString()) - .returningAll() - .executeTakeFirst() - return deleted || null -} - -const notifsForDelete = ( - deleted: IndexedFollow, - replacedBy: IndexedFollow | null, -) => { - const toDelete = replacedBy ? [] : [deleted.uri] - return { notifs: [], toDelete } -} - -const updateAggregates = async (db: DatabaseSchema, follow: IndexedFollow) => { - const followersCountQb = db - .insertInto('profile_agg') - .values({ - did: follow.subjectDid, - followersCount: db - .selectFrom('follow') - .where('follow.subjectDid', '=', follow.subjectDid) - .select(countAll.as('count')), - }) - .onConflict((oc) => - oc.column('did').doUpdateSet({ - followersCount: excluded(db, 'followersCount'), - }), - ) - const followsCountQb = db - .insertInto('profile_agg') - .values({ - did: follow.creator, - followsCount: db - .selectFrom('follow') - .where('follow.creator', '=', follow.creator) - .select(countAll.as('count')), - }) - .onConflict((oc) => - oc.column('did').doUpdateSet({ - followsCount: excluded(db, 'followsCount'), - }), - ) - await Promise.all([followersCountQb.execute(), followsCountQb.execute()]) -} - -export type PluginType = RecordProcessor - -export const makePlugin = ( - db: Database, - backgroundQueue: BackgroundQueue, -): PluginType => { - return new RecordProcessor(db, backgroundQueue, { - lexId, - insertFn, - findDuplicate, - deleteFn, - notifsForInsert, - notifsForDelete, - updateAggregates, - }) -} - -export default makePlugin diff --git a/packages/pds/src/app-view/services/indexing/plugins/like.ts b/packages/pds/src/app-view/services/indexing/plugins/like.ts deleted file mode 100644 index 32d87ebe177..00000000000 --- a/packages/pds/src/app-view/services/indexing/plugins/like.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { AtUri } from '@atproto/syntax' -import { CID } from 'multiformats/cid' -import * as Like from '../../../../lexicon/types/app/bsky/feed/like' -import * as lex from '../../../../lexicon/lexicons' -import Database from '../../../../db' -import { - DatabaseSchema, - DatabaseSchemaType, -} from '../../../../db/database-schema' -import { countAll, excluded } from '../../../../db/util' -import { BackgroundQueue } from '../../../../event-stream/background-queue' -import RecordProcessor from '../processor' -import { toSimplifiedISOSafe } from '../util' - -const lexId = lex.ids.AppBskyFeedLike -type IndexedLike = DatabaseSchemaType['like'] - -const insertFn = async ( - db: DatabaseSchema, - uri: AtUri, - cid: CID, - obj: Like.Record, - timestamp: string, -): Promise => { - const inserted = await db - .insertInto('like') - .values({ - uri: uri.toString(), - cid: cid.toString(), - creator: uri.host, - subject: obj.subject.uri, - subjectCid: obj.subject.cid, - createdAt: toSimplifiedISOSafe(obj.createdAt), - indexedAt: timestamp, - }) - .onConflict((oc) => oc.doNothing()) - .returningAll() - .executeTakeFirst() - return inserted || null -} - -const findDuplicate = async ( - db: DatabaseSchema, - uri: AtUri, - obj: Like.Record, -): Promise => { - const found = await db - .selectFrom('like') - .where('creator', '=', uri.host) - .where('subject', '=', obj.subject.uri) - .selectAll() - .executeTakeFirst() - return found ? new AtUri(found.uri) : null -} - -const notifsForInsert = (obj: IndexedLike) => { - const subjectUri = new AtUri(obj.subject) - // prevent self-notifications - const isSelf = subjectUri.host === obj.creator - return isSelf - ? [] - : [ - { - userDid: subjectUri.host, - author: obj.creator, - recordUri: obj.uri, - recordCid: obj.cid, - reason: 'like' as const, - reasonSubject: subjectUri.toString(), - indexedAt: obj.indexedAt, - }, - ] -} - -const deleteFn = async ( - db: DatabaseSchema, - uri: AtUri, -): Promise => { - const deleted = await db - .deleteFrom('like') - .where('uri', '=', uri.toString()) - .returningAll() - .executeTakeFirst() - return deleted || null -} - -const notifsForDelete = ( - deleted: IndexedLike, - replacedBy: IndexedLike | null, -) => { - const toDelete = replacedBy ? [] : [deleted.uri] - return { notifs: [], toDelete } -} - -const updateAggregates = async (db: DatabaseSchema, like: IndexedLike) => { - const likeCountQb = db - .insertInto('post_agg') - .values({ - uri: like.subject, - likeCount: db - .selectFrom('like') - .where('like.subject', '=', like.subject) - .select(countAll.as('count')), - }) - .onConflict((oc) => - oc.column('uri').doUpdateSet({ likeCount: excluded(db, 'likeCount') }), - ) - await likeCountQb.execute() -} - -export type PluginType = RecordProcessor - -export const makePlugin = ( - db: Database, - backgroundQueue: BackgroundQueue, -): PluginType => { - return new RecordProcessor(db, backgroundQueue, { - lexId, - insertFn, - findDuplicate, - deleteFn, - notifsForInsert, - notifsForDelete, - updateAggregates, - }) -} - -export default makePlugin diff --git a/packages/pds/src/app-view/services/indexing/plugins/list-item.ts b/packages/pds/src/app-view/services/indexing/plugins/list-item.ts deleted file mode 100644 index 33d0c7e8152..00000000000 --- a/packages/pds/src/app-view/services/indexing/plugins/list-item.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { AtUri } from '@atproto/syntax' -import { CID } from 'multiformats/cid' -import * as ListItem from '../../../../lexicon/types/app/bsky/graph/listitem' -import * as lex from '../../../../lexicon/lexicons' -import Database from '../../../../db' -import { - DatabaseSchema, - DatabaseSchemaType, -} from '../../../../db/database-schema' -import { BackgroundQueue } from '../../../../event-stream/background-queue' -import RecordProcessor from '../processor' -import { InvalidRequestError } from '@atproto/xrpc-server' -import { toSimplifiedISOSafe } from '../util' - -const lexId = lex.ids.AppBskyGraphListitem -type IndexedListItem = DatabaseSchemaType['list_item'] - -const insertFn = async ( - db: DatabaseSchema, - uri: AtUri, - cid: CID, - obj: ListItem.Record, - timestamp: string, -): Promise => { - const listUri = new AtUri(obj.list) - if (listUri.hostname !== uri.hostname) { - throw new InvalidRequestError( - 'Creator of listitem does not match creator of list', - ) - } - const inserted = await db - .insertInto('list_item') - .values({ - uri: uri.toString(), - cid: cid.toString(), - creator: uri.host, - subjectDid: obj.subject, - listUri: obj.list, - createdAt: toSimplifiedISOSafe(obj.createdAt), - indexedAt: timestamp, - }) - .onConflict((oc) => oc.doNothing()) - .returningAll() - .executeTakeFirst() - return inserted || null -} - -const findDuplicate = async ( - db: DatabaseSchema, - _uri: AtUri, - obj: ListItem.Record, -): Promise => { - const found = await db - .selectFrom('list_item') - .where('listUri', '=', obj.list) - .where('subjectDid', '=', obj.subject) - .selectAll() - .executeTakeFirst() - return found ? new AtUri(found.uri) : null -} - -const notifsForInsert = () => { - return [] -} - -const deleteFn = async ( - db: DatabaseSchema, - uri: AtUri, -): Promise => { - const deleted = await db - .deleteFrom('list_item') - .where('uri', '=', uri.toString()) - .returningAll() - .executeTakeFirst() - return deleted || null -} - -const notifsForDelete = ( - deleted: IndexedListItem, - replacedBy: IndexedListItem | null, -) => { - const toDelete = replacedBy ? [] : [deleted.uri] - return { notifs: [], toDelete } -} - -export type PluginType = RecordProcessor - -export const makePlugin = ( - db: Database, - backgroundQueue: BackgroundQueue, -): PluginType => { - return new RecordProcessor(db, backgroundQueue, { - lexId, - insertFn, - findDuplicate, - deleteFn, - notifsForInsert, - notifsForDelete, - }) -} - -export default makePlugin diff --git a/packages/pds/src/app-view/services/indexing/plugins/list.ts b/packages/pds/src/app-view/services/indexing/plugins/list.ts deleted file mode 100644 index b0250c5be39..00000000000 --- a/packages/pds/src/app-view/services/indexing/plugins/list.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { AtUri } from '@atproto/syntax' -import { CID } from 'multiformats/cid' -import * as List from '../../../../lexicon/types/app/bsky/graph/list' -import * as lex from '../../../../lexicon/lexicons' -import Database from '../../../../db' -import { - DatabaseSchema, - DatabaseSchemaType, -} from '../../../../db/database-schema' -import { BackgroundQueue } from '../../../../event-stream/background-queue' -import RecordProcessor from '../processor' -import { toSimplifiedISOSafe } from '../util' - -const lexId = lex.ids.AppBskyGraphList -type IndexedList = DatabaseSchemaType['list'] - -const insertFn = async ( - db: DatabaseSchema, - uri: AtUri, - cid: CID, - obj: List.Record, - timestamp: string, -): Promise => { - const inserted = await db - .insertInto('list') - .values({ - uri: uri.toString(), - cid: cid.toString(), - creator: uri.host, - name: obj.name, - purpose: obj.purpose, - description: obj.description, - descriptionFacets: obj.descriptionFacets - ? JSON.stringify(obj.descriptionFacets) - : undefined, - avatarCid: obj.avatar?.ref.toString(), - createdAt: toSimplifiedISOSafe(obj.createdAt), - indexedAt: timestamp, - }) - .onConflict((oc) => oc.doNothing()) - .returningAll() - .executeTakeFirst() - return inserted || null -} - -const findDuplicate = async (): Promise => { - return null -} - -const notifsForInsert = () => { - return [] -} - -const deleteFn = async ( - db: DatabaseSchema, - uri: AtUri, -): Promise => { - const deleted = await db - .deleteFrom('list') - .where('uri', '=', uri.toString()) - .returningAll() - .executeTakeFirst() - return deleted || null -} - -const notifsForDelete = () => { - return { notifs: [], toDelete: [] } -} - -export type PluginType = RecordProcessor - -export const makePlugin = ( - db: Database, - backgroundQueue: BackgroundQueue, -): PluginType => { - return new RecordProcessor(db, backgroundQueue, { - lexId, - insertFn, - findDuplicate, - deleteFn, - notifsForInsert, - notifsForDelete, - }) -} - -export default makePlugin diff --git a/packages/pds/src/app-view/services/indexing/plugins/post.ts b/packages/pds/src/app-view/services/indexing/plugins/post.ts deleted file mode 100644 index efc1f2532c7..00000000000 --- a/packages/pds/src/app-view/services/indexing/plugins/post.ts +++ /dev/null @@ -1,326 +0,0 @@ -import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/syntax' -import { Record as PostRecord } from '../../../../lexicon/types/app/bsky/feed/post' -import { isMain as isEmbedImage } from '../../../../lexicon/types/app/bsky/embed/images' -import { isMain as isEmbedExternal } from '../../../../lexicon/types/app/bsky/embed/external' -import { isMain as isEmbedRecord } from '../../../../lexicon/types/app/bsky/embed/record' -import { isMain as isEmbedRecordWithMedia } from '../../../../lexicon/types/app/bsky/embed/recordWithMedia' -import { - isMention, - isLink, -} from '../../../../lexicon/types/app/bsky/richtext/facet' -import * as lex from '../../../../lexicon/lexicons' -import Database from '../../../../db' -import { - DatabaseSchema, - DatabaseSchemaType, -} from '../../../../db/database-schema' -import { BackgroundQueue } from '../../../../event-stream/background-queue' -import RecordProcessor from '../processor' -import { UserNotification } from '../../../../db/tables/user-notification' -import { countAll, excluded } from '../../../../db/util' -import { toSimplifiedISOSafe } from '../util' - -type Post = DatabaseSchemaType['post'] -type PostEmbedImage = DatabaseSchemaType['post_embed_image'] -type PostEmbedExternal = DatabaseSchemaType['post_embed_external'] -type PostEmbedRecord = DatabaseSchemaType['post_embed_record'] -type PostAncestor = { - uri: string - height: number -} -type IndexedPost = { - post: Post - facets: { type: 'mention' | 'link'; value: string }[] - embeds?: (PostEmbedImage[] | PostEmbedExternal | PostEmbedRecord)[] - ancestors?: PostAncestor[] -} - -const lexId = lex.ids.AppBskyFeedPost - -const REPLY_NOTIF_DEPTH = 5 - -const insertFn = async ( - db: DatabaseSchema, - uri: AtUri, - cid: CID, - obj: PostRecord, - timestamp: string, -): Promise => { - const post = { - uri: uri.toString(), - cid: cid.toString(), - creator: uri.host, - text: obj.text, - createdAt: toSimplifiedISOSafe(obj.createdAt), - replyRoot: obj.reply?.root?.uri || null, - replyRootCid: obj.reply?.root?.cid || null, - replyParent: obj.reply?.parent?.uri || null, - replyParentCid: obj.reply?.parent?.cid || null, - indexedAt: timestamp, - } - const [insertedPost] = await Promise.all([ - db - .insertInto('post') - .values(post) - .onConflict((oc) => oc.doNothing()) - .returningAll() - .executeTakeFirst(), - db - .insertInto('feed_item') - .values({ - type: 'post', - uri: post.uri, - cid: post.cid, - postUri: post.uri, - originatorDid: post.creator, - sortAt: - post.indexedAt < post.createdAt ? post.indexedAt : post.createdAt, - }) - .onConflict((oc) => oc.doNothing()) - .executeTakeFirst(), - ]) - if (!insertedPost) { - return null // Post already indexed - } - - const facets = (obj.facets || []) - .flatMap((facet) => facet.features) - .flatMap((feature) => { - if (isMention(feature)) { - return { - type: 'mention' as const, - value: feature.did, - } - } - if (isLink(feature)) { - return { - type: 'link' as const, - value: feature.uri, - } - } - return [] - }) - // Embed indices - const embeds: (PostEmbedImage[] | PostEmbedExternal | PostEmbedRecord)[] = [] - const postEmbeds = separateEmbeds(obj.embed) - for (const postEmbed of postEmbeds) { - if (isEmbedImage(postEmbed)) { - const { images } = postEmbed - const imagesEmbed = images.map((img, i) => ({ - postUri: uri.toString(), - position: i, - imageCid: img.image.ref.toString(), - alt: img.alt, - })) - embeds.push(imagesEmbed) - await db.insertInto('post_embed_image').values(imagesEmbed).execute() - } else if (isEmbedExternal(postEmbed)) { - const { external } = postEmbed - const externalEmbed = { - postUri: uri.toString(), - uri: external.uri, - title: external.title, - description: external.description, - thumbCid: external.thumb?.ref.toString() || null, - } - embeds.push(externalEmbed) - await db.insertInto('post_embed_external').values(externalEmbed).execute() - } else if (isEmbedRecord(postEmbed)) { - const { record } = postEmbed - const recordEmbed = { - postUri: uri.toString(), - embedUri: record.uri, - embedCid: record.cid, - } - embeds.push(recordEmbed) - await db.insertInto('post_embed_record').values(recordEmbed).execute() - } - } - return { post: insertedPost, facets, embeds } -} - -const findDuplicate = async (): Promise => { - return null -} - -const notifsForInsert = (obj: IndexedPost) => { - const notifs: UserNotification[] = [] - const notified = new Set([obj.post.creator]) - const maybeNotify = (notif: UserNotification) => { - if (!notified.has(notif.userDid)) { - notified.add(notif.userDid) - notifs.push(notif) - } - } - for (const facet of obj.facets) { - if (facet.type === 'mention') { - maybeNotify({ - userDid: facet.value, - reason: 'mention', - reasonSubject: null, - author: obj.post.creator, - recordUri: obj.post.uri, - recordCid: obj.post.cid, - indexedAt: obj.post.indexedAt, - }) - } - } - for (const embed of obj.embeds ?? []) { - if ('embedUri' in embed) { - const embedUri = new AtUri(embed.embedUri) - if (embedUri.collection === lex.ids.AppBskyFeedPost) { - maybeNotify({ - userDid: embedUri.host, - reason: 'quote', - reasonSubject: embedUri.toString(), - author: obj.post.creator, - recordUri: obj.post.uri, - recordCid: obj.post.cid, - indexedAt: obj.post.indexedAt, - }) - } - } - } - for (const ancestor of obj.ancestors ?? []) { - if (ancestor.uri === obj.post.uri) continue // no need to notify for own post - if (ancestor.height < REPLY_NOTIF_DEPTH) { - const ancestorUri = new AtUri(ancestor.uri) - maybeNotify({ - userDid: ancestorUri.host, - reason: 'reply', - reasonSubject: ancestorUri.toString(), - author: obj.post.creator, - recordUri: obj.post.uri, - recordCid: obj.post.cid, - indexedAt: obj.post.indexedAt, - }) - } - } - return notifs -} - -const deleteFn = async ( - db: DatabaseSchema, - uri: AtUri, -): Promise => { - const uriStr = uri.toString() - const [deleted] = await Promise.all([ - db - .deleteFrom('post') - .where('uri', '=', uriStr) - .returningAll() - .executeTakeFirst(), - db.deleteFrom('feed_item').where('postUri', '=', uriStr).executeTakeFirst(), - ]) - const deletedEmbeds: ( - | PostEmbedImage[] - | PostEmbedExternal - | PostEmbedRecord - )[] = [] - const [deletedImgs, deletedExternals, deletedPosts] = await Promise.all([ - db - .deleteFrom('post_embed_image') - .where('postUri', '=', uriStr) - .returningAll() - .execute(), - db - .deleteFrom('post_embed_external') - .where('postUri', '=', uriStr) - .returningAll() - .executeTakeFirst(), - db - .deleteFrom('post_embed_record') - .where('postUri', '=', uriStr) - .returningAll() - .executeTakeFirst(), - ]) - if (deletedImgs.length) { - deletedEmbeds.push(deletedImgs) - } - if (deletedExternals) { - deletedEmbeds.push(deletedExternals) - } - if (deletedPosts) { - deletedEmbeds.push(deletedPosts) - } - return deleted - ? { - post: deleted, - facets: [], // Not used - embeds: deletedEmbeds, - } - : null -} - -const notifsForDelete = ( - deleted: IndexedPost, - replacedBy: IndexedPost | null, -) => { - const notifs = replacedBy ? notifsForInsert(replacedBy) : [] - return { - notifs, - toDelete: [deleted.post.uri], - } -} - -const updateAggregates = async (db: DatabaseSchema, postIdx: IndexedPost) => { - const replyCountQb = postIdx.post.replyParent - ? db - .insertInto('post_agg') - .values({ - uri: postIdx.post.replyParent, - replyCount: db - .selectFrom('post') - .where('post.replyParent', '=', postIdx.post.replyParent) - .select(countAll.as('count')), - }) - .onConflict((oc) => - oc - .column('uri') - .doUpdateSet({ replyCount: excluded(db, 'replyCount') }), - ) - : null - const postsCountQb = db - .insertInto('profile_agg') - .values({ - did: postIdx.post.creator, - postsCount: db - .selectFrom('post') - .where('post.creator', '=', postIdx.post.creator) - .select(countAll.as('count')), - }) - .onConflict((oc) => - oc.column('did').doUpdateSet({ postsCount: excluded(db, 'postsCount') }), - ) - await Promise.all([replyCountQb?.execute(), postsCountQb.execute()]) -} - -export type PluginType = RecordProcessor - -export const makePlugin = ( - db: Database, - backgroundQueue: BackgroundQueue, -): PluginType => { - return new RecordProcessor(db, backgroundQueue, { - lexId, - insertFn, - findDuplicate, - deleteFn, - notifsForInsert, - notifsForDelete, - updateAggregates, - }) -} - -export default makePlugin - -function separateEmbeds(embed: PostRecord['embed']) { - if (!embed) { - return [] - } - if (isEmbedRecordWithMedia(embed)) { - return [{ $type: lex.ids.AppBskyEmbedRecord, ...embed.record }, embed.media] - } - return [embed] -} diff --git a/packages/pds/src/app-view/services/indexing/plugins/profile.ts b/packages/pds/src/app-view/services/indexing/plugins/profile.ts deleted file mode 100644 index 3b8e6db506c..00000000000 --- a/packages/pds/src/app-view/services/indexing/plugins/profile.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { AtUri } from '@atproto/syntax' -import { CID } from 'multiformats/cid' -import * as Profile from '../../../../lexicon/types/app/bsky/actor/profile' -import * as lex from '../../../../lexicon/lexicons' -import Database from '../../../../db' -import { - DatabaseSchema, - DatabaseSchemaType, -} from '../../../../db/database-schema' -import { BackgroundQueue } from '../../../../event-stream/background-queue' -import RecordProcessor from '../processor' - -const lexId = lex.ids.AppBskyActorProfile -type IndexedProfile = DatabaseSchemaType['profile'] - -const insertFn = async ( - db: DatabaseSchema, - uri: AtUri, - cid: CID, - obj: Profile.Record, - timestamp: string, -): Promise => { - if (uri.rkey !== 'self') return null - const inserted = await db - .insertInto('profile') - .values({ - uri: uri.toString(), - cid: cid.toString(), - creator: uri.host, - displayName: obj.displayName, - description: obj.description, - avatarCid: obj.avatar?.ref.toString(), - bannerCid: obj.banner?.ref.toString(), - indexedAt: timestamp, - }) - .onConflict((oc) => oc.doNothing()) - .returningAll() - .executeTakeFirst() - return inserted || null -} - -const findDuplicate = async (): Promise => { - return null -} - -const notifsForInsert = () => { - return [] -} - -const deleteFn = async ( - db: DatabaseSchema, - uri: AtUri, -): Promise => { - const deleted = await db - .deleteFrom('profile') - .where('uri', '=', uri.toString()) - .returningAll() - .executeTakeFirst() - return deleted || null -} - -const notifsForDelete = () => { - return { notifs: [], toDelete: [] } -} - -export type PluginType = RecordProcessor - -export const makePlugin = ( - db: Database, - backgroundQueue: BackgroundQueue, -): PluginType => { - return new RecordProcessor(db, backgroundQueue, { - lexId, - insertFn, - findDuplicate, - deleteFn, - notifsForInsert, - notifsForDelete, - }) -} - -export default makePlugin diff --git a/packages/pds/src/app-view/services/indexing/plugins/repost.ts b/packages/pds/src/app-view/services/indexing/plugins/repost.ts deleted file mode 100644 index fb05ca57ffd..00000000000 --- a/packages/pds/src/app-view/services/indexing/plugins/repost.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/syntax' -import * as Repost from '../../../../lexicon/types/app/bsky/feed/repost' -import * as lex from '../../../../lexicon/lexicons' -import Database from '../../../../db' -import { - DatabaseSchema, - DatabaseSchemaType, -} from '../../../../db/database-schema' -import { BackgroundQueue } from '../../../../event-stream/background-queue' -import RecordProcessor from '../processor' -import { countAll, excluded } from '../../../../db/util' -import { toSimplifiedISOSafe } from '../util' - -const lexId = lex.ids.AppBskyFeedRepost -type IndexedRepost = DatabaseSchemaType['repost'] - -const insertFn = async ( - db: DatabaseSchema, - uri: AtUri, - cid: CID, - obj: Repost.Record, - timestamp: string, -): Promise => { - const repost = { - uri: uri.toString(), - cid: cid.toString(), - creator: uri.host, - subject: obj.subject.uri, - subjectCid: obj.subject.cid, - createdAt: toSimplifiedISOSafe(obj.createdAt), - indexedAt: timestamp, - } - const [inserted] = await Promise.all([ - db - .insertInto('repost') - .values(repost) - .onConflict((oc) => oc.doNothing()) - .returningAll() - .executeTakeFirst(), - db - .insertInto('feed_item') - .values({ - type: 'repost', - uri: repost.uri, - cid: repost.cid, - postUri: repost.subject, - originatorDid: repost.creator, - sortAt: - repost.indexedAt < repost.createdAt - ? repost.indexedAt - : repost.createdAt, - }) - .onConflict((oc) => oc.doNothing()) - .executeTakeFirst(), - ]) - - return inserted || null -} - -const findDuplicate = async ( - db: DatabaseSchema, - uri: AtUri, - obj: Repost.Record, -): Promise => { - const found = await db - .selectFrom('repost') - .where('creator', '=', uri.host) - .where('subject', '=', obj.subject.uri) - .selectAll() - .executeTakeFirst() - return found ? new AtUri(found.uri) : null -} - -const notifsForInsert = (obj: IndexedRepost) => { - const subjectUri = new AtUri(obj.subject) - // prevent self-notifications - const isSelf = subjectUri.host === obj.creator - return isSelf - ? [] - : [ - { - userDid: subjectUri.host, - author: obj.creator, - recordUri: obj.uri, - recordCid: obj.cid, - reason: 'repost' as const, - reasonSubject: subjectUri.toString(), - indexedAt: obj.indexedAt, - }, - ] -} - -const deleteFn = async ( - db: DatabaseSchema, - uri: AtUri, -): Promise => { - const uriStr = uri.toString() - const [deleted] = await Promise.all([ - db - .deleteFrom('repost') - .where('uri', '=', uriStr) - .returningAll() - .executeTakeFirst(), - db.deleteFrom('feed_item').where('uri', '=', uriStr).executeTakeFirst(), - ]) - return deleted || null -} - -const notifsForDelete = ( - deleted: IndexedRepost, - replacedBy: IndexedRepost | null, -) => { - const toDelete = replacedBy ? [] : [deleted.uri] - return { notifs: [], toDelete } -} - -const updateAggregates = async (db: DatabaseSchema, repost: IndexedRepost) => { - const repostCountQb = db - .insertInto('post_agg') - .values({ - uri: repost.subject, - repostCount: db - .selectFrom('repost') - .where('repost.subject', '=', repost.subject) - .select(countAll.as('count')), - }) - .onConflict((oc) => - oc - .column('uri') - .doUpdateSet({ repostCount: excluded(db, 'repostCount') }), - ) - await repostCountQb.execute() -} - -export type PluginType = RecordProcessor - -export const makePlugin = ( - db: Database, - backgroundQueue: BackgroundQueue, -): PluginType => { - return new RecordProcessor(db, backgroundQueue, { - lexId, - insertFn, - findDuplicate, - deleteFn, - notifsForInsert, - notifsForDelete, - updateAggregates, - }) -} - -export default makePlugin diff --git a/packages/pds/src/app-view/services/indexing/processor.ts b/packages/pds/src/app-view/services/indexing/processor.ts deleted file mode 100644 index 6b6712889f2..00000000000 --- a/packages/pds/src/app-view/services/indexing/processor.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/syntax' -import { cborToLexRecord } from '@atproto/repo' -import Database from '../../../db' -import DatabaseSchema from '../../../db/database-schema' -import { BackgroundQueue } from '../../../event-stream/background-queue' -import { lexicons } from '../../../lexicon/lexicons' -import { UserNotification } from '../../../db/tables/user-notification' - -// @NOTE re: insertions and deletions. Due to how record updates are handled, -// (insertFn) should have the same effect as (insertFn -> deleteFn -> insertFn). -type RecordProcessorParams = { - lexId: string - insertFn: ( - db: DatabaseSchema, - uri: AtUri, - cid: CID, - obj: T, - timestamp: string, - ) => Promise - findDuplicate: ( - db: DatabaseSchema, - uri: AtUri, - obj: T, - ) => Promise - deleteFn: (db: DatabaseSchema, uri: AtUri) => Promise - notifsForInsert: (obj: S) => UserNotification[] - notifsForDelete: ( - prev: S, - replacedBy: S | null, - ) => { notifs: UserNotification[]; toDelete: string[] } - updateAggregates?: (db: DatabaseSchema, obj: S) => Promise -} - -export class RecordProcessor { - collection: string - db: DatabaseSchema - constructor( - private appDb: Database, - private backgroundQueue: BackgroundQueue, - private params: RecordProcessorParams, - ) { - this.db = appDb.db - this.collection = this.params.lexId - } - - matchesSchema(obj: unknown): obj is T { - try { - this.assertValidRecord(obj) - return true - } catch { - return false - } - } - - assertValidRecord(obj: unknown): void { - lexicons.assertValidRecord(this.params.lexId, obj) - } - - async insertRecord(uri: AtUri, cid: CID, obj: unknown, timestamp: string) { - if (!this.matchesSchema(obj)) { - throw new Error(`Record does not match schema: ${this.params.lexId}`) - } - const inserted = await this.params.insertFn( - this.db, - uri, - cid, - obj, - timestamp, - ) - // if this was a new record, return events - if (inserted) { - this.aggregateOnCommit(inserted) - await this.handleNotifs({ inserted }) - return - } - // if duplicate, insert into duplicates table with no events - const found = await this.params.findDuplicate(this.db, uri, obj) - if (found && found.toString() !== uri.toString()) { - await this.db - .insertInto('duplicate_record') - .values({ - uri: uri.toString(), - cid: cid.toString(), - duplicateOf: found.toString(), - indexedAt: timestamp, - }) - .onConflict((oc) => oc.doNothing()) - .execute() - } - } - - // Currently using a very simple strategy for updates: purge the existing index - // for the uri then replace it. The main upside is that this allows the indexer - // for each collection to avoid bespoke logic for in-place updates, which isn't - // straightforward in the general case. We still get nice control over notifications. - async updateRecord(uri: AtUri, cid: CID, obj: unknown, timestamp: string) { - if (!this.matchesSchema(obj)) { - throw new Error(`Record does not match schema: ${this.params.lexId}`) - } - - // If the updated record was a dupe, update dupe info for it - const dupe = await this.params.findDuplicate(this.db, uri, obj) - if (dupe) { - await this.db - .updateTable('duplicate_record') - .where('uri', '=', uri.toString()) - .set({ - cid: cid.toString(), - duplicateOf: dupe.toString(), - indexedAt: timestamp, - }) - .execute() - } else { - await this.db - .deleteFrom('duplicate_record') - .where('uri', '=', uri.toString()) - .execute() - } - - const deleted = await this.params.deleteFn(this.db, uri) - if (!deleted) { - // If a record was updated but hadn't been indexed yet, treat it like a plain insert. - return this.insertRecord(uri, cid, obj, timestamp) - } - this.aggregateOnCommit(deleted) - const inserted = await this.params.insertFn( - this.db, - uri, - cid, - obj, - timestamp, - ) - if (!inserted) { - throw new Error( - 'Record update failed: removed from index but could not be replaced', - ) - } - this.aggregateOnCommit(inserted) - await this.handleNotifs({ inserted, deleted }) - } - - async deleteRecord(uri: AtUri, cascading = false) { - await this.db - .deleteFrom('duplicate_record') - .where('uri', '=', uri.toString()) - .execute() - const deleted = await this.params.deleteFn(this.db, uri) - if (!deleted) return - this.aggregateOnCommit(deleted) - if (cascading) { - await this.db - .deleteFrom('duplicate_record') - .where('duplicateOf', '=', uri.toString()) - .execute() - await this.handleNotifs({ deleted }) - return - } else { - const found = await this.db - .selectFrom('duplicate_record') - // @TODO remove ipld_block dependency from app-view - .innerJoin('ipld_block', (join) => - join - .onRef('ipld_block.cid', '=', 'duplicate_record.cid') - .on('ipld_block.creator', '=', uri.host), - ) - .where('duplicateOf', '=', uri.toString()) - .orderBy('duplicate_record.indexedAt', 'asc') - .limit(1) - .selectAll() - .executeTakeFirst() - - if (!found) { - return this.handleNotifs({ deleted }) - } - const record = cborToLexRecord(found.content) - if (!this.matchesSchema(record)) { - return this.handleNotifs({ deleted }) - } - const inserted = await this.params.insertFn( - this.db, - new AtUri(found.uri), - CID.parse(found.cid), - record, - found.indexedAt, - ) - if (inserted) { - this.aggregateOnCommit(inserted) - } - await this.handleNotifs({ deleted, inserted: inserted ?? undefined }) - } - } - - async handleNotifs(op: { deleted?: S; inserted?: S }) { - let notifs: UserNotification[] = [] - const runOnCommit: ((db: Database) => Promise)[] = [] - if (op.deleted) { - const forDelete = this.params.notifsForDelete( - op.deleted, - op.inserted ?? null, - ) - if (forDelete.toDelete.length > 0) { - // Notifs can be deleted in background: they are expensive to delete and - // listNotifications already excludes notifs with missing records. - runOnCommit.push(async (db) => { - await db.db - .deleteFrom('user_notification') - .where('recordUri', 'in', forDelete.toDelete) - .execute() - }) - } - notifs = forDelete.notifs - } else if (op.inserted) { - notifs = this.params.notifsForInsert(op.inserted) - } - if (notifs.length > 0) { - runOnCommit.push(async (db) => { - await db.db.insertInto('user_notification').values(notifs).execute() - }) - } - if (runOnCommit.length) { - // Need to ensure notif deletion always happens before creation, otherwise delete may clobber in a race. - this.appDb.onCommit(() => { - this.backgroundQueue.add(async (db) => { - for (const fn of runOnCommit) { - await fn(db) - } - }) - }) - } - } - - aggregateOnCommit(indexed: S) { - const { updateAggregates } = this.params - if (!updateAggregates) return - this.appDb.onCommit(() => { - this.backgroundQueue.add((db) => updateAggregates(db.db, indexed)) - }) - } -} - -export default RecordProcessor - -export class NoopProcessor extends RecordProcessor { - constructor( - lexId: string, - appDb: Database, - backgroundQueue: BackgroundQueue, - ) { - super(appDb, backgroundQueue, { - lexId, - insertFn: async () => null, - deleteFn: async () => null, - findDuplicate: async () => null, - notifsForInsert: () => [], - notifsForDelete: () => ({ notifs: [], toDelete: [] }), - }) - } -} diff --git a/packages/pds/src/app-view/services/indexing/util.ts b/packages/pds/src/app-view/services/indexing/util.ts deleted file mode 100644 index 5ce78959f8f..00000000000 --- a/packages/pds/src/app-view/services/indexing/util.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { isValidISODateString } from 'iso-datestring-validator' - -// Normalize date strings to simplified ISO so that the lexical sort preserves temporal sort. -// Rather than failing on an invalid date format, returns valid unix epoch. -export function toSimplifiedISOSafe(dateStr: string) { - const date = new Date(dateStr) - if (isNaN(date.getTime())) { - return new Date(0).toISOString() - } - const iso = date.toISOString() - if (!isValidISODateString(iso)) { - // Occurs in rare cases, e.g. where resulting UTC year is negative. These also don't preserve lexical sort. - return new Date(0).toISOString() - } - return iso // YYYY-MM-DDTHH:mm:ss.sssZ -} diff --git a/packages/pds/src/app-view/services/label/index.ts b/packages/pds/src/app-view/services/label/index.ts deleted file mode 100644 index a3534666d5d..00000000000 --- a/packages/pds/src/app-view/services/label/index.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { sql } from 'kysely' -import { AtUri } from '@atproto/syntax' -import Database from '../../../db' -import { - Label, - isSelfLabels, -} from '../../../lexicon/types/com/atproto/label/defs' -import { ids } from '../../../lexicon/lexicons' -import { LabelCache } from '../../../label-cache' -import { toSimplifiedISOSafe } from '../indexing/util' - -export type Labels = Record - -export class LabelService { - constructor(public db: Database, public cache: LabelCache) {} - - static creator(cache: LabelCache) { - return (db: Database) => new LabelService(db, cache) - } - - async formatAndCreate( - src: string, - uri: string, - cid: string | null, - labels: { create?: string[]; negate?: string[] }, - ) { - const { create = [], negate = [] } = labels - const toCreate = create.map((val) => ({ - src, - uri, - cid: cid ?? undefined, - val, - neg: false, - cts: new Date().toISOString(), - })) - const toNegate = negate.map((val) => ({ - src, - uri, - cid: cid ?? undefined, - val, - neg: true, - cts: new Date().toISOString(), - })) - await this.createLabels([...toCreate, ...toNegate]) - } - - async createLabels(labels: Label[]) { - if (labels.length < 1) return - const dbVals = labels.map((l) => ({ - ...l, - cid: l.cid ?? '', - neg: (l.neg ? 1 : 0) as 1 | 0, - })) - const { ref } = this.db.db.dynamic - const excluded = (col: string) => ref(`excluded.${col}`) - await this.db.db - .insertInto('label') - .values(dbVals) - .onConflict((oc) => - oc.columns(['src', 'uri', 'cid', 'val']).doUpdateSet({ - neg: sql`${excluded('neg')}`, - cts: sql`${excluded('cts')}`, - }), - ) - .execute() - } - - async getLabelsForUris( - subjects: string[], - opts?: { - includeNeg?: boolean - skipCache?: boolean - }, - ): Promise { - if (subjects.length < 1) return {} - const res = opts?.skipCache - ? await this.db.db - .selectFrom('label') - .where('label.uri', 'in', subjects) - .if(!opts?.includeNeg, (qb) => qb.where('neg', '=', 0)) - .selectAll() - .execute() - : this.cache.forSubjects(subjects, opts?.includeNeg) - return res.reduce((acc, cur) => { - acc[cur.uri] ??= [] - acc[cur.uri].push({ - ...cur, - cid: cur.cid === '' ? undefined : cur.cid, - neg: cur.neg === 1, // @TODO update in appview - }) - return acc - }, {} as Labels) - } - - // gets labels for any record. when did is present, combine labels for both did & profile record. - async getLabelsForSubjects( - subjects: string[], - opts?: { - includeNeg?: boolean - skipCache?: boolean - }, - ): Promise { - if (subjects.length < 1) return {} - const expandedSubjects = subjects.flatMap((subject) => { - if (subject.startsWith('did:')) { - return [ - subject, - AtUri.make(subject, ids.AppBskyActorProfile, 'self').toString(), - ] - } - return subject - }) - const labels = await this.getLabelsForUris(expandedSubjects, opts) - return Object.keys(labels).reduce((acc, cur) => { - const uri = cur.startsWith('at://') ? new AtUri(cur) : null - if ( - uri && - uri.collection === ids.AppBskyActorProfile && - uri.rkey === 'self' - ) { - // combine labels for profile + did - const did = uri.hostname - acc[did] ??= [] - acc[did].push(...labels[cur]) - } - acc[cur] ??= [] - acc[cur].push(...labels[cur]) - return acc - }, {} as Labels) - } - - async getLabels( - subject: string, - opts?: { - includeNeg?: boolean - skipCache?: boolean - }, - ): Promise { - const labels = await this.getLabelsForUris([subject], opts) - return labels[subject] ?? [] - } - - async getLabelsForProfile( - did: string, - opts?: { - includeNeg?: boolean - skipCache?: boolean - }, - ): Promise { - const labels = await this.getLabelsForSubjects([did], opts) - return labels[did] ?? [] - } -} - -export function getSelfLabels(details: { - uri: string | null - cid: string | null - record: Record | null -}): Label[] { - const { uri, cid, record } = details - if (!uri || !cid || !record) return [] - if (!isSelfLabels(record.labels)) return [] - const src = new AtUri(uri).host // record creator - const cts = - typeof record.createdAt === 'string' - ? toSimplifiedISOSafe(record.createdAt) - : new Date(0).toISOString() - return record.labels.values.map(({ val }) => { - return { src, uri, cid, val, cts, neg: false } - }) -} diff --git a/packages/pds/src/event-stream/background-queue.ts b/packages/pds/src/background.ts similarity index 92% rename from packages/pds/src/event-stream/background-queue.ts rename to packages/pds/src/background.ts index aa7671ccc07..65d8cbd473b 100644 --- a/packages/pds/src/event-stream/background-queue.ts +++ b/packages/pds/src/background.ts @@ -1,6 +1,6 @@ import PQueue from 'p-queue' -import Database from '../db' -import { dbLogger } from '../logger' +import Database from './db' +import { dbLogger } from './logger' // A simple queue for in-process, out-of-band/backgrounded work diff --git a/packages/pds/src/config.ts b/packages/pds/src/config.ts index 816aa511873..f8d7b04d8fa 100644 --- a/packages/pds/src/config.ts +++ b/packages/pds/src/config.ts @@ -50,12 +50,6 @@ export interface ServerConfigValues { moderationEmailAddress?: string moderationEmailSmtpUrl?: string - hiveApiKey?: string - labelerDid: string - labelerKeywords: Record - - feedGenDid?: string - maxSubscriptionBuffer: number repoBackfillLimitMs: number sequencerLeaderLockId?: number @@ -179,12 +173,6 @@ export class ServerConfig { const moderationEmailSmtpUrl = process.env.MODERATION_EMAIL_SMTP_URL || undefined - const hiveApiKey = process.env.HIVE_API_KEY || undefined - const labelerDid = process.env.LABELER_DID || 'did:example:labeler' - const labelerKeywords = {} - - const feedGenDid = process.env.FEED_GEN_DID - const dbPostgresUrl = process.env.DB_POSTGRES_URL const dbPostgresSchema = process.env.DB_POSTGRES_SCHEMA @@ -271,10 +259,6 @@ export class ServerConfig { emailNoReplyAddress, moderationEmailAddress, moderationEmailSmtpUrl, - hiveApiKey, - labelerDid, - labelerKeywords, - feedGenDid, maxSubscriptionBuffer, repoBackfillLimitMs, sequencerLeaderLockId, @@ -467,22 +451,6 @@ export class ServerConfig { return this.cfg.moderationEmailSmtpUrl } - get hiveApiKey() { - return this.cfg.hiveApiKey - } - - get labelerDid() { - return this.cfg.labelerDid - } - - get labelerKeywords() { - return this.cfg.labelerKeywords - } - - get feedGenDid() { - return this.cfg.feedGenDid - } - get maxSubscriptionBuffer() { return this.cfg.maxSubscriptionBuffer } diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index ea0990955e2..791100c492b 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -11,13 +11,10 @@ import { ServerMailer } from './mailer' import { ModerationMailer } from './mailer/moderation' import { BlobStore } from '@atproto/repo' import { Services } from './services' -import { MessageDispatcher } from './event-stream/message-queue' import { Sequencer, SequencerLeader } from './sequencer' -import { Labeler } from './labeler' -import { BackgroundQueue } from './event-stream/background-queue' +import { BackgroundQueue } from './background' import DidSqlCache from './did-cache' import { Crawlers } from './crawlers' -import { LabelCache } from './label-cache' import { RuntimeFlags } from './runtime-flags' export class AppContext { @@ -35,11 +32,8 @@ export class AppContext { mailer: ServerMailer moderationMailer: ModerationMailer services: Services - messageDispatcher: MessageDispatcher sequencer: Sequencer sequencerLeader: SequencerLeader | null - labeler: Labeler - labelCache: LabelCache runtimeFlags: RuntimeFlags backgroundQueue: BackgroundQueue appviewAgent: AtpAgent @@ -115,10 +109,6 @@ export class AppContext { return this.opts.services } - get messageDispatcher(): MessageDispatcher { - return this.opts.messageDispatcher - } - get sequencer(): Sequencer { return this.opts.sequencer } @@ -127,14 +117,6 @@ export class AppContext { return this.opts.sequencerLeader } - get labeler(): Labeler { - return this.opts.labeler - } - - get labelCache(): LabelCache { - return this.opts.labelCache - } - get runtimeFlags(): RuntimeFlags { return this.opts.runtimeFlags } diff --git a/packages/pds/src/db/database-schema.ts b/packages/pds/src/db/database-schema.ts index f77df6b21ad..eda67a4e6fb 100644 --- a/packages/pds/src/db/database-schema.ts +++ b/packages/pds/src/db/database-schema.ts @@ -18,14 +18,11 @@ import * as deleteAccountToken from './tables/delete-account-token' import * as moderation from './tables/moderation' import * as mute from './tables/mute' import * as listMute from './tables/list-mute' -import * as label from './tables/label' import * as repoSeq from './tables/repo-seq' import * as appMigration from './tables/app-migration' import * as runtimeFlag from './tables/runtime-flag' -import * as appView from '../app-view/db' -export type DatabaseSchemaType = appView.DatabaseSchemaType & - runtimeFlag.PartialDB & +export type DatabaseSchemaType = runtimeFlag.PartialDB & appMigration.PartialDB & userAccount.PartialDB & userState.PartialDB & @@ -46,7 +43,6 @@ export type DatabaseSchemaType = appView.DatabaseSchemaType & moderation.PartialDB & mute.PartialDB & listMute.PartialDB & - label.PartialDB & repoSeq.PartialDB export type DatabaseSchema = Kysely diff --git a/packages/pds/src/db/migrations/20230922T033938477Z-remove-appview.ts b/packages/pds/src/db/migrations/20230922T033938477Z-remove-appview.ts new file mode 100644 index 00000000000..f66825fa722 --- /dev/null +++ b/packages/pds/src/db/migrations/20230922T033938477Z-remove-appview.ts @@ -0,0 +1,29 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema.dropView('algo_whats_hot_view').materialized().execute() + await db.schema.dropTable('actor_block').execute() + await db.schema.dropTable('duplicate_record').execute() + await db.schema.dropTable('feed_generator').execute() + await db.schema.dropTable('feed_item').execute() + await db.schema.dropTable('follow').execute() + await db.schema.dropTable('label').execute() + await db.schema.dropTable('like').execute() + await db.schema.dropTable('list_item').execute() + await db.schema.dropTable('list').execute() + await db.schema.dropTable('post_agg').execute() + await db.schema.dropTable('post_embed_image').execute() + await db.schema.dropTable('post_embed_external').execute() + await db.schema.dropTable('post_embed_record').execute() + await db.schema.dropTable('post').execute() + await db.schema.dropTable('profile_agg').execute() + await db.schema.dropTable('profile').execute() + await db.schema.dropTable('repost').execute() + await db.schema.dropTable('subscription').execute() + await db.schema.dropTable('suggested_follow').execute() + await db.schema.dropTable('view_param').execute() +} + +export async function down(_db: Kysely): Promise { + // Migration code +} diff --git a/packages/pds/src/db/migrations/index.ts b/packages/pds/src/db/migrations/index.ts index e7e521e986a..fde3ddd2398 100644 --- a/packages/pds/src/db/migrations/index.ts +++ b/packages/pds/src/db/migrations/index.ts @@ -65,3 +65,4 @@ export * as _20230818T134357818Z from './20230818T134357818Z-runtime-flags' export * as _20230824T182048120Z from './20230824T182048120Z-remove-post-hierarchy' export * as _20230825T142507884Z from './20230825T142507884Z-blob-tempkey-idx' export * as _20230828T153013575Z from './20230828T153013575Z-repo-history-rewrite' +export * as _20230922T033938477Z from './20230922T033938477Z-remove-appview' diff --git a/packages/pds/src/db/periodic-moderation-action-reversal.ts b/packages/pds/src/db/periodic-moderation-action-reversal.ts index 72730e61565..b3b631de71d 100644 --- a/packages/pds/src/db/periodic-moderation-action-reversal.ts +++ b/packages/pds/src/db/periodic-moderation-action-reversal.ts @@ -4,7 +4,6 @@ import { Leader } from './leader' import { dbLogger } from '../logger' import AppContext from '../context' import { ModerationActionRow } from '../services/moderation' -import { LabelService } from '../app-view/services/label' export const MODERATION_ACTION_REVERSAL_ID = 1011 @@ -14,39 +13,15 @@ export class PeriodicModerationActionReversal { constructor(private appContext: AppContext) {} - // invert label creation & negations - async reverseLabels(labelTxn: LabelService, actionRow: ModerationActionRow) { - let uri: string - let cid: string | null = null - - if (actionRow.subjectUri && actionRow.subjectCid) { - uri = actionRow.subjectUri - cid = actionRow.subjectCid - } else { - uri = actionRow.subjectDid - } - - await labelTxn.formatAndCreate(this.appContext.cfg.labelerDid, uri, cid, { - create: actionRow.negateLabelVals - ? actionRow.negateLabelVals.split(' ') - : undefined, - negate: actionRow.createLabelVals - ? actionRow.createLabelVals.split(' ') - : undefined, - }) - } - async revertAction(actionRow: ModerationActionRow) { return this.appContext.db.transaction(async (dbTxn) => { const moderationTxn = this.appContext.services.moderation(dbTxn) - const labelTxn = this.appContext.services.appView.label(dbTxn) await moderationTxn.revertAction({ id: actionRow.id, createdBy: actionRow.createdBy, createdAt: new Date(), reason: `[SCHEDULED_REVERSAL] Reverting action as originally scheduled`, }) - await this.reverseLabels(labelTxn, actionRow) }) } diff --git a/packages/pds/src/db/tables/label.ts b/packages/pds/src/db/tables/label.ts deleted file mode 100644 index 1837faab1c8..00000000000 --- a/packages/pds/src/db/tables/label.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const tableName = 'label' - -export interface Label { - src: string - uri: string - cid: string - val: string - neg: 0 | 1 // @TODO convert to boolean in app-view - cts: string -} - -export type PartialDB = { [tableName]: Label } diff --git a/packages/pds/src/event-stream/message-queue.ts b/packages/pds/src/event-stream/message-queue.ts deleted file mode 100644 index 20aa5d5b862..00000000000 --- a/packages/pds/src/event-stream/message-queue.ts +++ /dev/null @@ -1,48 +0,0 @@ -import Database from '../db' -import { dbLogger as log } from '../logger' -import { MessageQueue, Listenable, Listener, MessageOfType } from './types' - -// @NOTE A message dispatcher for loose coupling within db transactions. -// Messages are handled immediately. This should not be around for long. -export class MessageDispatcher implements MessageQueue { - private destroyed = false - private listeners: Map = new Map() - - async send( - tx: Database, - message: MessageOfType | MessageOfType[], - ): Promise { - if (this.destroyed) return - const messages = Array.isArray(message) ? message : [message] - for (const msg of messages) { - await this.handleMessage(tx, msg) - } - } - - listen>( - topic: T, - listenable: Listenable, - ) { - const listeners = this.listeners.get(topic) ?? [] - listeners.push(listenable.listener as Listener) // @TODO avoid upcast - this.listeners.set(topic, listeners) - } - - destroy(): void { - this.destroyed = true - } - - private async handleMessage(db: Database, message: MessageOfType) { - const listeners = this.listeners.get(message.type) - if (!listeners?.length) { - return log.error({ message }, `no listeners for event: ${message.type}`) - } - for (const listener of listeners) { - await listener({ message, db }) - } - } - - // Unused by MessageDispatcher - async processNext(): Promise {} - async processAll(): Promise {} -} diff --git a/packages/pds/src/event-stream/messages.ts b/packages/pds/src/event-stream/messages.ts deleted file mode 100644 index 9c5f00baca6..00000000000 --- a/packages/pds/src/event-stream/messages.ts +++ /dev/null @@ -1,51 +0,0 @@ -// Below specific to message dispatcher - -import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/syntax' -import { WriteOpAction } from '@atproto/repo' - -export type IndexRecord = { - type: 'index_record' - action: WriteOpAction.Create | WriteOpAction.Update - uri: AtUri - cid: CID - obj: unknown - timestamp: string -} - -export type DeleteRecord = { - type: 'delete_record' - uri: AtUri - cascading: boolean -} - -export type DeleteRepo = { - type: 'delete_repo' - did: string -} - -export const indexRecord = ( - uri: AtUri, - cid: CID, - obj: unknown, - action: WriteOpAction.Create | WriteOpAction.Update, - timestamp: string, -): IndexRecord => ({ - type: 'index_record', - uri, - cid, - obj, - action, - timestamp, -}) - -export const deleteRecord = (uri: AtUri, cascading: boolean): DeleteRecord => ({ - type: 'delete_record', - uri, - cascading, -}) - -export const deleteRepo = (did: string): DeleteRepo => ({ - type: 'delete_repo', - did, -}) diff --git a/packages/pds/src/event-stream/types.ts b/packages/pds/src/event-stream/types.ts deleted file mode 100644 index aad8fc5f01b..00000000000 --- a/packages/pds/src/event-stream/types.ts +++ /dev/null @@ -1,38 +0,0 @@ -import Database from '../db' - -export type MessageOfType = { - type: T - [s: string]: unknown -} - -export type Listener = (ctx: { - message: M - db: Database -}) => Promise - -export interface Listenable { - listener: Listener -} - -export abstract class Consumer - implements Listenable -{ - abstract dispatch(ctx: { - db: Database - message: M - }): Promise - get listener() { - return this.dispatch.bind(this) - } -} - -export interface MessageQueue { - send(tx: Database, message: MessageOfType | MessageOfType[]): Promise - listen>( - topic: T, - listenable: Listenable, - ): void - processNext(): Promise - processAll(): Promise - destroy(): void -} diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index 36f15adae8e..fb08ef375e9 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -21,7 +21,6 @@ import { Options as XrpcServerOptions, } from '@atproto/xrpc-server' import { DAY, HOUR, MINUTE } from '@atproto/common' -import * as appviewConsumers from './app-view/event-stream/consumers' import inProcessAppView from './app-view/api' import API from './api' import * as basicRoutes from './basic-routes' @@ -35,16 +34,13 @@ import { ServerConfig } from './config' import { ServerMailer } from './mailer' import { ModerationMailer } from './mailer/moderation' import { createServer } from './lexicon' -import { MessageDispatcher } from './event-stream/message-queue' import { createServices } from './services' import { createHttpTerminator, HttpTerminator } from 'http-terminator' import AppContext from './context' import { Sequencer, SequencerLeader } from './sequencer' -import { Labeler, HiveLabeler, KeywordLabeler } from './labeler' -import { BackgroundQueue } from './event-stream/background-queue' +import { BackgroundQueue } from './background' import DidSqlCache from './did-cache' import { Crawlers } from './crawlers' -import { LabelCache } from './label-cache' import { getRedisClient } from './redis' import { RuntimeFlags } from './runtime-flags' @@ -95,7 +91,6 @@ export class PDS { backupNameservers: config.handleResolveNameservers, }) - const messageDispatcher = new MessageDispatcher() const sequencer = new Sequencer(db) const sequencerLeader = config.sequencerLeaderEnabled ? new SequencerLeader(db, config.sequencerLeaderLockId) @@ -129,35 +124,11 @@ export class PDS { config.crawlersToNotify ?? [], ) - let labeler: Labeler - if (config.hiveApiKey) { - labeler = new HiveLabeler({ - db, - blobstore, - backgroundQueue, - labelerDid: config.labelerDid, - hiveApiKey: config.hiveApiKey, - keywords: config.labelerKeywords, - }) - } else { - labeler = new KeywordLabeler({ - db, - blobstore, - backgroundQueue, - labelerDid: config.labelerDid, - keywords: config.labelerKeywords, - }) - } - - const labelCache = new LabelCache(db) const appviewAgent = new AtpAgent({ service: config.bskyAppViewEndpoint }) const services = createServices({ repoSigningKey, - messageDispatcher, blobstore, - labeler, - labelCache, appviewAgent, appviewDid: config.bskyAppViewDid, appviewCdnUrlPattern: config.bskyAppViewCdnUrlPattern, @@ -185,11 +156,8 @@ export class PDS { didCache, cfg: config, auth, - messageDispatcher, sequencer, sequencerLeader, - labeler, - labelCache, runtimeFlags, services, mailer, @@ -293,11 +261,9 @@ export class PDS { } } }, 500) - appviewConsumers.listen(this.ctx) this.ctx.sequencerLeader?.run() await this.ctx.sequencer.start() await this.ctx.db.startListeningToChannels() - this.ctx.labelCache.start() await this.ctx.runtimeFlags.start() const server = this.app.listen(this.ctx.cfg.port) this.server = server @@ -309,7 +275,6 @@ export class PDS { async destroy(): Promise { await this.ctx.runtimeFlags.destroy() - this.ctx.labelCache.stop() await this.ctx.sequencerLeader?.destroy() await this.terminator?.terminate() await this.ctx.backgroundQueue.destroy() diff --git a/packages/pds/src/label-cache.ts b/packages/pds/src/label-cache.ts deleted file mode 100644 index e4f23daa599..00000000000 --- a/packages/pds/src/label-cache.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { wait } from '@atproto/common' -import Database from './db' -import { Label } from './db/tables/label' -import { labelerLogger as log } from './logger' - -export class LabelCache { - bySubject: Record = {} - latestLabel = '' - refreshes = 0 - - destroyed = false - - constructor(public db: Database) {} - - start() { - this.poll() - } - - async fullRefresh() { - const allLabels = await this.db.db.selectFrom('label').selectAll().execute() - this.wipeCache() - this.processLabels(allLabels) - } - - async partialRefresh() { - const labels = await this.db.db - .selectFrom('label') - .selectAll() - .where('cts', '>', this.latestLabel) - .execute() - this.processLabels(labels) - } - - async poll() { - try { - if (this.destroyed) return - if (this.refreshes >= 120) { - await this.fullRefresh() - this.refreshes = 0 - } else { - await this.partialRefresh() - this.refreshes++ - } - } catch (err) { - log.error( - { err, latestLabel: this.latestLabel, refreshes: this.refreshes }, - 'label cache failed to refresh', - ) - } - await wait(500) - this.poll() - } - - processLabels(labels: Label[]) { - for (const label of labels) { - if (label.cts > this.latestLabel) { - this.latestLabel = label.cts - } - this.bySubject[label.uri] ??= [] - this.bySubject[label.uri].push(label) - } - } - - wipeCache() { - this.bySubject = {} - } - - stop() { - this.destroyed = true - } - - forSubject(subject: string, includeNeg = false): Label[] { - const labels = this.bySubject[subject] ?? [] - return includeNeg ? labels : labels.filter((l) => l.neg === 0) - } - - forSubjects(subjects: string[], includeNeg?: boolean): Label[] { - let labels: Label[] = [] - const alreadyAdded = new Set() - for (const subject of subjects) { - if (alreadyAdded.has(subject)) { - continue - } - const subLabels = this.forSubject(subject, includeNeg) - labels = [...labels, ...subLabels] - alreadyAdded.add(subject) - } - return labels - } -} diff --git a/packages/pds/src/labeler/base.ts b/packages/pds/src/labeler/base.ts deleted file mode 100644 index 8a52ed2c717..00000000000 --- a/packages/pds/src/labeler/base.ts +++ /dev/null @@ -1,72 +0,0 @@ -import Database from '../db' -import { BlobStore, cidForRecord } from '@atproto/repo' -import { dedupe, getFieldsFromRecord } from './util' -import { AtUri } from '@atproto/syntax' -import { labelerLogger as log } from '../logger' -import { BackgroundQueue } from '../event-stream/background-queue' -import { CID } from 'multiformats/cid' - -export abstract class Labeler { - public db: Database - public blobstore: BlobStore - public labelerDid: string - public backgroundQueue: BackgroundQueue - constructor(opts: { - db: Database - blobstore: BlobStore - labelerDid: string - backgroundQueue: BackgroundQueue - }) { - this.db = opts.db - this.blobstore = opts.blobstore - this.labelerDid = opts.labelerDid - this.backgroundQueue = opts.backgroundQueue - } - - processRecord(uri: AtUri, obj: unknown) { - this.backgroundQueue.add(() => - this.createAndStoreLabels(uri, obj).catch((err) => { - log.error( - { err, uri: uri.toString(), record: obj }, - 'failed to label record', - ) - }), - ) - } - - async createAndStoreLabels(uri: AtUri, obj: unknown): Promise { - const labels = await this.labelRecord(obj) - if (labels.length < 1) return - const cid = await cidForRecord(obj) - const rows = labels.map((val) => ({ - src: this.labelerDid, - uri: uri.toString(), - cid: cid.toString(), - val, - neg: 0 as const, - cts: new Date().toISOString(), - })) - - await this.db.db - .insertInto('label') - .values(rows) - .onConflict((oc) => oc.doNothing()) - .execute() - } - - async labelRecord(obj: unknown): Promise { - const { text, imgs } = getFieldsFromRecord(obj) - const txtLabels = await this.labelText(text.join(' ')) - const imgLabels = await Promise.all( - imgs.map(async (cid) => this.labelImg(cid)), - ) - return dedupe([...txtLabels, ...imgLabels.flat()]) - } - - abstract labelText(text: string): Promise - abstract labelImg(cid: CID): Promise - - async processAll() { - await this.backgroundQueue.processAll() - } -} diff --git a/packages/pds/src/labeler/hive.ts b/packages/pds/src/labeler/hive.ts deleted file mode 100644 index e4a0bc48045..00000000000 --- a/packages/pds/src/labeler/hive.ts +++ /dev/null @@ -1,191 +0,0 @@ -import stream from 'stream' -import axios from 'axios' -import FormData from 'form-data' -import { Labeler } from './base' -import Database from '../db' -import { BlobStore } from '@atproto/repo' -import { keywordLabeling } from './util' -import { BackgroundQueue } from '../event-stream/background-queue' -import { CID } from 'multiformats/cid' - -const HIVE_ENDPOINT = 'https://api.thehive.ai/api/v2/task/sync' - -export class HiveLabeler extends Labeler { - hiveApiKey: string - keywords: Record - - constructor(opts: { - db: Database - blobstore: BlobStore - backgroundQueue: BackgroundQueue - labelerDid: string - hiveApiKey: string - keywords: Record - }) { - const { db, blobstore, backgroundQueue, labelerDid, hiveApiKey, keywords } = - opts - super({ db, blobstore, backgroundQueue, labelerDid }) - this.hiveApiKey = hiveApiKey - this.keywords = keywords - } - - async labelText(text: string): Promise { - return keywordLabeling(this.keywords, text) - } - - async labelImg(cid: CID): Promise { - const stream = await this.blobstore.getStream(cid) - return labelBlob(stream, this.hiveApiKey) - } -} - -export const labelBlob = async ( - blob: stream.Readable, - hiveApiKey: string, -): Promise => { - const classes = await makeHiveReq(blob, hiveApiKey) - return summarizeLabels(classes) -} - -export const makeHiveReq = async ( - blob: stream.Readable, - hiveApiKey: string, -): Promise => { - const form = new FormData() - form.append('media', blob) - const res = await axios.post(HIVE_ENDPOINT, form, { - headers: { - 'Content-Type': 'multipart/form-data', - authorization: `token ${hiveApiKey}`, - accept: 'application/json', - }, - }) - return respToClasses(res.data) -} - -export const respToClasses = (res: HiveResp): HiveRespClass[] => { - const classes: HiveRespClass[] = [] - for (const status of res.status) { - for (const out of status.response.output) { - for (const cls of out.classes) { - classes.push(cls) - } - } - } - return classes -} - -// Matches only one (or none) of: porn, sexual, nudity -// -// porn: sexual and nudity. including both explicit activity or full-frontal and suggestive/intent -// sexual: sexually suggestive, not explicit; may include some forms of nudity -// nudity: non-sexual nudity (eg, artistic, possibly some photographic) -// -// hive docs/definitions: https://docs.thehive.ai/docs/sexual-content -export const sexualLabels = (classes: HiveRespClass[]): string[] => { - const scores = {} - - for (const cls of classes) { - scores[cls.class] = cls.score - } - - // first check if porn... - for (const pornClass of [ - 'yes_sexual_activity', - 'animal_genitalia_and_human', - 'yes_realistic_nsfw', - ]) { - if (scores[pornClass] >= 0.9) { - return ['porn'] - } - } - if (scores['general_nsfw'] >= 0.9) { - // special case for some anime examples - if (scores['animated_animal_genitalia'] >= 0.5) { - return ['porn'] - } - // special case for some pornographic/explicit classic drawings - if (scores['yes_undressed'] >= 0.9 && scores['yes_sexual_activity'] > 0.9) { - return ['porn'] - } - } - - // then check for sexual suggestive (which may include nudity)... - for (const sexualClass of ['yes_sexual_intent', 'yes_sex_toy']) { - if (scores[sexualClass] >= 0.9) { - return ['sexual'] - } - } - if (scores['yes_undressed'] >= 0.9) { - // special case for bondage examples - if (scores['yes_sex_toy'] > 0.75) { - return ['sexual'] - } - } - - // then non-sexual nudity... - for (const nudityClass of [ - 'yes_male_nudity', - 'yes_female_nudity', - 'yes_undressed', - ]) { - if (scores[nudityClass] >= 0.9) { - return ['nudity'] - } - } - - // then finally flag remaining "underwear" images in to sexually suggestive - // (after non-sexual content already labeled above) - for (const nudityClass of ['yes_male_underwear', 'yes_female_underwear']) { - if (scores[nudityClass] >= 0.9) { - return ['sexual'] - } - } - - return [] -} - -// gore and violence: https://docs.thehive.ai/docs/class-descriptions-violence-gore -const labelForClass = { - very_bloody: 'gore', - human_corpse: 'corpse', - hanging: 'corpse', -} -const labelForClassLessSensitive = { - yes_self_harm: 'self-harm', -} - -export const summarizeLabels = (classes: HiveRespClass[]): string[] => { - const labels: string[] = sexualLabels(classes) - for (const cls of classes) { - if (labelForClass[cls.class] && cls.score >= 0.9) { - labels.push(labelForClass[cls.class]) - } - } - for (const cls of classes) { - if (labelForClassLessSensitive[cls.class] && cls.score >= 0.96) { - labels.push(labelForClassLessSensitive[cls.class]) - } - } - return labels -} - -type HiveResp = { - status: HiveRespStatus[] -} - -type HiveRespStatus = { - response: { - output: HiveRespOutput[] - } -} - -type HiveRespOutput = { - time: number - classes: HiveRespClass[] -} - -type HiveRespClass = { - class: string - score: number -} diff --git a/packages/pds/src/labeler/index.ts b/packages/pds/src/labeler/index.ts deleted file mode 100644 index cd6d2a64345..00000000000 --- a/packages/pds/src/labeler/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './base' -export * from './hive' -export * from './keyword' diff --git a/packages/pds/src/labeler/keyword.ts b/packages/pds/src/labeler/keyword.ts deleted file mode 100644 index 8838e87486a..00000000000 --- a/packages/pds/src/labeler/keyword.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { BlobStore } from '@atproto/repo' -import Database from '../db' -import { Labeler } from './base' -import { keywordLabeling } from './util' -import { BackgroundQueue } from '../event-stream/background-queue' - -export class KeywordLabeler extends Labeler { - keywords: Record - - constructor(opts: { - db: Database - blobstore: BlobStore - backgroundQueue: BackgroundQueue - labelerDid: string - keywords: Record - }) { - const { db, blobstore, backgroundQueue, labelerDid, keywords } = opts - super({ db, blobstore, backgroundQueue, labelerDid }) - this.keywords = keywords - } - - async labelText(text: string): Promise { - return keywordLabeling(this.keywords, text) - } - - async labelImg(): Promise { - return [] - } -} diff --git a/packages/pds/src/labeler/util.ts b/packages/pds/src/labeler/util.ts deleted file mode 100644 index 4175886a542..00000000000 --- a/packages/pds/src/labeler/util.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { CID } from 'multiformats/cid' -import * as lex from '../lexicon/lexicons' -import { Record as PostRecord } from '../lexicon/types/app/bsky/feed/post' -import { Record as ProfileRecord } from '../lexicon/types/app/bsky/actor/profile' -import { isMain as isEmbedImage } from '../lexicon/types/app/bsky/embed/images' -import { isMain as isEmbedExternal } from '../lexicon/types/app/bsky/embed/external' -import { isMain as isEmbedRecordWithMedia } from '../lexicon/types/app/bsky/embed/recordWithMedia' - -type RecordFields = { - text: string[] - imgs: CID[] -} - -export const getFieldsFromRecord = (record: unknown): RecordFields => { - if (isPost(record)) { - return getFieldsFromPost(record) - // @TODO add back in profile labeling - // } else if (isProfile(record)) { - // return getFieldsFromProfile(record) - } else { - return { text: [], imgs: [] } - } -} - -export const getFieldsFromPost = (record: PostRecord): RecordFields => { - const text: string[] = [] - const imgs: CID[] = [] - text.push(record.text) - const embeds = separateEmbeds(record.embed) - for (const embed of embeds) { - if (isEmbedImage(embed)) { - for (const img of embed.images) { - imgs.push(img.image.ref) - text.push(img.alt) - } - } else if (isEmbedExternal(embed)) { - if (embed.external.thumb) { - imgs.push(embed.external.thumb.ref) - } - text.push(embed.external.title) - text.push(embed.external.description) - } - } - return { text, imgs } -} - -export const getFieldsFromProfile = (record: ProfileRecord): RecordFields => { - const text: string[] = [] - const imgs: CID[] = [] - if (record.displayName) { - text.push(record.displayName) - } - if (record.description) { - text.push(record.description) - } - if (record.avatar) { - imgs.push(record.avatar.ref) - } - if (record.banner) { - imgs.push(record.banner.ref) - } - return { text, imgs } -} - -export const dedupe = (str: string[]): string[] => { - const set = new Set(str) - return [...set] -} - -export const isPost = (obj: unknown): obj is PostRecord => { - return isRecordType(obj, 'app.bsky.feed.post') -} - -export const isProfile = (obj: unknown): obj is ProfileRecord => { - return isRecordType(obj, 'app.bsky.actor.profile') -} - -export const isRecordType = (obj: unknown, lexId: string): boolean => { - try { - lex.lexicons.assertValidRecord(lexId, obj) - return true - } catch { - return false - } -} - -export const keywordLabeling = ( - keywords: Record, - text: string, -): string[] => { - const lowerText = text.toLowerCase() - const labels: string[] = [] - for (const word of Object.keys(keywords)) { - if (lowerText.includes(word)) { - labels.push(keywords[word]) - } - } - return labels -} - -const separateEmbeds = (embed: PostRecord['embed']) => { - if (!embed) { - return [] - } - if (isEmbedRecordWithMedia(embed)) { - return [{ $type: lex.ids.AppBskyEmbedRecord, ...embed.record }, embed.media] - } - return [embed] -} diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 3ec69a3503f..3dd2b5104cc 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -107,7 +107,6 @@ import * as AppBskyNotificationGetUnreadCount from './types/app/bsky/notificatio import * as AppBskyNotificationListNotifications from './types/app/bsky/notification/listNotifications' import * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/registerPush' import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen' -import * as AppBskyUnspeccedApplyLabels from './types/app/bsky/unspecced/applyLabels' import * as AppBskyUnspeccedGetPopular from './types/app/bsky/unspecced/getPopular' import * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators' import * as AppBskyUnspeccedGetTimelineSkeleton from './types/app/bsky/unspecced/getTimelineSkeleton' @@ -1402,17 +1401,6 @@ export class UnspeccedNS { this._server = server } - applyLabels( - cfg: ConfigOf< - AV, - AppBskyUnspeccedApplyLabels.Handler>, - AppBskyUnspeccedApplyLabels.HandlerReqCtx> - >, - ) { - const nsid = 'app.bsky.unspecced.applyLabels' // @ts-ignore - return this._server.xrpc.method(nsid, cfg) - } - getPopular( cfg: ConfigOf< AV, diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index b624ce335ed..177b63808f4 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -6905,32 +6905,6 @@ export const schemaDict = { }, }, }, - AppBskyUnspeccedApplyLabels: { - lexicon: 1, - id: 'app.bsky.unspecced.applyLabels', - defs: { - main: { - type: 'procedure', - description: 'Allow a labeler to apply labels directly.', - input: { - encoding: 'application/json', - schema: { - type: 'object', - required: ['labels'], - properties: { - labels: { - type: 'array', - items: { - type: 'ref', - ref: 'lex:com.atproto.label.defs#label', - }, - }, - }, - }, - }, - }, - }, - }, AppBskyUnspeccedDefs: { lexicon: 1, id: 'app.bsky.unspecced.defs', @@ -7359,7 +7333,6 @@ export const ids = { AppBskyNotificationRegisterPush: 'app.bsky.notification.registerPush', AppBskyNotificationUpdateSeen: 'app.bsky.notification.updateSeen', AppBskyRichtextFacet: 'app.bsky.richtext.facet', - AppBskyUnspeccedApplyLabels: 'app.bsky.unspecced.applyLabels', AppBskyUnspeccedDefs: 'app.bsky.unspecced.defs', AppBskyUnspeccedGetPopular: 'app.bsky.unspecced.getPopular', AppBskyUnspeccedGetPopularFeedGenerators: diff --git a/packages/pds/src/lexicon/types/app/bsky/unspecced/applyLabels.ts b/packages/pds/src/lexicon/types/app/bsky/unspecced/applyLabels.ts deleted file mode 100644 index 1d359a9547d..00000000000 --- a/packages/pds/src/lexicon/types/app/bsky/unspecced/applyLabels.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * GENERATED CODE - DO NOT MODIFY - */ -import express from 'express' -import { ValidationResult, BlobRef } from '@atproto/lexicon' -import { lexicons } from '../../../../lexicons' -import { isObj, hasProp } from '../../../../util' -import { CID } from 'multiformats/cid' -import { HandlerAuth } from '@atproto/xrpc-server' -import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' - -export interface QueryParams {} - -export interface InputSchema { - labels: ComAtprotoLabelDefs.Label[] - [k: string]: unknown -} - -export interface HandlerInput { - encoding: 'application/json' - body: InputSchema -} - -export interface HandlerError { - status: number - message?: string -} - -export type HandlerOutput = HandlerError | void -export type HandlerReqCtx = { - auth: HA - params: QueryParams - input: HandlerInput - req: express.Request - res: express.Response -} -export type Handler = ( - ctx: HandlerReqCtx, -) => Promise | HandlerOutput diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts index 8173d9160af..0cb293ff26c 100644 --- a/packages/pds/src/services/account/index.ts +++ b/packages/pds/src/services/account/index.ts @@ -1,18 +1,17 @@ -import { WhereInterface, sql } from 'kysely' +import { sql } from 'kysely' import { dbLogger as log } from '../../logger' import Database from '../../db' import * as scrypt from '../../db/scrypt' import { UserAccountEntry } from '../../db/tables/user-account' import { DidHandle } from '../../db/tables/did-handle' import { RepoRoot } from '../../db/tables/repo-root' -import { DbRef, countAll, notSoftDeletedClause } from '../../db/util' +import { countAll, notSoftDeletedClause } from '../../db/util' import { getUserSearchQueryPg, getUserSearchQuerySqlite } from '../util/search' import { paginate, TimeCidKeyset } from '../../db/pagination' import * as sequencer from '../../sequencer' import { AppPassword } from '../../lexicon/types/com/atproto/server/createAppPassword' import { randomStr } from '@atproto/crypto' import { InvalidRequestError } from '@atproto/xrpc-server' -import { NotEmptyArray } from '@atproto/common' export class AccountService { constructor(public db: Database) {} @@ -355,27 +354,6 @@ export class AccountService { .execute() } - whereNotMuted>( - qb: W, - requester: string, - refs: NotEmptyArray, - ) { - const subjectRefs = sql.join(refs) - const actorMute = this.db.db - .selectFrom('mute') - .where('mutedByDid', '=', requester) - .where('did', 'in', sql`(${subjectRefs})`) - .select('did as muted') - const listMute = this.db.db - .selectFrom('list_item') - .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') - .where('list_mute.mutedByDid', '=', requester) - .whereRef('list_item.subjectDid', 'in', sql`(${subjectRefs})`) - .select('list_item.subjectDid as muted') - // Splitting the mute from list-mute checks seems to be more flexible for the query-planner and often quicker - return qb.whereNotExists(actorMute).whereNotExists(listMute) - } - async search(opts: { searchField?: 'did' | 'handle' term: string @@ -397,13 +375,9 @@ export class AccountService { const builder = this.db.dialect === 'pg' ? getUserSearchQueryPg(this.db, opts) - .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') .selectAll('did_handle') .selectAll('repo_root') - .select('results.distance as distance') : getUserSearchQuerySqlite(this.db, opts) - .leftJoin('profile', 'profile.creator', 'did_handle.did') // @TODO leaky, for getUserSearchQuerySqlite() - .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') .selectAll('did_handle') .selectAll('repo_root') .select(sql`0`.as('distance')) diff --git a/packages/pds/src/services/index.ts b/packages/pds/src/services/index.ts index a60451785ec..3dd376a8dd6 100644 --- a/packages/pds/src/services/index.ts +++ b/packages/pds/src/services/index.ts @@ -2,26 +2,18 @@ import { AtpAgent } from '@atproto/api' import * as crypto from '@atproto/crypto' import { BlobStore } from '@atproto/repo' import Database from '../db' -import { MessageDispatcher } from '../event-stream/message-queue' import { AccountService } from './account' import { AuthService } from './auth' import { RecordService } from './record' import { RepoService } from './repo' import { ModerationService } from './moderation' -import { IndexingService } from '../app-view/services/indexing' -import { Labeler } from '../labeler' -import { LabelService } from '../app-view/services/label' -import { BackgroundQueue } from '../event-stream/background-queue' +import { BackgroundQueue } from '../background' import { Crawlers } from '../crawlers' -import { LabelCache } from '../label-cache' import { LocalService } from './local' export function createServices(resources: { repoSigningKey: crypto.Keypair - messageDispatcher: MessageDispatcher blobstore: BlobStore - labeler: Labeler - labelCache: LabelCache appviewAgent?: AtpAgent appviewDid?: string appviewCdnUrlPattern?: string @@ -30,10 +22,7 @@ export function createServices(resources: { }): Services { const { repoSigningKey, - messageDispatcher, blobstore, - labeler, - labelCache, appviewAgent, appviewDid, appviewCdnUrlPattern, @@ -43,14 +32,12 @@ export function createServices(resources: { return { account: AccountService.creator(), auth: AuthService.creator(), - record: RecordService.creator(messageDispatcher), + record: RecordService.creator(), repo: RepoService.creator( repoSigningKey, - messageDispatcher, blobstore, backgroundQueue, crawlers, - labeler, ), local: LocalService.creator( repoSigningKey, @@ -58,11 +45,7 @@ export function createServices(resources: { appviewDid, appviewCdnUrlPattern, ), - moderation: ModerationService.creator(messageDispatcher, blobstore), - appView: { - indexing: IndexingService.creator(backgroundQueue), - label: LabelService.creator(labelCache), - }, + moderation: ModerationService.creator(blobstore), } } @@ -73,10 +56,6 @@ export type Services = { repo: FromDb local: FromDb moderation: FromDb - appView: { - indexing: FromDb - label: FromDb - } } type FromDb = (db: Database) => T diff --git a/packages/pds/src/services/moderation/index.ts b/packages/pds/src/services/moderation/index.ts index 6104ffe728b..9e46332cf33 100644 --- a/packages/pds/src/services/moderation/index.ts +++ b/packages/pds/src/services/moderation/index.ts @@ -4,7 +4,6 @@ import { BlobStore } from '@atproto/repo' import { AtUri } from '@atproto/syntax' import { InvalidRequestError } from '@atproto/xrpc-server' import Database from '../../db' -import { MessageQueue } from '../../event-stream/types' import { ModerationAction, ModerationReport } from '../../db/tables/moderation' import { RecordService } from '../record' import { ModerationViews } from './views' @@ -13,21 +12,16 @@ import { TAKEDOWN } from '../../lexicon/types/com/atproto/admin/defs' import { addHoursToDate } from '../../util/date' export class ModerationService { - constructor( - public db: Database, - public messageDispatcher: MessageQueue, - public blobstore: BlobStore, - ) {} - - static creator(messageDispatcher: MessageQueue, blobstore: BlobStore) { - return (db: Database) => - new ModerationService(db, messageDispatcher, blobstore) + constructor(public db: Database, public blobstore: BlobStore) {} + + static creator(blobstore: BlobStore) { + return (db: Database) => new ModerationService(db, blobstore) } - views = new ModerationViews(this.db, this.messageDispatcher) + views = new ModerationViews(this.db) services = { - record: RecordService.creator(this.messageDispatcher), + record: RecordService.creator(), } async getAction(id: number): Promise { diff --git a/packages/pds/src/services/moderation/views.ts b/packages/pds/src/services/moderation/views.ts index 2403de729fb..e8d89620d73 100644 --- a/packages/pds/src/services/moderation/views.ts +++ b/packages/pds/src/services/moderation/views.ts @@ -2,7 +2,6 @@ import { Selectable } from 'kysely' import { ArrayEl, cborBytesToRecord } from '@atproto/common' import { AtUri } from '@atproto/syntax' import Database from '../../db' -import { MessageQueue } from '../../event-stream/types' import { DidHandle } from '../../db/tables/did-handle' import { RepoRoot } from '../../db/tables/repo-root' import { @@ -17,19 +16,18 @@ import { BlobView, } from '../../lexicon/types/com/atproto/admin/defs' import { OutputSchema as ReportOutput } from '../../lexicon/types/com/atproto/moderation/createReport' -import { Label } from '../../lexicon/types/com/atproto/label/defs' import { ModerationAction } from '../../db/tables/moderation' import { AccountService } from '../account' import { RecordService } from '../record' import { ModerationReportRowWithHandle } from '.' -import { getSelfLabels } from '../../app-view/services/label' +import { ids } from '../../lexicon/lexicons' export class ModerationViews { - constructor(private db: Database, private messageDispatcher: MessageQueue) {} + constructor(private db: Database) {} services = { account: AccountService.creator(), - record: RecordService.creator(this.messageDispatcher), + record: RecordService.creator(), } repo(result: RepoResult, opts: ModViewOptions): Promise @@ -45,10 +43,15 @@ export class ModerationViews { await this.db.db .selectFrom('did_handle') .leftJoin('user_account', 'user_account.did', 'did_handle.did') - .leftJoin('profile', 'profile.creator', 'did_handle.did') + .leftJoin('record as profile_record', (join) => + join + .onRef('profile_record.did', '=', 'did_handle.did') + .on('profile_record.collection', '=', ids.AppBskyActorProfile) + .on('profile_record.rkey', '=', 'self'), + ) .leftJoin('ipld_block as profile_block', (join) => join - .onRef('profile_block.cid', '=', 'profile.cid') + .onRef('profile_block.cid', '=', 'profile_record.cid') .onRef('profile_block.creator', '=', 'did_handle.did'), ) .where( @@ -143,10 +146,9 @@ export class ModerationViews { .execute(), this.services.account(this.db).getAccountInviteCodes(repo.did), ]) - const [reports, actions, labels] = await Promise.all([ + const [reports, actions] = await Promise.all([ this.report(reportResults), this.action(actionResults), - this.labels(repo.did), ]) return { ...repo, @@ -156,7 +158,6 @@ export class ModerationViews { actions, }, invites: inviteCodes, - labels, } } @@ -270,17 +271,11 @@ export class ModerationViews { .selectAll() .execute(), ]) - const [reports, actions, blobs, labels] = await Promise.all([ + const [reports, actions, blobs] = await Promise.all([ this.report(reportResults), this.action(actionResults), this.blob(record.blobCids), - this.labels(record.uri), ]) - const selfLabels = getSelfLabels({ - uri: result.uri, - cid: result.cid, - record: result.value as Record, - }) return { ...record, blobs, @@ -289,7 +284,6 @@ export class ModerationViews { reports, actions, }, - labels: [...labels, ...selfLabels], } } @@ -609,21 +603,6 @@ export class ModerationViews { } }) } - - // @TODO: call into label service instead on AppView - async labels(subject: string, includeNeg?: boolean): Promise { - const res = await this.db.db - .selectFrom('label') - .where('label.uri', '=', subject) - .if(!includeNeg, (qb) => qb.where('neg', '=', 0)) - .selectAll() - .execute() - return res.map((l) => ({ - ...l, - cid: l.cid === '' ? undefined : l.cid, - neg: l.neg === 1, - })) - } } type RepoResult = DidHandle & RepoRoot diff --git a/packages/pds/src/services/record/index.ts b/packages/pds/src/services/record/index.ts index 857ae7f1d95..3cdb7bbc05e 100644 --- a/packages/pds/src/services/record/index.ts +++ b/packages/pds/src/services/record/index.ts @@ -6,19 +6,13 @@ import { dbLogger as log } from '../../logger' import Database from '../../db' import { notSoftDeletedClause } from '../../db/util' import { Backlink } from '../../db/tables/backlink' -import { MessageQueue } from '../../event-stream/types' -import { - indexRecord, - deleteRecord, - deleteRepo, -} from '../../event-stream/messages' import { ids } from '../../lexicon/lexicons' export class RecordService { - constructor(public db: Database, public messageDispatcher: MessageQueue) {} + constructor(public db: Database) {} - static creator(messageDispatcher: MessageQueue) { - return (db: Database) => new RecordService(db, messageDispatcher) + static creator() { + return (db: Database) => new RecordService(db) } async indexRecord( @@ -70,30 +64,19 @@ export class RecordService { } await this.addBacklinks(backlinks) - // Send to indexers - await this.messageDispatcher.send( - this.db, - indexRecord(uri, cid, obj, action, record.indexedAt), - ) - log.info({ uri }, 'indexed record') } - async deleteRecord(uri: AtUri, cascading = false) { + async deleteRecord(uri: AtUri) { this.db.assertTransaction() log.debug({ uri }, 'deleting indexed record') const deleteQuery = this.db.db .deleteFrom('record') .where('uri', '=', uri.toString()) - .execute() - await this.db.db + const backlinkQuery = this.db.db .deleteFrom('backlink') .where('uri', '=', uri.toString()) - .execute() - await Promise.all([ - this.messageDispatcher.send(this.db, deleteRecord(uri, cascading)), - deleteQuery, - ]) + await Promise.all([deleteQuery.execute(), backlinkQuery.execute()]) log.info({ uri }, 'deleted indexed record') } @@ -233,7 +216,6 @@ export class RecordService { async deleteForActor(did: string) { // Not done in transaction because it would be too long, prone to contention. // Also, this can safely be run multiple times if it fails. - await this.messageDispatcher.send(this.db, deleteRepo(did)) // Needs record table await this.db.db.deleteFrom('record').where('did', '=', did).execute() await this.db.db .deleteFrom('user_notification') diff --git a/packages/pds/src/services/repo/blobs.ts b/packages/pds/src/services/repo/blobs.ts index 7078f66a76e..a366f4def1f 100644 --- a/packages/pds/src/services/repo/blobs.ts +++ b/packages/pds/src/services/repo/blobs.ts @@ -13,7 +13,7 @@ import { Blob as BlobTable } from '../../db/tables/blob' import * as img from '../../image' import { BlobRef } from '@atproto/lexicon' import { PreparedDelete, PreparedUpdate } from '../../repo' -import { BackgroundQueue } from '../../event-stream/background-queue' +import { BackgroundQueue } from '../../background' export class RepoBlobs { constructor( diff --git a/packages/pds/src/services/repo/index.ts b/packages/pds/src/services/repo/index.ts index 8b8db8eb6be..406635b736d 100644 --- a/packages/pds/src/services/repo/index.ts +++ b/packages/pds/src/services/repo/index.ts @@ -4,7 +4,6 @@ import { BlobStore, CommitData, Repo, WriteOpAction } from '@atproto/repo' import { InvalidRequestError } from '@atproto/xrpc-server' import { AtUri } from '@atproto/syntax' import Database from '../../db' -import { MessageQueue } from '../../event-stream/types' import SqlRepoStorage from '../../sql-repo-storage' import { BadCommitSwapError, @@ -16,9 +15,8 @@ import { RepoBlobs } from './blobs' import { createWriteToOp, writeToOp } from '../../repo' import { RecordService } from '../record' import * as sequencer from '../../sequencer' -import { Labeler } from '../../labeler' import { wait } from '@atproto/common' -import { BackgroundQueue } from '../../event-stream/background-queue' +import { BackgroundQueue } from '../../background' import { Crawlers } from '../../crawlers' export class RepoService { @@ -27,37 +25,25 @@ export class RepoService { constructor( public db: Database, public repoSigningKey: crypto.Keypair, - public messageDispatcher: MessageQueue, public blobstore: BlobStore, public backgroundQueue: BackgroundQueue, public crawlers: Crawlers, - public labeler: Labeler, ) { this.blobs = new RepoBlobs(db, blobstore, backgroundQueue) } static creator( keypair: crypto.Keypair, - messageDispatcher: MessageQueue, blobstore: BlobStore, backgroundQueue: BackgroundQueue, crawlers: Crawlers, - labeler: Labeler, ) { return (db: Database) => - new RepoService( - db, - keypair, - messageDispatcher, - blobstore, - backgroundQueue, - crawlers, - labeler, - ) + new RepoService(db, keypair, blobstore, backgroundQueue, crawlers) } services = { - record: RecordService.creator(this.messageDispatcher), + record: RecordService.creator(), } private async serviceTx( @@ -68,11 +54,9 @@ export class RepoService { const srvc = new RepoService( dbTxn, this.repoSigningKey, - this.messageDispatcher, this.blobstore, this.backgroundQueue, this.crawlers, - this.labeler, ) return fn(srvc) }) @@ -294,15 +278,6 @@ export class RepoService { this.backgroundQueue.add(async () => { await this.crawlers.notifyOfUpdate() }) - writes.forEach((write) => { - if ( - write.action === WriteOpAction.Create || - write.action === WriteOpAction.Update - ) { - // @TODO move to appview - this.labeler.processRecord(write.uri, write.record) - } - }) }) const seqEvt = await sequencer.formatSeqCommit(did, commitData, writes) diff --git a/packages/pds/src/services/util/search.ts b/packages/pds/src/services/util/search.ts index 007052e707c..26b3e81bf06 100644 --- a/packages/pds/src/services/util/search.ts +++ b/packages/pds/src/services/util/search.ts @@ -1,7 +1,7 @@ import { sql } from 'kysely' import { InvalidRequestError } from '@atproto/xrpc-server' import Database from '../../db' -import { notSoftDeletedClause, DbRef, AnyQb } from '../../db/util' +import { notSoftDeletedClause, DbRef } from '../../db/util' import { GenericKeyset, paginate } from '../../db/pagination' // @TODO utilized in both pds and app-view @@ -19,58 +19,13 @@ export const getUserSearchQueryPg = ( const { term, limit, cursor, includeSoftDeleted } = opts // Matching user accounts based on handle const distanceAccount = distance(term, ref('handle')) - let accountsQb = getMatchingAccountsQb(db, { term, includeSoftDeleted }) - accountsQb = paginate(accountsQb, { + const accountsQb = getMatchingAccountsQb(db, { term, includeSoftDeleted }) + return paginate(accountsQb, { limit, cursor, direction: 'asc', keyset: new SearchKeyset(distanceAccount, ref('handle')), }) - // Matching profiles based on display name - const distanceProfile = distance(term, ref('displayName')) - let profilesQb = getMatchingProfilesQb(db, { term, includeSoftDeleted }) - profilesQb = paginate( - profilesQb.innerJoin('did_handle', 'did_handle.did', 'profile.creator'), // for handle pagination - { - limit, - cursor, - direction: 'asc', - keyset: new SearchKeyset(distanceProfile, ref('handle')), - }, - ) - // Combine and paginate result set - return paginate(combineAccountsAndProfilesQb(db, accountsQb, profilesQb), { - limit, - cursor, - direction: 'asc', - keyset: new SearchKeyset(ref('distance'), ref('handle')), - }) -} - -// Takes maximal advantage of trigram index at the expense of ability to paginate. -export const getUserSearchQuerySimplePg = ( - db: Database, - opts: { - term: string - limit: number - }, -) => { - const { ref } = db.db.dynamic - const { term, limit } = opts - // Matching user accounts based on handle - const accountsQb = getMatchingAccountsQb(db, { term }) - .orderBy('distance', 'asc') - .limit(limit) - // Matching profiles based on display name - const profilesQb = getMatchingProfilesQb(db, { term }) - .orderBy('distance', 'asc') - .limit(limit) - // Combine and paginate result set - return paginate(combineAccountsAndProfilesQb(db, accountsQb, profilesQb), { - limit, - direction: 'asc', - keyset: new SearchKeyset(ref('distance'), ref('handle')), - }) } // Matching user accounts based on handle @@ -91,51 +46,6 @@ const getMatchingAccountsQb = ( .select(['did_handle.did as did', distanceAccount.as('distance')]) } -// Matching profiles based on display name -const getMatchingProfilesQb = ( - db: Database, - opts: { term: string; includeSoftDeleted?: boolean }, -) => { - const { ref } = db.db.dynamic - const { term, includeSoftDeleted } = opts - const distanceProfile = distance(term, ref('displayName')) - return db.db - .selectFrom('profile') - .innerJoin('repo_root', 'repo_root.did', 'profile.creator') - .if(!includeSoftDeleted, (qb) => - qb.where(notSoftDeletedClause(ref('repo_root'))), - ) - .where(similar(term, ref('displayName'))) // Coarse filter engaging trigram index - .select(['profile.creator as did', distanceProfile.as('distance')]) -} - -// Combine profile and account result sets -const combineAccountsAndProfilesQb = ( - db: Database, - accountsQb: AnyQb, - profilesQb: AnyQb, -) => { - // Combine user account and profile results, taking best matches from each - const emptyQb = db.db - .selectFrom('user_account') - .where(sql`1 = 0`) - .select([sql.literal('').as('did'), sql`0`.as('distance')]) - const resultsQb = db.db - .selectFrom( - emptyQb - .unionAll(sql`${accountsQb}`) // The sql`` is adding parens - .unionAll(sql`${profilesQb}`) - .as('accounts_and_profiles'), - ) - .selectAll() - .distinctOn('did') // Per did, take whichever of account and profile distance is best - .orderBy('did') - .orderBy('distance') - return db.db - .selectFrom(resultsQb.as('results')) - .innerJoin('did_handle', 'did_handle.did', 'results.did') -} - export const getUserSearchQuerySqlite = ( db: Database, opts: { @@ -160,7 +70,10 @@ export const getUserSearchQuerySqlite = ( if (!safeWords.length) { // Return no results. This could happen with weird input like ' % _ '. - return db.db.selectFrom('did_handle').where(sql`1 = 0`) + return db.db + .selectFrom('did_handle') + .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') + .where(sql`1 = 0`) } // We'll ensure there's a space before each word in both textForMatch and in safeWords, @@ -174,9 +87,9 @@ export const getUserSearchQuerySqlite = ( return db.db .selectFrom('did_handle') - .innerJoin('repo_root as _repo_root', '_repo_root.did', 'did_handle.did') + .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') .if(!includeSoftDeleted, (qb) => - qb.where(notSoftDeletedClause(ref('_repo_root'))), + qb.where(notSoftDeletedClause(ref('repo_root'))), ) .where((q) => { safeWords.forEach((word) => { diff --git a/packages/pds/tests/_util.ts b/packages/pds/tests/_util.ts index 16061b8af51..b8135836c15 100644 --- a/packages/pds/tests/_util.ts +++ b/packages/pds/tests/_util.ts @@ -98,9 +98,6 @@ export const runTestServer = async ( dbPostgresUrl: process.env.DB_POSTGRES_URL, blobstoreLocation: `${blobstoreLoc}/blobs`, blobstoreTmp: `${blobstoreLoc}/tmp`, - labelerDid: 'did:example:labeler', - labelerKeywords: { label_me: 'test-label', label_me_2: 'test-label-2' }, - feedGenDid: 'did:example:feedGen', maxSubscriptionBuffer: 200, repoBackfillLimitMs: HOUR, sequencerLeaderLockId: uniqueLockId(), @@ -153,9 +150,6 @@ export const runTestServer = async ( const pdsServer = await pds.start() const pdsPort = (pdsServer.address() as AddressInfo).port - // we refresh label cache by hand in `processAll` instead of on a timer - pds.ctx.labelCache.stop() - return { url: `http://localhost:${pdsPort}`, ctx: pds.ctx, @@ -165,7 +159,6 @@ export const runTestServer = async ( }, processAll: async () => { await pds.ctx.backgroundQueue.processAll() - await pds.ctx.labelCache.fullRefresh() }, } } diff --git a/packages/pds/tests/account-deletion.test.ts b/packages/pds/tests/account-deletion.test.ts index b99807ec6d5..d6b4aa101ce 100644 --- a/packages/pds/tests/account-deletion.test.ts +++ b/packages/pds/tests/account-deletion.test.ts @@ -11,24 +11,12 @@ import { BlobNotFoundError, BlobStore } from '@atproto/repo' import { RepoRoot } from '../src/db/tables/repo-root' import { UserAccount } from '../src/db/tables/user-account' import { IpldBlock } from '../src/db/tables/ipld-block' -import { Post } from '../src/app-view/db/tables/post' -import { Like } from '../src/app-view/db/tables/like' -import { Repost } from '../src/app-view/db/tables/repost' -import { Follow } from '../src/app-view/db/tables/follow' import { RepoBlob } from '../src/db/tables/repo-blob' import { Blob } from '../src/db/tables/blob' -import { - PostEmbedImage, - PostEmbedExternal, - PostEmbedRecord, -} from '../src/app-view/db/tables/post-embed' import { Record } from '../src/db/tables/record' import { RepoSeq } from '../src/db/tables/repo-seq' import { ACKNOWLEDGE } from '../src/lexicon/types/com/atproto/admin/defs' import { UserState } from '../src/db/tables/user-state' -import { ActorBlock } from '../src/app-view/db/tables/actor-block' -import { List } from '../src/app-view/db/tables/list' -import { ListItem } from '../src/app-view/db/tables/list-item' describe('account deletion', () => { let server: util.TestServerInfo @@ -179,43 +167,10 @@ describe('account deletion', () => { (row) => row.did === carol.did && row.eventType === 'tombstone', ).length, ).toEqual(1) - }) - it('no longer stores indexed records from the user', async () => { expect(updatedDbContents.records).toEqual( initialDbContents.records.filter((row) => row.did !== carol.did), ) - expect(updatedDbContents.posts).toEqual( - initialDbContents.posts.filter((row) => row.creator !== carol.did), - ) - expect(updatedDbContents.likes).toEqual( - initialDbContents.likes.filter((row) => row.creator !== carol.did), - ) - expect(updatedDbContents.actorBlocks).toEqual( - initialDbContents.actorBlocks.filter((row) => row.creator !== carol.did), - ) - expect(updatedDbContents.lists).toEqual( - initialDbContents.lists.filter((row) => row.creator !== carol.did), - ) - expect(updatedDbContents.listItems).toEqual( - initialDbContents.listItems.filter((row) => row.creator !== carol.did), - ) - expect(updatedDbContents.reposts).toEqual( - initialDbContents.reposts.filter((row) => row.creator !== carol.did), - ) - expect(updatedDbContents.follows).toEqual( - initialDbContents.follows.filter((row) => row.creator !== carol.did), - ) - expect(updatedDbContents.postImages).toEqual( - initialDbContents.postImages.filter( - (row) => !row.postUri.includes(carol.did), - ), - ) - expect(updatedDbContents.postExternals).toEqual( - initialDbContents.postExternals.filter( - (row) => !row.postUri.includes(carol.did), - ), - ) }) it('deletes relevant blobs', async () => { @@ -269,82 +224,32 @@ type DbContents = { blocks: IpldBlock[] seqs: Selectable[] records: Record[] - posts: Post[] - postImages: PostEmbedImage[] - postExternals: PostEmbedExternal[] - postRecords: PostEmbedRecord[] - likes: Like[] - reposts: Repost[] - follows: Follow[] - actorBlocks: ActorBlock[] - lists: List[] - listItems: ListItem[] repoBlobs: RepoBlob[] blobs: Blob[] } const getDbContents = async (db: Database): Promise => { - const [ - roots, - users, - userState, - blocks, - seqs, - records, - posts, - postImages, - postExternals, - postRecords, - likes, - reposts, - follows, - actorBlocks, - lists, - listItems, - repoBlobs, - blobs, - ] = await Promise.all([ - db.db.selectFrom('repo_root').orderBy('did').selectAll().execute(), - db.db.selectFrom('user_account').orderBy('did').selectAll().execute(), - db.db.selectFrom('user_state').orderBy('did').selectAll().execute(), - db.db - .selectFrom('ipld_block') - .orderBy('creator') - .orderBy('cid') - .selectAll() - .execute(), - db.db.selectFrom('repo_seq').orderBy('id').selectAll().execute(), - db.db.selectFrom('record').orderBy('uri').selectAll().execute(), - db.db.selectFrom('post').orderBy('uri').selectAll().execute(), - db.db - .selectFrom('post_embed_image') - .orderBy('postUri') - .selectAll() - .execute(), - db.db - .selectFrom('post_embed_external') - .orderBy('postUri') - .selectAll() - .execute(), - db.db - .selectFrom('post_embed_record') - .orderBy('postUri') - .selectAll() - .execute(), - db.db.selectFrom('like').orderBy('uri').selectAll().execute(), - db.db.selectFrom('repost').orderBy('uri').selectAll().execute(), - db.db.selectFrom('follow').orderBy('uri').selectAll().execute(), - db.db.selectFrom('actor_block').orderBy('uri').selectAll().execute(), - db.db.selectFrom('list').orderBy('uri').selectAll().execute(), - db.db.selectFrom('list_item').orderBy('uri').selectAll().execute(), - db.db - .selectFrom('repo_blob') - .orderBy('did') - .orderBy('cid') - .selectAll() - .execute(), - db.db.selectFrom('blob').orderBy('cid').selectAll().execute(), - ]) + const [roots, users, userState, blocks, seqs, records, repoBlobs, blobs] = + await Promise.all([ + db.db.selectFrom('repo_root').orderBy('did').selectAll().execute(), + db.db.selectFrom('user_account').orderBy('did').selectAll().execute(), + db.db.selectFrom('user_state').orderBy('did').selectAll().execute(), + db.db + .selectFrom('ipld_block') + .orderBy('creator') + .orderBy('cid') + .selectAll() + .execute(), + db.db.selectFrom('repo_seq').orderBy('id').selectAll().execute(), + db.db.selectFrom('record').orderBy('uri').selectAll().execute(), + db.db + .selectFrom('repo_blob') + .orderBy('did') + .orderBy('cid') + .selectAll() + .execute(), + db.db.selectFrom('blob').orderBy('cid').selectAll().execute(), + ]) return { roots, @@ -353,16 +258,6 @@ const getDbContents = async (db: Database): Promise => { blocks, seqs, records, - posts, - postImages, - postExternals, - postRecords, - likes, - reposts, - follows, - actorBlocks, - lists, - listItems, repoBlobs, blobs, } diff --git a/packages/pds/tests/admin/__snapshots__/get-record.test.ts.snap b/packages/pds/tests/admin/__snapshots__/get-record.test.ts.snap index 3a816975459..00fbc5bda1c 100644 --- a/packages/pds/tests/admin/__snapshots__/get-record.test.ts.snap +++ b/packages/pds/tests/admin/__snapshots__/get-record.test.ts.snap @@ -6,16 +6,6 @@ Object { "blobs": Array [], "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(0)", - "val": "self-label", - }, - ], "moderation": Object { "actions": Array [ Object { @@ -143,169 +133,6 @@ Object { "blobs": Array [], "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(0)", - "val": "self-label", - }, - ], - "moderation": Object { - "actions": Array [ - Object { - "action": "com.atproto.admin.defs#takedown", - "createdAt": "1970-01-01T00:00:00.000Z", - "createdBy": "did:example:admin", - "id": 3, - "reason": "X", - "resolvedReportIds": Array [], - "subject": Object { - "$type": "com.atproto.repo.strongRef", - "cid": "cids(0)", - "uri": "record(0)", - }, - "subjectBlobCids": Array [], - }, - Object { - "action": "com.atproto.admin.defs#acknowledge", - "createdAt": "1970-01-01T00:00:00.000Z", - "createdBy": "did:example:admin", - "id": 2, - "reason": "X", - "resolvedReportIds": Array [], - "reversal": Object { - "createdAt": "1970-01-01T00:00:00.000Z", - "createdBy": "did:example:admin", - "reason": "X", - }, - "subject": Object { - "$type": "com.atproto.repo.strongRef", - "cid": "cids(0)", - "uri": "record(0)", - }, - "subjectBlobCids": Array [], - }, - ], - "currentAction": Object { - "action": "com.atproto.admin.defs#takedown", - "id": 3, - }, - "reports": Array [ - Object { - "createdAt": "1970-01-01T00:00:00.000Z", - "id": 2, - "reason": "defamation", - "reasonType": "com.atproto.moderation.defs#reasonOther", - "reportedBy": "user(1)", - "resolvedByActionIds": Array [], - "subject": Object { - "$type": "com.atproto.repo.strongRef", - "cid": "cids(0)", - "uri": "record(0)", - }, - "subjectRepoHandle": "alice.test", - }, - Object { - "createdAt": "1970-01-01T00:00:00.000Z", - "id": 1, - "reasonType": "com.atproto.moderation.defs#reasonSpam", - "reportedBy": "user(2)", - "resolvedByActionIds": Array [], - "subject": Object { - "$type": "com.atproto.repo.strongRef", - "cid": "cids(0)", - "uri": "record(0)", - }, - "subjectRepoHandle": "alice.test", - }, - ], - }, - "repo": Object { - "did": "user(0)", - "email": "alice@test.com", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invitesDisabled": false, - "moderation": Object {}, - "relatedRecords": Array [ - Object { - "$type": "app.bsky.actor.profile", - "avatar": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(1)", - }, - "size": 3976, - }, - "description": "its me!", - "displayName": "ali", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label-a", - }, - Object { - "val": "self-label-b", - }, - ], - }, - }, - ], - }, - "uri": "record(0)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label", - }, - ], - }, - "text": "hey there", - }, -} -`; - -exports[`pds admin get record view serves labels. 1`] = ` -Object { - "blobCids": Array [], - "blobs": Array [], - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(0)", - "val": "kittens", - }, - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(0)", - "val": "puppies", - }, - Object { - "cid": "cids(0)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(0)", - "val": "self-label", - }, - ], "moderation": Object { "actions": Array [ Object { diff --git a/packages/pds/tests/admin/__snapshots__/get-repo.test.ts.snap b/packages/pds/tests/admin/__snapshots__/get-repo.test.ts.snap index 7234789c2c1..c90b1a070b2 100644 --- a/packages/pds/tests/admin/__snapshots__/get-repo.test.ts.snap +++ b/packages/pds/tests/admin/__snapshots__/get-repo.test.ts.snap @@ -8,124 +8,6 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "invites": Array [], "invitesDisabled": false, - "labels": Array [], - "moderation": Object { - "actions": Array [ - Object { - "action": "com.atproto.admin.defs#takedown", - "createdAt": "1970-01-01T00:00:00.000Z", - "createdBy": "did:example:admin", - "id": 3, - "reason": "X", - "resolvedReportIds": Array [], - "subject": Object { - "$type": "com.atproto.admin.defs#repoRef", - "did": "user(0)", - }, - "subjectBlobCids": Array [], - }, - Object { - "action": "com.atproto.admin.defs#acknowledge", - "createdAt": "1970-01-01T00:00:00.000Z", - "createdBy": "did:example:admin", - "id": 2, - "reason": "X", - "resolvedReportIds": Array [], - "reversal": Object { - "createdAt": "1970-01-01T00:00:00.000Z", - "createdBy": "did:example:admin", - "reason": "X", - }, - "subject": Object { - "$type": "com.atproto.admin.defs#repoRef", - "did": "user(0)", - }, - "subjectBlobCids": Array [], - }, - ], - "currentAction": Object { - "action": "com.atproto.admin.defs#takedown", - "id": 3, - }, - "reports": Array [ - Object { - "createdAt": "1970-01-01T00:00:00.000Z", - "id": 2, - "reason": "defamation", - "reasonType": "com.atproto.moderation.defs#reasonOther", - "reportedBy": "user(1)", - "resolvedByActionIds": Array [], - "subject": Object { - "$type": "com.atproto.admin.defs#repoRef", - "did": "user(0)", - }, - }, - Object { - "createdAt": "1970-01-01T00:00:00.000Z", - "id": 1, - "reasonType": "com.atproto.moderation.defs#reasonSpam", - "reportedBy": "user(2)", - "resolvedByActionIds": Array [], - "subject": Object { - "$type": "com.atproto.admin.defs#repoRef", - "did": "user(0)", - }, - }, - ], - }, - "relatedRecords": Array [ - Object { - "$type": "app.bsky.actor.profile", - "avatar": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(0)", - }, - "size": 3976, - }, - "description": "its me!", - "displayName": "ali", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label-a", - }, - Object { - "val": "self-label-b", - }, - ], - }, - }, - ], -} -`; - -exports[`pds admin get repo view serves labels. 1`] = ` -Object { - "did": "user(0)", - "email": "alice@test.com", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invites": Array [], - "invitesDisabled": false, - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(0)", - "val": "kittens", - }, - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(0)", - "val": "puppies", - }, - ], "moderation": Object { "actions": Array [ Object { diff --git a/packages/pds/tests/admin/get-record.test.ts b/packages/pds/tests/admin/get-record.test.ts index d70707b2b70..6c38419612e 100644 --- a/packages/pds/tests/admin/get-record.test.ts +++ b/packages/pds/tests/admin/get-record.test.ts @@ -120,29 +120,4 @@ describe('pds admin get record view', () => { ) await expect(promise).rejects.toThrow('Record not found') }) - - it('serves labels.', async () => { - const { ctx } = server - const labelingService = ctx.services.appView.label(ctx.db) - await labelingService.formatAndCreate( - ctx.cfg.labelerDid, - sc.posts[sc.dids.alice][0].ref.uriStr, - sc.posts[sc.dids.alice][0].ref.cidStr, - { create: ['kittens', 'puppies', 'birds'] }, - ) - await labelingService.formatAndCreate( - ctx.cfg.labelerDid, - sc.posts[sc.dids.alice][0].ref.uriStr, - sc.posts[sc.dids.alice][0].ref.cidStr, - { negate: ['birds'] }, - ) - const result = await agent.api.com.atproto.admin.getRecord( - { - uri: sc.posts[sc.dids.alice][0].ref.uriStr, - cid: sc.posts[sc.dids.alice][0].ref.cidStr, - }, - { headers: { authorization: adminAuth() } }, - ) - expect(forSnapshot(result.data)).toMatchSnapshot() - }) }) diff --git a/packages/pds/tests/admin/get-repo.test.ts b/packages/pds/tests/admin/get-repo.test.ts index 3cb997f6ff2..9cd38ae101f 100644 --- a/packages/pds/tests/admin/get-repo.test.ts +++ b/packages/pds/tests/admin/get-repo.test.ts @@ -108,26 +108,4 @@ describe('pds admin get repo view', () => { ) await expect(promise).rejects.toThrow('Repo not found') }) - - it('serves labels.', async () => { - const { ctx } = server - const labelingService = ctx.services.appView.label(ctx.db) - await labelingService.formatAndCreate( - ctx.cfg.labelerDid, - sc.dids.alice, - null, - { create: ['kittens', 'puppies', 'birds'] }, - ) - await labelingService.formatAndCreate( - ctx.cfg.labelerDid, - sc.dids.alice, - null, - { negate: ['birds'] }, - ) - const result = await agent.api.com.atproto.admin.getRepo( - { did: sc.dids.alice }, - { headers: { authorization: adminAuth() } }, - ) - expect(forSnapshot(result.data)).toMatchSnapshot() - }) }) diff --git a/packages/pds/tests/admin/repo-search.test.ts b/packages/pds/tests/admin/repo-search.test.ts index 3f25c893901..e3fcdef2d80 100644 --- a/packages/pds/tests/admin/repo-search.test.ts +++ b/packages/pds/tests/admin/repo-search.test.ts @@ -1,19 +1,11 @@ import AtpAgent from '@atproto/api' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' -import { - runTestServer, - forSnapshot, - CloseFn, - paginateAll, - adminAuth, -} from '../_util' +import { runTestServer, CloseFn, paginateAll, adminAuth } from '../_util' import { SeedClient } from '../seeds/client' import usersBulkSeed from '../seeds/users-bulk' -import { Database } from '../../src' describe('pds admin repo search view', () => { let agent: AtpAgent - let db: Database let close: CloseFn let sc: SeedClient let headers: { [s: string]: string } @@ -23,7 +15,6 @@ describe('pds admin repo search view', () => { dbPostgresSchema: 'views_admin_repo_search', }) close = server.close - db = server.ctx.db agent = new AtpAgent({ service: server.url }) sc = new SeedClient(agent) await usersBulkSeed(sc) @@ -54,21 +45,12 @@ describe('pds admin repo search view', () => { const shouldContain = [ 'cara-wiegand69.test', // Present despite repo takedown - 'eudora-dietrich4.test', // Carol Littel - 'shane-torphy52.test', // Sadie Carter - 'aliya-hodkiewicz.test', // Carlton Abernathy IV 'carlos6.test', 'carolina-mcdermott77.test', ] shouldContain.forEach((handle) => expect(handles).toContain(handle)) - if (db.dialect === 'pg') { - expect(handles).toContain('cayla-marquardt39.test') // Fuzzy match supported by postgres - } else { - expect(handles).not.toContain('cayla-marquardt39.test') - } - const shouldNotContain = [ 'sven70.test', 'hilario84.test', @@ -80,18 +62,12 @@ describe('pds admin repo search view', () => { ] shouldNotContain.forEach((handle) => expect(handles).not.toContain(handle)) - - if (db.dialect === 'pg') { - expect(forSnapshot(result.data.repos)).toMatchInlineSnapshot(snapPg) - } else { - expect(forSnapshot(result.data.repos)).toMatchInlineSnapshot(snapSqlite) - } }) it('finds repo by did', async () => { const term = sc.dids['cara-wiegand69.test'] const res = await agent.api.com.atproto.admin.searchRepos( - { term, limit: 1 }, + { term }, { headers }, ) @@ -99,6 +75,19 @@ describe('pds admin repo search view', () => { expect(res.data.repos[0].did).toEqual(term) }) + it('finds repo by email', async () => { + const did = sc.dids['cara-wiegand69.test'] + const { email } = sc.accounts[did] + const res = await agent.api.com.atproto.admin.searchRepos( + { term: email }, + { headers }, + ) + + expect(res.data.repos.length).toEqual(1) + expect(res.data.repos[0].did).toEqual(did) + expect(res.data.repos[0].email).toEqual(email) + }) + it('paginates with term', async () => { const results = (results) => results.flatMap((res) => res.users) const paginator = async (cursor?: string) => { @@ -119,7 +108,7 @@ describe('pds admin repo search view', () => { { headers }, ) - expect(full.data.repos.length).toBeGreaterThan(5) + expect(full.data.repos.length).toBeGreaterThan(3) expect(results(paginatedAll)).toEqual(results([full.data])) }) @@ -147,269 +136,3 @@ describe('pds admin repo search view', () => { expect(results(paginatedAll)).toEqual(results([full.data])) }) }) - -// Not using jest snapshots because it doesn't handle the conditional pg/sqlite very well: -// you can achieve it using named snapshots, but when you run the tests for pg the test suite fails -// since the sqlite snapshots appear obsolete to jest (and vice-versa when you run the sqlite suite). - -const snapPg = ` -Array [ - Object { - "did": "user(0)", - "email": "aliya-hodkiewicz.test@bsky.app", - "handle": "aliya-hodkiewicz.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invitesDisabled": false, - "moderation": Object {}, - "relatedRecords": Array [ - Object { - "$type": "app.bsky.actor.profile", - "avatar": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(0)", - }, - "size": 3976, - }, - "description": "", - "displayName": "Carlton Abernathy IV", - }, - ], - }, - Object { - "did": "user(1)", - "email": "cara-wiegand69.test@bsky.app", - "handle": "cara-wiegand69.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invitesDisabled": false, - "moderation": Object { - "currentAction": Object { - "action": "com.atproto.admin.defs#takedown", - "id": 1, - }, - }, - "relatedRecords": Array [], - }, - Object { - "did": "user(2)", - "email": "carlos6.test@bsky.app", - "handle": "carlos6.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invitesDisabled": false, - "moderation": Object {}, - "relatedRecords": Array [], - }, - Object { - "did": "user(3)", - "email": "carolina-mcdermott77.test@bsky.app", - "handle": "carolina-mcdermott77.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invitesDisabled": false, - "moderation": Object {}, - "relatedRecords": Array [ - Object { - "$type": "app.bsky.actor.profile", - "avatar": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(0)", - }, - "size": 3976, - }, - "description": "", - "displayName": "Latoya Windler", - }, - ], - }, - Object { - "did": "user(4)", - "email": "eudora-dietrich4.test@bsky.app", - "handle": "eudora-dietrich4.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invitesDisabled": false, - "moderation": Object {}, - "relatedRecords": Array [ - Object { - "$type": "app.bsky.actor.profile", - "avatar": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(0)", - }, - "size": 3976, - }, - "description": "", - "displayName": "Carol Littel", - }, - ], - }, - Object { - "did": "user(5)", - "email": "shane-torphy52.test@bsky.app", - "handle": "shane-torphy52.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invitesDisabled": false, - "moderation": Object {}, - "relatedRecords": Array [ - Object { - "$type": "app.bsky.actor.profile", - "avatar": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(0)", - }, - "size": 3976, - }, - "description": "", - "displayName": "Sadie Carter", - }, - ], - }, - Object { - "did": "user(6)", - "email": "cayla-marquardt39.test@bsky.app", - "handle": "cayla-marquardt39.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invitesDisabled": false, - "moderation": Object {}, - "relatedRecords": Array [ - Object { - "$type": "app.bsky.actor.profile", - "avatar": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(0)", - }, - "size": 3976, - }, - "description": "", - "displayName": "Rachel Kshlerin", - }, - ], - }, -] -` -const snapSqlite = ` -Array [ - Object { - "did": "user(0)", - "email": "aliya-hodkiewicz.test@bsky.app", - "handle": "aliya-hodkiewicz.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invitesDisabled": false, - "moderation": Object {}, - "relatedRecords": Array [ - Object { - "$type": "app.bsky.actor.profile", - "avatar": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(0)", - }, - "size": 3976, - }, - "description": "", - "displayName": "Carlton Abernathy IV", - }, - ], - }, - Object { - "did": "user(1)", - "email": "cara-wiegand69.test@bsky.app", - "handle": "cara-wiegand69.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invitesDisabled": false, - "moderation": Object { - "currentAction": Object { - "action": "com.atproto.admin.defs#takedown", - "id": 1, - }, - }, - "relatedRecords": Array [], - }, - Object { - "did": "user(2)", - "email": "carlos6.test@bsky.app", - "handle": "carlos6.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invitesDisabled": false, - "moderation": Object {}, - "relatedRecords": Array [], - }, - Object { - "did": "user(3)", - "email": "carolina-mcdermott77.test@bsky.app", - "handle": "carolina-mcdermott77.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invitesDisabled": false, - "moderation": Object {}, - "relatedRecords": Array [ - Object { - "$type": "app.bsky.actor.profile", - "avatar": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(0)", - }, - "size": 3976, - }, - "description": "", - "displayName": "Latoya Windler", - }, - ], - }, - Object { - "did": "user(4)", - "email": "eudora-dietrich4.test@bsky.app", - "handle": "eudora-dietrich4.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invitesDisabled": false, - "moderation": Object {}, - "relatedRecords": Array [ - Object { - "$type": "app.bsky.actor.profile", - "avatar": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(0)", - }, - "size": 3976, - }, - "description": "", - "displayName": "Carol Littel", - }, - ], - }, - Object { - "did": "user(5)", - "email": "shane-torphy52.test@bsky.app", - "handle": "shane-torphy52.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invitesDisabled": false, - "moderation": Object {}, - "relatedRecords": Array [ - Object { - "$type": "app.bsky.actor.profile", - "avatar": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(0)", - }, - "size": 3976, - }, - "description": "", - "displayName": "Sadie Carter", - }, - ], - }, -] -` diff --git a/packages/pds/tests/duplicate-records.test.ts b/packages/pds/tests/duplicate-records.test.ts deleted file mode 100644 index b435dca32f3..00000000000 --- a/packages/pds/tests/duplicate-records.test.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/syntax' -import { cidForCbor, TID, cborEncode } from '@atproto/common' -import { CloseFn, runTestServer } from './_util' -import { Database } from '../src' -import * as lex from '../src/lexicon/lexicons' -import { Services } from '../src/services' - -describe('duplicate record', () => { - let close: CloseFn - let did: string - let db: Database - let services: Services - - beforeAll(async () => { - const server = await runTestServer({ - dbPostgresSchema: 'duplicates', - }) - db = server.ctx.db - services = server.ctx.services - close = server.close - did = 'did:example:alice' - }) - - afterAll(async () => { - await close() - }) - - const countRecords = async (db: Database, table: string) => { - const got = await db.db - .selectFrom(table as any) - .selectAll() - .where('creator', '=', did) - .execute() - return got.length - } - - const putBlock = async ( - db: Database, - creator: string, - data: object, - ): Promise => { - const cid = await cidForCbor(data) - const bytes = await cborEncode(data) - await db.db - .insertInto('ipld_block') - .values({ - cid: cid.toString(), - creator, - size: bytes.length, - content: bytes, - }) - .onConflict((oc) => oc.doNothing()) - .execute() - return cid - } - - it('dedupes reposts', async () => { - const subject = AtUri.make(did, lex.ids.AppBskyFeedPost, TID.nextStr()) - const subjectCid = await putBlock(db, did, { test: 'blah' }) - const coll = lex.ids.AppBskyFeedRepost - const uris: AtUri[] = [] - await db.transaction(async (tx) => { - for (let i = 0; i < 5; i++) { - const repost = { - $type: coll, - subject: { - uri: subject.toString(), - cid: subjectCid.toString(), - }, - createdAt: new Date().toISOString(), - } - const uri = AtUri.make(did, coll, TID.nextStr()) - const cid = await putBlock(tx, did, repost) - await services.record(tx).indexRecord(uri, cid, repost) - uris.push(uri) - } - }) - - let count = await countRecords(db, 'repost') - expect(count).toBe(1) - - await db.transaction(async (tx) => { - await services.record(tx).deleteRecord(uris[0], false) - }) - - count = await countRecords(db, 'repost') - expect(count).toBe(1) - - await db.transaction(async (tx) => { - await services.record(tx).deleteRecord(uris[1], true) - }) - - count = await countRecords(db, 'repost') - expect(count).toBe(0) - }) - - it('dedupes likes', async () => { - const subject = AtUri.make(did, lex.ids.AppBskyFeedPost, TID.nextStr()) - const subjectCid = await putBlock(db, did, { test: 'blah' }) - const coll = lex.ids.AppBskyFeedLike - const uris: AtUri[] = [] - await db.transaction(async (tx) => { - for (let i = 0; i < 5; i++) { - const like = { - $type: coll, - subject: { - uri: subject.toString(), - cid: subjectCid.toString(), - }, - createdAt: new Date().toISOString(), - } - const uri = AtUri.make(did, coll, TID.nextStr()) - const cid = await putBlock(tx, did, like) - await services.record(tx).indexRecord(uri, cid, like) - uris.push(uri) - } - }) - - let count = await countRecords(db, 'like') - expect(count).toBe(1) - - await db.transaction(async (tx) => { - await services.record(tx).deleteRecord(uris[0], false) - }) - - count = await countRecords(db, 'like') - expect(count).toBe(1) - - const got = await db.db - .selectFrom('like') - .where('creator', '=', did) - .selectAll() - .executeTakeFirst() - expect(got?.uri).toEqual(uris[1].toString()) - - await db.transaction(async (tx) => { - await services.record(tx).deleteRecord(uris[1], true) - }) - - count = await countRecords(db, 'like') - expect(count).toBe(0) - }) - - it('dedupes follows', async () => { - const coll = lex.ids.AppBskyGraphFollow - const uris: AtUri[] = [] - await db.transaction(async (tx) => { - for (let i = 0; i < 5; i++) { - const follow = { - $type: coll, - subject: 'did:example:bob', - createdAt: new Date().toISOString(), - } - const uri = AtUri.make(did, coll, TID.nextStr()) - const cid = await putBlock(tx, did, follow) - await services.record(tx).indexRecord(uri, cid, follow) - uris.push(uri) - } - }) - - let count = await countRecords(db, 'follow') - expect(count).toBe(1) - - await db.transaction(async (tx) => { - await services.record(tx).deleteRecord(uris[0], false) - }) - - count = await countRecords(db, 'follow') - expect(count).toBe(1) - - await db.transaction(async (tx) => { - await services.record(tx).deleteRecord(uris[1], true) - }) - - count = await countRecords(db, 'follow') - expect(count).toBe(0) - }) -}) diff --git a/packages/pds/tests/labeler/apply-labels.test.ts b/packages/pds/tests/labeler/apply-labels.test.ts deleted file mode 100644 index 80fedc459d5..00000000000 --- a/packages/pds/tests/labeler/apply-labels.test.ts +++ /dev/null @@ -1,180 +0,0 @@ -import AtpAgent from '@atproto/api' -import { TestNetworkNoAppView } from '@atproto/dev-env' -import { adminAuth, moderatorAuth } from '../_util' -import { SeedClient } from '../seeds/client' -import basicSeed from '../seeds/basic' - -describe('unspecced.applyLabels', () => { - let network: TestNetworkNoAppView - let agent: AtpAgent - let sc: SeedClient - - beforeAll(async () => { - network = await TestNetworkNoAppView.create({ - dbPostgresSchema: 'apply_labels', - }) - agent = network.pds.getClient() - sc = new SeedClient(agent) - await basicSeed(sc) - }) - - afterAll(async () => { - await network.close() - }) - - it('requires admin auth.', async () => { - const tryToLabel = agent.api.app.bsky.unspecced.applyLabels( - { - labels: [ - { - src: network.pds.ctx.cfg.labelerDid, - uri: sc.dids.carol, - val: 'cats', - neg: false, - cts: new Date().toISOString(), - }, - ], - }, - { - encoding: 'application/json', - headers: { authorization: moderatorAuth() }, - }, - ) - await expect(tryToLabel).rejects.toThrow('Insufficient privileges') - }) - - it('adds and removes labels on record as though applied by the labeler.', async () => { - const post = sc.posts[sc.dids.bob][1].ref - await agent.api.app.bsky.unspecced.applyLabels( - { - labels: [ - { - src: network.pds.ctx.cfg.labelerDid, - uri: post.uriStr, - cid: post.cidStr, - val: 'birds', - neg: false, - cts: new Date().toISOString(), - }, - { - src: network.pds.ctx.cfg.labelerDid, - uri: post.uriStr, - cid: post.cidStr, - val: 'bats', - neg: false, - cts: new Date().toISOString(), - }, - ], - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - await expect(getRecordLabels(post.uriStr)).resolves.toEqual([ - 'birds', - 'bats', - ]) - await agent.api.app.bsky.unspecced.applyLabels( - { - labels: [ - { - src: network.pds.ctx.cfg.labelerDid, - uri: post.uriStr, - cid: post.cidStr, - val: 'birds', - neg: true, - cts: new Date().toISOString(), - }, - { - src: network.pds.ctx.cfg.labelerDid, - uri: post.uriStr, - cid: post.cidStr, - val: 'bats', - neg: true, - cts: new Date().toISOString(), - }, - ], - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - await expect(getRecordLabels(post.uriStr)).resolves.toEqual([]) - }) - - it('adds and removes labels on repo as though applied by the labeler.', async () => { - await agent.api.app.bsky.unspecced.applyLabels( - { - labels: [ - { - src: network.pds.ctx.cfg.labelerDid, - uri: sc.dids.carol, - val: 'birds', - neg: false, - cts: new Date().toISOString(), - }, - { - src: network.pds.ctx.cfg.labelerDid, - uri: sc.dids.carol, - val: 'bats', - neg: false, - cts: new Date().toISOString(), - }, - ], - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - await expect(getRepoLabels(sc.dids.carol)).resolves.toEqual([ - 'birds', - 'bats', - ]) - await agent.api.app.bsky.unspecced.applyLabels( - { - labels: [ - { - src: network.pds.ctx.cfg.labelerDid, - uri: sc.dids.carol, - val: 'birds', - neg: true, - cts: new Date().toISOString(), - }, - { - src: network.pds.ctx.cfg.labelerDid, - uri: sc.dids.carol, - val: 'bats', - neg: true, - cts: new Date().toISOString(), - }, - ], - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - await expect(getRepoLabels(sc.dids.carol)).resolves.toEqual([]) - }) - - async function getRecordLabels(uri: string) { - const result = await agent.api.com.atproto.admin.getRecord( - { uri }, - { headers: { authorization: adminAuth() } }, - ) - const labels = result.data.labels ?? [] - return labels.map((l) => l.val) - } - - async function getRepoLabels(did: string) { - const result = await agent.api.com.atproto.admin.getRepo( - { did }, - { headers: { authorization: adminAuth() } }, - ) - const labels = result.data.labels ?? [] - return labels.map((l) => l.val) - } -}) diff --git a/packages/pds/tests/labeler/fixtures/hiveai_resp_example.json b/packages/pds/tests/labeler/fixtures/hiveai_resp_example.json deleted file mode 100644 index 2315fa9d0c0..00000000000 --- a/packages/pds/tests/labeler/fixtures/hiveai_resp_example.json +++ /dev/null @@ -1,401 +0,0 @@ -{ - "id": "02122580-c37f-11ed-81d2-000000000000", - "code": 200, - "project_id": 12345, - "user_id": 12345, - "created_on": "2023-03-15T22:16:18.408Z", - "status": [ - { - "status": { - "code": "0", - "message": "SUCCESS" - }, - "response": { - "input": { - "id": "02122580-c37f-11ed-81d2-000000000000", - "charge": 0.003, - "model": "mod55_dense", - "model_version": 1, - "model_type": "CATEGORIZATION", - "created_on": "2023-03-15T22:16:18.136Z", - "media": { - "url": null, - "filename": "bafkreiam7k6mvkyuoybq4ynhljvj5xa75sdbhjbolzjf5j2udx7vj5gnsy", - "type": "PHOTO", - "mime_type": "jpeg", - "mimetype": "image/jpeg", - "width": 800, - "height": 800, - "num_frames": 1, - "duration": 0 - }, - "user_id": 12345, - "project_id": 12345, - "config_version": 1, - "config_tag": "default" - }, - "output": [ - { - "time": 0, - "classes": [ - { - "class": "general_not_nsfw_not_suggestive", - "score": 0.9998097218132356 - }, - { - "class": "general_nsfw", - "score": 8.857344804177162e-5 - }, - { - "class": "general_suggestive", - "score": 0.00010170473872266839 - }, - { - "class": "no_female_underwear", - "score": 0.9999923079040384 - }, - { - "class": "yes_female_underwear", - "score": 7.692095961599136e-6 - }, - { - "class": "no_male_underwear", - "score": 0.9999984904867634 - }, - { - "class": "yes_male_underwear", - "score": 1.5095132367094679e-6 - }, - { - "class": "no_sex_toy", - "score": 0.9999970970762551 - }, - { - "class": "yes_sex_toy", - "score": 2.9029237450490604e-6 - }, - { - "class": "no_female_nudity", - "score": 0.9999739028909301 - }, - { - "class": "yes_female_nudity", - "score": 2.60971090699536e-5 - }, - { - "class": "no_male_nudity", - "score": 0.9999711373083747 - }, - { - "class": "yes_male_nudity", - "score": 2.8862691625255323e-5 - }, - { - "class": "no_female_swimwear", - "score": 0.9999917609899659 - }, - { - "class": "yes_female_swimwear", - "score": 8.239010034025379e-6 - }, - { - "class": "no_male_shirtless", - "score": 0.9999583350744331 - }, - { - "class": "yes_male_shirtless", - "score": 4.166492556688088e-5 - }, - { - "class": "no_text", - "score": 0.9958378716447616 - }, - { - "class": "text", - "score": 0.0041621283552384265 - }, - { - "class": "animated", - "score": 0.46755478950048235 - }, - { - "class": "hybrid", - "score": 0.0011440363434524984 - }, - { - "class": "natural", - "score": 0.5313011741560651 - }, - { - "class": "animated_gun", - "score": 2.0713000782979496e-5 - }, - { - "class": "gun_in_hand", - "score": 1.5844730446534659e-6 - }, - { - "class": "gun_not_in_hand", - "score": 1.0338973818006654e-6 - }, - { - "class": "no_gun", - "score": 0.9999766686287906 - }, - { - "class": "culinary_knife_in_hand", - "score": 3.8063500083369785e-6 - }, - { - "class": "culinary_knife_not_in_hand", - "score": 7.94057948996249e-7 - }, - { - "class": "knife_in_hand", - "score": 4.5578955723278505e-7 - }, - { - "class": "knife_not_in_hand", - "score": 3.842124714748908e-7 - }, - { - "class": "no_knife", - "score": 0.999994559590014 - }, - { - "class": "a_little_bloody", - "score": 2.1317745626539786e-7 - }, - { - "class": "no_blood", - "score": 0.9999793341236429 - }, - { - "class": "other_blood", - "score": 2.0322054269591763e-5 - }, - { - "class": "very_bloody", - "score": 1.306446309561673e-7 - }, - { - "class": "no_pills", - "score": 0.9999989592376954 - }, - { - "class": "yes_pills", - "score": 1.0407623044588633e-6 - }, - { - "class": "no_smoking", - "score": 0.9999939101969173 - }, - { - "class": "yes_smoking", - "score": 6.089803082758281e-6 - }, - { - "class": "illicit_injectables", - "score": 6.925695592003094e-7 - }, - { - "class": "medical_injectables", - "score": 8.587808234452378e-7 - }, - { - "class": "no_injectables", - "score": 0.9999984486496174 - }, - { - "class": "no_nazi", - "score": 0.9999987449628097 - }, - { - "class": "yes_nazi", - "score": 1.2550371902234279e-6 - }, - { - "class": "no_kkk", - "score": 0.999999762417549 - }, - { - "class": "yes_kkk", - "score": 2.3758245111050425e-7 - }, - { - "class": "no_middle_finger", - "score": 0.9999881515231847 - }, - { - "class": "yes_middle_finger", - "score": 1.184847681536747e-5 - }, - { - "class": "no_terrorist", - "score": 0.9999998870793229 - }, - { - "class": "yes_terrorist", - "score": 1.1292067715380635e-7 - }, - { - "class": "no_overlay_text", - "score": 0.9996453363440359 - }, - { - "class": "yes_overlay_text", - "score": 0.0003546636559640924 - }, - { - "class": "no_sexual_activity", - "score": 0.9999563580374798 - }, - { - "class": "yes_sexual_activity", - "score": 0.99, - "realScore": 4.364196252012032e-5 - }, - { - "class": "hanging", - "score": 3.6435135762510905e-7 - }, - { - "class": "no_hanging_no_noose", - "score": 0.9999980779196416 - }, - { - "class": "noose", - "score": 1.5577290007796094e-6 - }, - { - "class": "no_realistic_nsfw", - "score": 0.9999944341007805 - }, - { - "class": "yes_realistic_nsfw", - "score": 5.565899219571182e-6 - }, - { - "class": "animated_corpse", - "score": 5.276802046755426e-7 - }, - { - "class": "human_corpse", - "score": 2.5449360984211012e-8 - }, - { - "class": "no_corpse", - "score": 0.9999994468704343 - }, - { - "class": "no_self_harm", - "score": 0.9999994515625507 - }, - { - "class": "yes_self_harm", - "score": 5.484374493605692e-7 - }, - { - "class": "no_drawing", - "score": 0.9978276028816608 - }, - { - "class": "yes_drawing", - "score": 0.0021723971183392485 - }, - { - "class": "no_emaciated_body", - "score": 0.9999998146500432 - }, - { - "class": "yes_emaciated_body", - "score": 1.853499568724518e-7 - }, - { - "class": "no_child_present", - "score": 0.9999970498515446 - }, - { - "class": "yes_child_present", - "score": 2.950148455380443e-6 - }, - { - "class": "no_sexual_intent", - "score": 0.9999963861546292 - }, - { - "class": "yes_sexual_intent", - "score": 3.613845370766111e-6 - }, - { - "class": "animal_genitalia_and_human", - "score": 2.255472023465222e-8 - }, - { - "class": "animal_genitalia_only", - "score": 4.6783185199931176e-7 - }, - { - "class": "animated_animal_genitalia", - "score": 6.707857419436447e-7 - }, - { - "class": "no_animal_genitalia", - "score": 0.9999988388276858 - }, - { - "class": "no_gambling", - "score": 0.9999960939687145 - }, - { - "class": "yes_gambling", - "score": 3.906031285604864e-6 - }, - { - "class": "no_undressed", - "score": 0.99999923356218 - }, - { - "class": "yes_undressed", - "score": 7.664378199789045e-7 - }, - { - "class": "no_confederate", - "score": 0.9999925456900376 - }, - { - "class": "yes_confederate", - "score": 7.454309962453175e-6 - }, - { - "class": "animated_alcohol", - "score": 1.8109949948066074e-6 - }, - { - "class": "no_alcohol", - "score": 0.9999916620957963 - }, - { - "class": "yes_alcohol", - "score": 5.88781463445443e-6 - }, - { - "class": "yes_drinking_alcohol", - "score": 6.390945746578106e-7 - }, - { - "class": "no_religious_icon", - "score": 0.9999862158580689 - }, - { - "class": "yes_religious_icon", - "score": 1.3784141931119298e-5 - } - ] - } - ] - } - } - ], - "from_cache": false -} diff --git a/packages/pds/tests/labeler/hive.test.ts b/packages/pds/tests/labeler/hive.test.ts deleted file mode 100644 index 3213d794e30..00000000000 --- a/packages/pds/tests/labeler/hive.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import fs from 'fs/promises' -import * as hive from '../../src/labeler/hive' - -describe('labeling', () => { - it('correctly parses hive responses', async () => { - const exampleRespBytes = await fs.readFile( - 'tests/labeler/fixtures/hiveai_resp_example.json', - ) - const exampleResp = JSON.parse(exampleRespBytes.toString()) - const classes = hive.respToClasses(exampleResp) - expect(classes.length).toBeGreaterThan(10) - - const labels = hive.summarizeLabels(classes) - expect(labels).toEqual(['porn']) - }) -}) diff --git a/packages/pds/tests/labeler/labeler.test.ts b/packages/pds/tests/labeler/labeler.test.ts deleted file mode 100644 index 646e5d621d9..00000000000 --- a/packages/pds/tests/labeler/labeler.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { AtUri, BlobRef } from '@atproto/api' -import { runTestServer, CloseFn, TestServerInfo } from '../_util' -import { Labeler } from '../../src/labeler' -import { AppContext, Database } from '../../src' -import { BlobStore, cidForRecord } from '@atproto/repo' -import { keywordLabeling } from '../../src/labeler/util' -import { cidForCbor, TID } from '@atproto/common' -import { LabelService } from '../../src/app-view/services/label' -import { BackgroundQueue } from '../../src/event-stream/background-queue' -import { CID } from 'multiformats/cid' - -// outside of test suite so that TestLabeler can access them -let badCid1: CID | undefined = undefined -let badCid2: CID | undefined = undefined - -describe('labeler', () => { - let server: TestServerInfo - let close: CloseFn - let labeler: Labeler - let labelSrvc: LabelService - let ctx: AppContext - let labelerDid: string - let badBlob1: BlobRef - let badBlob2: BlobRef - let goodBlob: BlobRef - - beforeAll(async () => { - server = await runTestServer({ - dbPostgresSchema: 'labeler', - }) - close = server.close - ctx = server.ctx - labelerDid = ctx.cfg.labelerDid - labeler = new TestLabeler({ - db: ctx.db, - blobstore: ctx.blobstore, - backgroundQueue: ctx.backgroundQueue, - labelerDid, - keywords: { label_me: 'test-label', another_label: 'another-label' }, - }) - labelSrvc = ctx.services.appView.label(ctx.db) - const bytes1 = new Uint8Array([1, 2, 3, 4]) - const bytes2 = new Uint8Array([5, 6, 7, 8]) - const bytes3 = new Uint8Array([4, 3, 2, 1]) - const cid1 = await cidForCbor(bytes1) - const cid2 = await cidForCbor(bytes2) - const cid3 = await cidForCbor(bytes3) - ctx.blobstore.putPermanent(cid1, bytes1) - ctx.blobstore.putPermanent(cid2, bytes2) - ctx.blobstore.putPermanent(cid3, bytes3) - badBlob1 = new BlobRef(cid1, 'image/jpeg', 4) - badBlob2 = new BlobRef(cid2, 'image/jpeg', 4) - goodBlob = new BlobRef(cid3, 'image/jpeg', 4) - badCid1 = badBlob1.ref - badCid2 = badBlob2.ref - }) - - afterAll(async () => { - await close() - }) - - it('labels text in posts', async () => { - const post = { - $type: 'app.bsky.feed.post', - text: 'blah blah label_me', - createdAt: new Date().toISOString(), - } - const cid = await cidForRecord(post) - const uri = postUri() - labeler.processRecord(uri, post) - await labeler.processAll() - await server.processAll() - const labels = await labelSrvc.getLabels(uri.toString()) - expect(labels.length).toBe(1) - expect(labels[0]).toMatchObject({ - src: labelerDid, - uri: uri.toString(), - cid: cid.toString(), - val: 'test-label', - neg: false, - }) - }) - - it('labels embeds in posts', async () => { - const post = { - $type: 'app.bsky.feed.post', - text: 'blah blah', - embed: { - $type: 'app.bsky.embed.images', - images: [ - { - image: badBlob1, - alt: 'img', - }, - { - image: badBlob2, - alt: 'another_label', - }, - { - image: goodBlob, - alt: 'img', - }, - ], - }, - createdAt: new Date().toISOString(), - } - const uri = postUri() - labeler.processRecord(uri, post) - await labeler.processAll() - await server.processAll() - const dbLabels = await labelSrvc.getLabels(uri.toString()) - const labels = dbLabels.map((row) => row.val).sort() - expect(labels).toEqual( - ['another-label', 'img-label', 'other-img-label'].sort(), - ) - }) - - it('retrieves repo labels on profile views', async () => { - await ctx.db.db - .insertInto('label') - .values({ - src: labelerDid, - uri: aliceDid, - cid: '', - val: 'repo-label', - neg: 0, - cts: new Date().toISOString(), - }) - .execute() - await server.processAll() - - const labels = await labelSrvc.getLabelsForProfile('did:example:alice') - // 4 from earlier & then just added one - expect(labels.length).toBe(1) - expect(labels[0]).toMatchObject({ - src: labelerDid, - uri: aliceDid, - val: 'repo-label', - neg: false, - }) - }) -}) - -const aliceDid = 'did:example:alice' - -const postUri = () => AtUri.make(aliceDid, 'app.bsky.feed.post', TID.nextStr()) - -class TestLabeler extends Labeler { - hiveApiKey: string - keywords: Record - - constructor(opts: { - db: Database - blobstore: BlobStore - backgroundQueue: BackgroundQueue - labelerDid: string - keywords: Record - }) { - const { db, blobstore, backgroundQueue, labelerDid, keywords } = opts - super({ db, blobstore, backgroundQueue, labelerDid }) - this.keywords = keywords - } - - async labelText(text: string): Promise { - return keywordLabeling(this.keywords, text) - } - - async labelImg(cid: CID): Promise { - if (cid.equals(badCid1)) { - return ['img-label'] - } - - if (cid.equals(badCid2)) { - return ['other-img-label'] - } - return [] - } -} diff --git a/packages/pds/tests/moderation.test.ts b/packages/pds/tests/moderation.test.ts index 34e52a156a0..edbb23c6578 100644 --- a/packages/pds/tests/moderation.test.ts +++ b/packages/pds/tests/moderation.test.ts @@ -1,4 +1,4 @@ -import AtpAgent, { ComAtprotoAdminTakeModerationAction } from '@atproto/api' +import AtpAgent from '@atproto/api' import { AtUri } from '@atproto/syntax' import { BlobNotFoundError } from '@atproto/repo' import { @@ -827,164 +827,6 @@ describe('moderation', () => { ) }) - it('negates an existing label and reverses.', async () => { - const { ctx } = server - const post = sc.posts[sc.dids.bob][0].ref - const labelingService = ctx.services.appView.label(ctx.db) - await labelingService.formatAndCreate( - ctx.cfg.labelerDid, - post.uriStr, - post.cidStr, - { create: ['kittens'] }, - ) - const action = await actionWithLabels({ - negateLabelVals: ['kittens'], - subject: { - $type: 'com.atproto.repo.strongRef', - uri: post.uriStr, - cid: post.cidStr, - }, - }) - await expect(getRecordLabels(post.uriStr)).resolves.toEqual([]) - await reverse(action.id) - await expect(getRecordLabels(post.uriStr)).resolves.toEqual(['kittens']) - // Cleanup - await labelingService.formatAndCreate( - ctx.cfg.labelerDid, - post.uriStr, - post.cidStr, - { negate: ['kittens'] }, - ) - }) - - it('no-ops when negating an already-negated label and reverses.', async () => { - const { ctx } = server - const post = sc.posts[sc.dids.bob][0].ref - const labelingService = ctx.services.appView.label(ctx.db) - const action = await actionWithLabels({ - negateLabelVals: ['bears'], - subject: { - $type: 'com.atproto.repo.strongRef', - uri: post.uriStr, - cid: post.cidStr, - }, - }) - await expect(getRecordLabels(post.uriStr)).resolves.toEqual([]) - await reverse(action.id) - await expect(getRecordLabels(post.uriStr)).resolves.toEqual(['bears']) - // Cleanup - await labelingService.formatAndCreate( - ctx.cfg.labelerDid, - post.uriStr, - post.cidStr, - { negate: ['bears'] }, - ) - }) - - it('creates non-existing labels and reverses.', async () => { - const post = sc.posts[sc.dids.bob][0].ref - const action = await actionWithLabels({ - createLabelVals: ['puppies', 'doggies'], - negateLabelVals: [], - subject: { - $type: 'com.atproto.repo.strongRef', - uri: post.uriStr, - cid: post.cidStr, - }, - }) - await expect(getRecordLabels(post.uriStr)).resolves.toEqual([ - 'puppies', - 'doggies', - ]) - await reverse(action.id) - await expect(getRecordLabels(post.uriStr)).resolves.toEqual([]) - }) - - it('no-ops when creating an existing label and reverses.', async () => { - const { ctx } = server - const post = sc.posts[sc.dids.bob][0].ref - const labelingService = ctx.services.appView.label(ctx.db) - await labelingService.formatAndCreate( - ctx.cfg.labelerDid, - post.uriStr, - post.cidStr, - { create: ['birds'] }, - ) - const action = await actionWithLabels({ - createLabelVals: ['birds'], - subject: { - $type: 'com.atproto.repo.strongRef', - uri: post.uriStr, - cid: post.cidStr, - }, - }) - await expect(getRecordLabels(post.uriStr)).resolves.toEqual(['birds']) - await reverse(action.id) - await expect(getRecordLabels(post.uriStr)).resolves.toEqual([]) - }) - - it('creates labels on a repo and reverses.', async () => { - const action = await actionWithLabels({ - createLabelVals: ['puppies', 'doggies'], - negateLabelVals: [], - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.bob, - }, - }) - await expect(getRepoLabels(sc.dids.bob)).resolves.toEqual([ - 'puppies', - 'doggies', - ]) - await reverse(action.id) - await expect(getRepoLabels(sc.dids.bob)).resolves.toEqual([]) - }) - - it('creates and negates labels on a repo and reverses.', async () => { - const { ctx } = server - const labelingService = ctx.services.appView.label(ctx.db) - await labelingService.formatAndCreate( - ctx.cfg.labelerDid, - sc.dids.bob, - null, - { create: ['kittens'] }, - ) - const action = await actionWithLabels({ - createLabelVals: ['puppies'], - negateLabelVals: ['kittens'], - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.bob, - }, - }) - await expect(getRepoLabels(sc.dids.bob)).resolves.toEqual(['puppies']) - await reverse(action.id) - await expect(getRepoLabels(sc.dids.bob)).resolves.toEqual(['kittens']) - }) - - it('does not allow triage moderators to label.', async () => { - const attemptLabel = agent.api.com.atproto.admin.takeModerationAction( - { - action: ACKNOWLEDGE, - createdBy: 'did:example:moderator', - reason: 'Y', - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.bob, - }, - negateLabelVals: ['a'], - createLabelVals: ['b', 'c'], - }, - { - encoding: 'application/json', - headers: { authorization: triageAuth() }, - }, - ) - await expect(attemptLabel).rejects.toThrow( - 'Must be a full moderator to label content', - ) - }) - it('allows full moderators to takedown.', async () => { const { data: action } = await agent.api.com.atproto.admin.takeModerationAction( @@ -1017,7 +859,6 @@ describe('moderation', () => { $type: 'com.atproto.admin.defs#repoRef', did: sc.dids.bob, }, - createLabelVals: ['takendown'], // Use negative value to set the expiry time in the past so that the action is automatically reversed // right away without having to wait n number of hours for a successful assertion durationInHours: -1, @@ -1028,8 +869,6 @@ describe('moderation', () => { }, ) - const labelsAfterTakedown = await getRepoLabels(sc.dids.bob) - expect(labelsAfterTakedown).toContain('takendown') // In the actual app, this will be instantiated and run on server startup const periodicReversal = new PeriodicModerationActionReversal(server.ctx) await periodicReversal.findAndRevertDueActions() @@ -1046,10 +885,6 @@ describe('moderation', () => { createdBy: action.createdBy, reason: '[SCHEDULED_REVERSAL] Reverting action as originally scheduled', }) - - // Verify that labels are also reversed when takedown action is reversed - const labelsAfterReversal = await getRepoLabels(sc.dids.bob) - expect(labelsAfterReversal).not.toContain('takendown') }) it('does not allow non-full moderators to takedown.', async () => { @@ -1074,26 +909,6 @@ describe('moderation', () => { ) }) - async function actionWithLabels( - opts: Partial & { - subject: ComAtprotoAdminTakeModerationAction.InputSchema['subject'] - }, - ) { - const result = await agent.api.com.atproto.admin.takeModerationAction( - { - action: FLAG, - createdBy: 'did:example:admin', - reason: 'Y', - ...opts, - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - return result.data - } - async function reverse(actionId: number) { await agent.api.com.atproto.admin.reverseModerationAction( { @@ -1107,24 +922,6 @@ describe('moderation', () => { }, ) } - - async function getRecordLabels(uri: string) { - const result = await agent.api.com.atproto.admin.getRecord( - { uri }, - { headers: { authorization: adminAuth() } }, - ) - const labels = result.data.labels ?? [] - return labels.map((l) => l.val) - } - - async function getRepoLabels(did: string) { - const result = await agent.api.com.atproto.admin.getRepo( - { did }, - { headers: { authorization: adminAuth() } }, - ) - const labels = result.data.labels ?? [] - return labels.map((l) => l.val) - } }) describe('blob takedown', () => { diff --git a/packages/pds/tests/proxied/admin.test.ts b/packages/pds/tests/proxied/admin.test.ts index 127717225a6..98ec8f1f70c 100644 --- a/packages/pds/tests/proxied/admin.test.ts +++ b/packages/pds/tests/proxied/admin.test.ts @@ -24,7 +24,6 @@ describe('proxies admin requests', () => { dbPostgresSchema: 'proxy_admin', pds: { // @NOTE requires admin pass be the same on pds and appview, which TestNetwork is handling for us. - enableInProcessAppView: true, bskyAppViewModeration: true, inviteRequired: true, }, @@ -246,7 +245,7 @@ describe('proxies admin requests', () => { }) it('takesdown and labels repos, and reverts.', async () => { - const { db, services } = network.pds.ctx + const { db, services } = network.bsky.ctx // takedown repo const { data: action } = await agent.api.com.atproto.admin.takeModerationAction( @@ -276,8 +275,8 @@ describe('proxies admin requests', () => { await expect(tryGetProfileAppview).rejects.toThrow( 'Account has been taken down', ) - const labelsA = await services.appView - .label(db) + const labelsA = await services + .label(db.getPrimary()) .getLabels(sc.dids.alice, { includeNeg: false, skipCache: true }) expect(labelsA.map((l) => l.val)).toEqual(['dogs']) // reverse action @@ -298,14 +297,14 @@ describe('proxies admin requests', () => { expect(profileAppview).toEqual( expect.objectContaining({ did: sc.dids.alice, handle: 'alice.test' }), ) - const labelsB = await services.appView - .label(db) + const labelsB = await services + .label(db.getPrimary()) .getLabels(sc.dids.alice, { includeNeg: false, skipCache: true }) expect(labelsB.map((l) => l.val)).toEqual(['cats']) }) it('takesdown and labels records, and reverts.', async () => { - const { db, services } = network.pds.ctx + const { db, services } = network.bsky.ctx const post = sc.posts[sc.dids.alice][0] // takedown post const { data: action } = @@ -335,8 +334,8 @@ describe('proxies admin requests', () => { }, ) await expect(tryGetPostAppview).rejects.toThrow(NotFoundError) - const labelsA = await services.appView - .label(db) + const labelsA = await services + .label(db.getPrimary()) .getLabels(post.ref.uriStr, { includeNeg: false, skipCache: true }) expect(labelsA.map((l) => l.val)).toEqual(['dogs']) // reverse action @@ -357,8 +356,8 @@ describe('proxies admin requests', () => { expect(threadAppview.thread.post).toEqual( expect.objectContaining({ uri: post.ref.uriStr, cid: post.ref.cidStr }), ) - const labelsB = await services.appView - .label(db) + const labelsB = await services + .label(db.getPrimary()) .getLabels(post.ref.uriStr, { includeNeg: false, skipCache: true }) expect(labelsB.map((l) => l.val)).toEqual(['cats']) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 843e3acd27e..fd9ef8cdbad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -213,9 +213,6 @@ importers: ioredis: specifier: ^5.3.2 version: 5.3.2 - iso-datestring-validator: - specifier: ^2.2.2 - version: 2.2.2 kysely: specifier: ^0.22.0 version: 0.22.0 @@ -292,6 +289,9 @@ importers: cbor-x: specifier: ^1.5.1 version: 1.5.1 + iso-datestring-validator: + specifier: ^2.2.2 + version: 2.2.2 multiformats: specifier: ^9.9.0 version: 9.9.0 @@ -531,9 +531,6 @@ importers: ioredis: specifier: ^5.3.2 version: 5.3.2 - iso-datestring-validator: - specifier: ^2.2.2 - version: 2.2.2 jsonwebtoken: specifier: ^8.5.1 version: 8.5.1 From 5b06d07422081e7cff38f48ce0fef9fa9c2c9f15 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Tue, 26 Sep 2023 11:10:20 -0500 Subject: [PATCH 42/46] Refactor PDS appview routes (#1673) move routes around --- .../api/app/bsky/actor/getProfile.ts | 10 +-- .../api/app/bsky/actor/getProfiles.ts | 8 +- .../api/app/bsky/actor/getSuggestions.ts | 4 +- packages/pds/src/api/app/bsky/actor/index.ts | 10 +++ .../api/app/bsky/actor/searchActors.ts | 4 +- .../app/bsky/actor/searchActorsTypeahead.ts | 4 +- .../api/app/bsky/feed/getActorFeeds.ts | 4 +- .../api/app/bsky/feed/getActorLikes.ts | 10 +-- .../api/app/bsky/feed/getAuthorFeed.ts | 12 +-- .../api/app/bsky/feed/getFeed.ts | 4 +- .../api/app/bsky/feed/getFeedGenerator.ts | 4 +- .../api/app/bsky/feed/getFeedGenerators.ts | 4 +- .../api/app/bsky/feed/getLikes.ts | 4 +- .../api/app/bsky/feed/getListFeed.ts | 4 +- .../api/app/bsky/feed/getPostThread.ts | 12 +-- .../api/app/bsky/feed/getPosts.ts | 4 +- .../api/app/bsky/feed/getRepostedBy.ts | 4 +- .../api/app/bsky/feed/getSuggestedFeeds.ts | 4 +- .../api/app/bsky/feed/getTimeline.ts | 8 +- packages/pds/src/api/app/bsky/feed/index.ts | 31 ++++++++ .../api/app/bsky/graph/getBlocks.ts | 4 +- .../api/app/bsky/graph/getFollowers.ts | 6 +- .../api/app/bsky/graph/getFollows.ts | 6 +- .../api/app/bsky/graph/getList.ts | 4 +- .../api/app/bsky/graph/getListBlocks.ts | 4 +- .../api/app/bsky/graph/getListMutes.ts | 4 +- .../api/app/bsky/graph/getLists.ts | 4 +- .../api/app/bsky/graph/getMutes.ts | 4 +- .../bsky/graph/getSuggestedFollowsByActor.ts | 4 +- packages/pds/src/api/app/bsky/graph/index.ts | 31 ++++++++ .../api/app/bsky/graph/muteActor.ts | 4 +- .../api/app/bsky/graph/muteActorList.ts | 6 +- .../api/app/bsky/graph/unmuteActor.ts | 4 +- .../api/app/bsky/graph/unmuteActorList.ts | 4 +- packages/pds/src/api/app/bsky/index.ts | 8 ++ .../app/bsky/notification/getUnreadCount.ts | 4 +- .../src/api/app/bsky/notification/index.ts | 14 ++++ .../bsky/notification/listNotifications.ts | 4 +- .../api/app/bsky/notification/registerPush.ts | 4 +- .../api/app/bsky/notification/updateSeen.ts | 4 +- .../src/api/app/bsky/unspecced/getPopular.ts | 23 ++++++ .../unspecced/getPopularFeedGenerators.ts | 21 +++++ .../pds/src/api/app/bsky/unspecced/index.ts | 10 +++ .../api/app/bsky/util/read-after-write.ts | 6 +- .../api/app/bsky/util/resolver.ts | 2 +- .../pds/src/app-view/api/app/bsky/index.ts | 77 ------------------- .../src/app-view/api/app/bsky/unspecced.ts | 68 ---------------- packages/pds/src/app-view/api/index.ts | 8 -- packages/pds/src/app-view/logger.ts | 3 - packages/pds/src/index.ts | 2 - 50 files changed, 243 insertions(+), 253 deletions(-) rename packages/pds/src/{app-view => }/api/app/bsky/actor/getProfile.ts (76%) rename packages/pds/src/{app-view => }/api/app/bsky/actor/getProfiles.ts (82%) rename packages/pds/src/{app-view => }/api/app/bsky/actor/getSuggestions.ts (82%) rename packages/pds/src/{app-view => }/api/app/bsky/actor/searchActors.ts (82%) rename packages/pds/src/{app-view => }/api/app/bsky/actor/searchActorsTypeahead.ts (83%) rename packages/pds/src/{app-view => }/api/app/bsky/feed/getActorFeeds.ts (82%) rename packages/pds/src/{app-view => }/api/app/bsky/feed/getActorLikes.ts (82%) rename packages/pds/src/{app-view => }/api/app/bsky/feed/getAuthorFeed.ts (83%) rename packages/pds/src/{app-view => }/api/app/bsky/feed/getFeed.ts (87%) rename packages/pds/src/{app-view => }/api/app/bsky/feed/getFeedGenerator.ts (83%) rename packages/pds/src/{app-view => }/api/app/bsky/feed/getFeedGenerators.ts (83%) rename packages/pds/src/{app-view => }/api/app/bsky/feed/getLikes.ts (82%) rename packages/pds/src/{app-view => }/api/app/bsky/feed/getListFeed.ts (82%) rename packages/pds/src/{app-view => }/api/app/bsky/feed/getPostThread.ts (94%) rename packages/pds/src/{app-view => }/api/app/bsky/feed/getPosts.ts (82%) rename packages/pds/src/{app-view => }/api/app/bsky/feed/getRepostedBy.ts (82%) rename packages/pds/src/{app-view => }/api/app/bsky/feed/getSuggestedFeeds.ts (83%) rename packages/pds/src/{app-view => }/api/app/bsky/feed/getTimeline.ts (76%) create mode 100644 packages/pds/src/api/app/bsky/feed/index.ts rename packages/pds/src/{app-view => }/api/app/bsky/graph/getBlocks.ts (82%) rename packages/pds/src/{app-view => }/api/app/bsky/graph/getFollowers.ts (76%) rename packages/pds/src/{app-view => }/api/app/bsky/graph/getFollows.ts (76%) rename packages/pds/src/{app-view => }/api/app/bsky/graph/getList.ts (82%) rename packages/pds/src/{app-view => }/api/app/bsky/graph/getListBlocks.ts (82%) rename packages/pds/src/{app-view => }/api/app/bsky/graph/getListMutes.ts (82%) rename packages/pds/src/{app-view => }/api/app/bsky/graph/getLists.ts (82%) rename packages/pds/src/{app-view => }/api/app/bsky/graph/getMutes.ts (82%) rename packages/pds/src/{app-view => }/api/app/bsky/graph/getSuggestedFollowsByActor.ts (84%) create mode 100644 packages/pds/src/api/app/bsky/graph/index.ts rename packages/pds/src/{app-view => }/api/app/bsky/graph/muteActor.ts (90%) rename packages/pds/src/{app-view => }/api/app/bsky/graph/muteActorList.ts (85%) rename packages/pds/src/{app-view => }/api/app/bsky/graph/unmuteActor.ts (89%) rename packages/pds/src/{app-view => }/api/app/bsky/graph/unmuteActorList.ts (86%) rename packages/pds/src/{app-view => }/api/app/bsky/notification/getUnreadCount.ts (83%) create mode 100644 packages/pds/src/api/app/bsky/notification/index.ts rename packages/pds/src/{app-view => }/api/app/bsky/notification/listNotifications.ts (83%) rename packages/pds/src/{app-view => }/api/app/bsky/notification/registerPush.ts (93%) rename packages/pds/src/{app-view => }/api/app/bsky/notification/updateSeen.ts (92%) create mode 100644 packages/pds/src/api/app/bsky/unspecced/getPopular.ts create mode 100644 packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts create mode 100644 packages/pds/src/api/app/bsky/unspecced/index.ts rename packages/pds/src/{app-view => }/api/app/bsky/util/read-after-write.ts (91%) rename packages/pds/src/{app-view => }/api/app/bsky/util/resolver.ts (93%) delete mode 100644 packages/pds/src/app-view/api/app/bsky/index.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/unspecced.ts delete mode 100644 packages/pds/src/app-view/api/index.ts delete mode 100644 packages/pds/src/app-view/logger.ts diff --git a/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts b/packages/pds/src/api/app/bsky/actor/getProfile.ts similarity index 76% rename from packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts rename to packages/pds/src/api/app/bsky/actor/getProfile.ts index 4d510bda720..52858515827 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts +++ b/packages/pds/src/api/app/bsky/actor/getProfile.ts @@ -1,9 +1,9 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' -import { authPassthru } from '../../../../../api/com/atproto/admin/util' -import { OutputSchema } from '../../../../../lexicon/types/app/bsky/actor/getProfile' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { authPassthru } from '../../../../api/com/atproto/admin/util' +import { OutputSchema } from '../../../../lexicon/types/app/bsky/actor/getProfile' import { handleReadAfterWrite } from '../util/read-after-write' -import { LocalRecords } from '../../../../../services/local' +import { LocalRecords } from '../../../../services/local' export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.getProfile({ diff --git a/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts b/packages/pds/src/api/app/bsky/actor/getProfiles.ts similarity index 82% rename from packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts rename to packages/pds/src/api/app/bsky/actor/getProfiles.ts index 08f30dfe690..46af4b08a0c 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts +++ b/packages/pds/src/api/app/bsky/actor/getProfiles.ts @@ -1,7 +1,7 @@ -import AppContext from '../../../../../context' -import { Server } from '../../../../../lexicon' -import { OutputSchema } from '../../../../../lexicon/types/app/bsky/actor/getProfiles' -import { LocalRecords } from '../../../../../services/local' +import AppContext from '../../../../context' +import { Server } from '../../../../lexicon' +import { OutputSchema } from '../../../../lexicon/types/app/bsky/actor/getProfiles' +import { LocalRecords } from '../../../../services/local' import { handleReadAfterWrite } from '../util/read-after-write' export default function (server: Server, ctx: AppContext) { diff --git a/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts b/packages/pds/src/api/app/bsky/actor/getSuggestions.ts similarity index 82% rename from packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts rename to packages/pds/src/api/app/bsky/actor/getSuggestions.ts index 9047634dd30..c3ceb16cf14 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts +++ b/packages/pds/src/api/app/bsky/actor/getSuggestions.ts @@ -1,5 +1,5 @@ -import AppContext from '../../../../../context' -import { Server } from '../../../../../lexicon' +import AppContext from '../../../../context' +import { Server } from '../../../../lexicon' export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.getSuggestions({ diff --git a/packages/pds/src/api/app/bsky/actor/index.ts b/packages/pds/src/api/app/bsky/actor/index.ts index a2561170571..db69616f7e8 100644 --- a/packages/pds/src/api/app/bsky/actor/index.ts +++ b/packages/pds/src/api/app/bsky/actor/index.ts @@ -1,9 +1,19 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import getPreferences from './getPreferences' +import getProfile from './getProfile' +import getProfiles from './getProfiles' +import getSuggestions from './getSuggestions' import putPreferences from './putPreferences' +import searchActors from './searchActors' +import searchActorsTypeahead from './searchActorsTypeahead' export default function (server: Server, ctx: AppContext) { getPreferences(server, ctx) + getProfile(server, ctx) + getProfiles(server, ctx) + getSuggestions(server, ctx) putPreferences(server, ctx) + searchActors(server, ctx) + searchActorsTypeahead(server, ctx) } diff --git a/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts b/packages/pds/src/api/app/bsky/actor/searchActors.ts similarity index 82% rename from packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts rename to packages/pds/src/api/app/bsky/actor/searchActors.ts index ff88cd8d233..921f4363bfc 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts +++ b/packages/pds/src/api/app/bsky/actor/searchActors.ts @@ -1,5 +1,5 @@ -import AppContext from '../../../../../context' -import { Server } from '../../../../../lexicon' +import AppContext from '../../../../context' +import { Server } from '../../../../lexicon' export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.searchActors({ diff --git a/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts b/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts similarity index 83% rename from packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts rename to packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts index 5853cf3104e..d8ef8f72dda 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts @@ -1,5 +1,5 @@ -import AppContext from '../../../../../context' -import { Server } from '../../../../../lexicon' +import AppContext from '../../../../context' +import { Server } from '../../../../lexicon' export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.searchActorsTypeahead({ diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts b/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts similarity index 82% rename from packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts rename to packages/pds/src/api/app/bsky/feed/getActorFeeds.ts index 57d7d72b6e4..ec77754b4b2 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts +++ b/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts @@ -1,5 +1,5 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getActorFeeds({ diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts b/packages/pds/src/api/app/bsky/feed/getActorLikes.ts similarity index 82% rename from packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts rename to packages/pds/src/api/app/bsky/feed/getActorLikes.ts index 6e56bc81214..53557c6ae4c 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts +++ b/packages/pds/src/api/app/bsky/feed/getActorLikes.ts @@ -1,9 +1,9 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' -import { OutputSchema } from '../../../../../lexicon/types/app/bsky/feed/getAuthorFeed' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { OutputSchema } from '../../../../lexicon/types/app/bsky/feed/getAuthorFeed' import { handleReadAfterWrite } from '../util/read-after-write' -import { authPassthru } from '../../../../../api/com/atproto/admin/util' -import { LocalRecords } from '../../../../../services/local' +import { authPassthru } from '../../../../api/com/atproto/admin/util' +import { LocalRecords } from '../../../../services/local' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getActorLikes({ diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts similarity index 83% rename from packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts rename to packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts index 735004f52e3..7237a2df755 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts @@ -1,10 +1,10 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' -import { OutputSchema } from '../../../../../lexicon/types/app/bsky/feed/getAuthorFeed' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { OutputSchema } from '../../../../lexicon/types/app/bsky/feed/getAuthorFeed' import { handleReadAfterWrite } from '../util/read-after-write' -import { authPassthru } from '../../../../../api/com/atproto/admin/util' -import { LocalRecords } from '../../../../../services/local' -import { isReasonRepost } from '../../../../../lexicon/types/app/bsky/feed/defs' +import { authPassthru } from '../../../../api/com/atproto/admin/util' +import { LocalRecords } from '../../../../services/local' +import { isReasonRepost } from '../../../../lexicon/types/app/bsky/feed/defs' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getAuthorFeed({ diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts b/packages/pds/src/api/app/bsky/feed/getFeed.ts similarity index 87% rename from packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts rename to packages/pds/src/api/app/bsky/feed/getFeed.ts index a5cf4176d4e..6ed14b0546c 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getFeed.ts @@ -1,5 +1,5 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getFeed({ diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts b/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts similarity index 83% rename from packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts rename to packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts index 210b1be54ef..b9451ca16c3 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts +++ b/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts @@ -1,5 +1,5 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getFeedGenerator({ diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts b/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts similarity index 83% rename from packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts rename to packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts index 36353f015b7..1d085830004 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts +++ b/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts @@ -1,5 +1,5 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getFeedGenerators({ diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts b/packages/pds/src/api/app/bsky/feed/getLikes.ts similarity index 82% rename from packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts rename to packages/pds/src/api/app/bsky/feed/getLikes.ts index a78469355d5..75197acbcc8 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts +++ b/packages/pds/src/api/app/bsky/feed/getLikes.ts @@ -1,5 +1,5 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getLikes({ diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getListFeed.ts b/packages/pds/src/api/app/bsky/feed/getListFeed.ts similarity index 82% rename from packages/pds/src/app-view/api/app/bsky/feed/getListFeed.ts rename to packages/pds/src/api/app/bsky/feed/getListFeed.ts index 1ac1e983861..7344b2476ba 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getListFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getListFeed.ts @@ -1,5 +1,5 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getListFeed({ diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts b/packages/pds/src/api/app/bsky/feed/getPostThread.ts similarity index 94% rename from packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts rename to packages/pds/src/api/app/bsky/feed/getPostThread.ts index be1e8004bac..e8dd062da14 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts +++ b/packages/pds/src/api/app/bsky/feed/getPostThread.ts @@ -1,22 +1,22 @@ import { AtUri } from '@atproto/syntax' import { AppBskyFeedGetPostThread } from '@atproto/api' import { Headers } from '@atproto/xrpc' -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' import { ThreadViewPost, isThreadViewPost, -} from '../../../../../lexicon/types/app/bsky/feed/defs' -import { Record as PostRecord } from '../../../../../lexicon/types/app/bsky/feed/post' +} from '../../../../lexicon/types/app/bsky/feed/defs' +import { Record as PostRecord } from '../../../../lexicon/types/app/bsky/feed/post' import { OutputSchema, QueryParams, -} from '../../../../../lexicon/types/app/bsky/feed/getPostThread' +} from '../../../../lexicon/types/app/bsky/feed/getPostThread' import { LocalRecords, LocalService, RecordDescript, -} from '../../../../../services/local' +} from '../../../../services/local' import { getLocalLag, getRepoRev, diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts b/packages/pds/src/api/app/bsky/feed/getPosts.ts similarity index 82% rename from packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts rename to packages/pds/src/api/app/bsky/feed/getPosts.ts index f394ae57a08..05173c48ef9 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts +++ b/packages/pds/src/api/app/bsky/feed/getPosts.ts @@ -1,5 +1,5 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getPosts({ diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts b/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts similarity index 82% rename from packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts rename to packages/pds/src/api/app/bsky/feed/getRepostedBy.ts index 9704597ad27..44a5b15191d 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts +++ b/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts @@ -1,5 +1,5 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getRepostedBy({ diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getSuggestedFeeds.ts b/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts similarity index 83% rename from packages/pds/src/app-view/api/app/bsky/feed/getSuggestedFeeds.ts rename to packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts index 37f06390fb9..9c8d338104b 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getSuggestedFeeds.ts +++ b/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts @@ -1,5 +1,5 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getSuggestedFeeds({ diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts b/packages/pds/src/api/app/bsky/feed/getTimeline.ts similarity index 76% rename from packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts rename to packages/pds/src/api/app/bsky/feed/getTimeline.ts index 08676b3a943..7d4e52ce918 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts +++ b/packages/pds/src/api/app/bsky/feed/getTimeline.ts @@ -1,8 +1,8 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' -import { OutputSchema } from '../../../../../lexicon/types/app/bsky/feed/getTimeline' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { OutputSchema } from '../../../../lexicon/types/app/bsky/feed/getTimeline' import { handleReadAfterWrite } from '../util/read-after-write' -import { LocalRecords } from '../../../../../services/local' +import { LocalRecords } from '../../../../services/local' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getTimeline({ diff --git a/packages/pds/src/api/app/bsky/feed/index.ts b/packages/pds/src/api/app/bsky/feed/index.ts new file mode 100644 index 00000000000..8c4cfaa8b5f --- /dev/null +++ b/packages/pds/src/api/app/bsky/feed/index.ts @@ -0,0 +1,31 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import getActorFeeds from './getActorFeeds' +import getActorLikes from './getActorLikes' +import getAuthorFeed from './getAuthorFeed' +import getFeed from './getFeed' +import getFeedGenerator from './getFeedGenerator' +import getFeedGenerators from './getFeedGenerators' +import getLikes from './getLikes' +import getListFeed from './getListFeed' +import getPosts from './getPosts' +import getPostThread from './getPostThread' +import getRepostedBy from './getRepostedBy' +import getSuggestedFeeds from './getSuggestedFeeds' +import getTimeline from './getTimeline' + +export default function (server: Server, ctx: AppContext) { + getActorFeeds(server, ctx) + getActorLikes(server, ctx) + getAuthorFeed(server, ctx) + getFeed(server, ctx) + getFeedGenerator(server, ctx) + getFeedGenerators(server, ctx) + getLikes(server, ctx) + getListFeed(server, ctx) + getPosts(server, ctx) + getPostThread(server, ctx) + getRepostedBy(server, ctx) + getSuggestedFeeds(server, ctx) + getTimeline(server, ctx) +} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts b/packages/pds/src/api/app/bsky/graph/getBlocks.ts similarity index 82% rename from packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts rename to packages/pds/src/api/app/bsky/graph/getBlocks.ts index 52f7b4d6f7f..284dafd3034 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts +++ b/packages/pds/src/api/app/bsky/graph/getBlocks.ts @@ -1,5 +1,5 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getBlocks({ diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts b/packages/pds/src/api/app/bsky/graph/getFollowers.ts similarity index 76% rename from packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts rename to packages/pds/src/api/app/bsky/graph/getFollowers.ts index 0c7f23869eb..da0541d7e75 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts +++ b/packages/pds/src/api/app/bsky/graph/getFollowers.ts @@ -1,6 +1,6 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' -import { authPassthru } from '../../../../../api/com/atproto/admin/util' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { authPassthru } from '../../../../api/com/atproto/admin/util' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getFollowers({ diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts b/packages/pds/src/api/app/bsky/graph/getFollows.ts similarity index 76% rename from packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts rename to packages/pds/src/api/app/bsky/graph/getFollows.ts index 5871a213302..f49c812f9ca 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts +++ b/packages/pds/src/api/app/bsky/graph/getFollows.ts @@ -1,6 +1,6 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' -import { authPassthru } from '../../../../../api/com/atproto/admin/util' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { authPassthru } from '../../../../api/com/atproto/admin/util' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getFollows({ diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getList.ts b/packages/pds/src/api/app/bsky/graph/getList.ts similarity index 82% rename from packages/pds/src/app-view/api/app/bsky/graph/getList.ts rename to packages/pds/src/api/app/bsky/graph/getList.ts index ba9b9f9346f..5fd3c93df75 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getList.ts +++ b/packages/pds/src/api/app/bsky/graph/getList.ts @@ -1,5 +1,5 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getList({ diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getListBlocks.ts b/packages/pds/src/api/app/bsky/graph/getListBlocks.ts similarity index 82% rename from packages/pds/src/app-view/api/app/bsky/graph/getListBlocks.ts rename to packages/pds/src/api/app/bsky/graph/getListBlocks.ts index 98c55e14bc9..04fd55a324e 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getListBlocks.ts +++ b/packages/pds/src/api/app/bsky/graph/getListBlocks.ts @@ -1,5 +1,5 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getListBlocks({ diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts b/packages/pds/src/api/app/bsky/graph/getListMutes.ts similarity index 82% rename from packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts rename to packages/pds/src/api/app/bsky/graph/getListMutes.ts index adae96d58b0..e0a624a3864 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts +++ b/packages/pds/src/api/app/bsky/graph/getListMutes.ts @@ -1,5 +1,5 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getListMutes({ diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts b/packages/pds/src/api/app/bsky/graph/getLists.ts similarity index 82% rename from packages/pds/src/app-view/api/app/bsky/graph/getLists.ts rename to packages/pds/src/api/app/bsky/graph/getLists.ts index 893b6d5a9ad..e43a8d2b1d6 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts +++ b/packages/pds/src/api/app/bsky/graph/getLists.ts @@ -1,5 +1,5 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getLists({ diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts b/packages/pds/src/api/app/bsky/graph/getMutes.ts similarity index 82% rename from packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts rename to packages/pds/src/api/app/bsky/graph/getMutes.ts index 596fef9dd15..9aa6b74445c 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts +++ b/packages/pds/src/api/app/bsky/graph/getMutes.ts @@ -1,5 +1,5 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getMutes({ diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getSuggestedFollowsByActor.ts b/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts similarity index 84% rename from packages/pds/src/app-view/api/app/bsky/graph/getSuggestedFollowsByActor.ts rename to packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts index dfafa6b65ea..1db1c7f498f 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getSuggestedFollowsByActor.ts +++ b/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts @@ -1,5 +1,5 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getSuggestedFollowsByActor({ diff --git a/packages/pds/src/api/app/bsky/graph/index.ts b/packages/pds/src/api/app/bsky/graph/index.ts new file mode 100644 index 00000000000..0c18b9e51b4 --- /dev/null +++ b/packages/pds/src/api/app/bsky/graph/index.ts @@ -0,0 +1,31 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import getBlocks from './getBlocks' +import getFollowers from './getFollowers' +import getFollows from './getFollows' +import getList from './getList' +import getListBlocks from './getListBlocks' +import getListMutes from './getListMutes' +import getLists from './getLists' +import getMutes from './getMutes' +import getSuggestedFollowsByActor from './getSuggestedFollowsByActor' +import muteActor from './muteActor' +import muteActorList from './muteActorList' +import unmuteActor from './unmuteActor' +import unmuteActorList from './unmuteActorList' + +export default function (server: Server, ctx: AppContext) { + getBlocks(server, ctx) + getFollowers(server, ctx) + getFollows(server, ctx) + getList(server, ctx) + getListBlocks(server, ctx) + getListMutes(server, ctx) + getLists(server, ctx) + getMutes(server, ctx) + getSuggestedFollowsByActor(server, ctx) + muteActor(server, ctx) + muteActorList(server, ctx) + unmuteActor(server, ctx) + unmuteActorList(server, ctx) +} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/muteActor.ts b/packages/pds/src/api/app/bsky/graph/muteActor.ts similarity index 90% rename from packages/pds/src/app-view/api/app/bsky/graph/muteActor.ts rename to packages/pds/src/api/app/bsky/graph/muteActor.ts index 4cae0c45e27..44d4747f5e9 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/muteActor.ts +++ b/packages/pds/src/api/app/bsky/graph/muteActor.ts @@ -1,6 +1,6 @@ import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.muteActor({ diff --git a/packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts b/packages/pds/src/api/app/bsky/graph/muteActorList.ts similarity index 85% rename from packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts rename to packages/pds/src/api/app/bsky/graph/muteActorList.ts index 728aa2a5837..e554f7fce8b 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts +++ b/packages/pds/src/api/app/bsky/graph/muteActorList.ts @@ -1,6 +1,6 @@ -import { Server } from '../../../../../lexicon' -import * as lex from '../../../../../lexicon/lexicons' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import * as lex from '../../../../lexicon/lexicons' +import AppContext from '../../../../context' import { AtUri } from '@atproto/syntax' import { InvalidRequestError } from '@atproto/xrpc-server' diff --git a/packages/pds/src/app-view/api/app/bsky/graph/unmuteActor.ts b/packages/pds/src/api/app/bsky/graph/unmuteActor.ts similarity index 89% rename from packages/pds/src/app-view/api/app/bsky/graph/unmuteActor.ts rename to packages/pds/src/api/app/bsky/graph/unmuteActor.ts index ed9585a60b5..84819cc1e15 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/unmuteActor.ts +++ b/packages/pds/src/api/app/bsky/graph/unmuteActor.ts @@ -1,6 +1,6 @@ -import { Server } from '../../../../../lexicon' +import { Server } from '../../../../lexicon' import { InvalidRequestError } from '@atproto/xrpc-server' -import AppContext from '../../../../../context' +import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.unmuteActor({ diff --git a/packages/pds/src/app-view/api/app/bsky/graph/unmuteActorList.ts b/packages/pds/src/api/app/bsky/graph/unmuteActorList.ts similarity index 86% rename from packages/pds/src/app-view/api/app/bsky/graph/unmuteActorList.ts rename to packages/pds/src/api/app/bsky/graph/unmuteActorList.ts index 6141bd619bc..ce3c1a4b254 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/unmuteActorList.ts +++ b/packages/pds/src/api/app/bsky/graph/unmuteActorList.ts @@ -1,5 +1,5 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.unmuteActorList({ diff --git a/packages/pds/src/api/app/bsky/index.ts b/packages/pds/src/api/app/bsky/index.ts index ad7675caeb6..8f38a52683f 100644 --- a/packages/pds/src/api/app/bsky/index.ts +++ b/packages/pds/src/api/app/bsky/index.ts @@ -1,7 +1,15 @@ import { Server } from '../../../lexicon' import AppContext from '../../../context' import actor from './actor' +import feed from './feed' +import graph from './graph' +import notification from './notification' +import unspecced from './unspecced' export default function (server: Server, ctx: AppContext) { actor(server, ctx) + feed(server, ctx) + graph(server, ctx) + notification(server, ctx) + unspecced(server, ctx) } diff --git a/packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts b/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts similarity index 83% rename from packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts rename to packages/pds/src/api/app/bsky/notification/getUnreadCount.ts index 61c2b48f1e3..e8100b183c5 100644 --- a/packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts +++ b/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts @@ -1,5 +1,5 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.notification.getUnreadCount({ diff --git a/packages/pds/src/api/app/bsky/notification/index.ts b/packages/pds/src/api/app/bsky/notification/index.ts new file mode 100644 index 00000000000..f5d8ee59449 --- /dev/null +++ b/packages/pds/src/api/app/bsky/notification/index.ts @@ -0,0 +1,14 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +import getUnreadCount from './getUnreadCount' +import listNotifications from './listNotifications' +import registerPush from './registerPush' +import updateSeen from './updateSeen' + +export default function (server: Server, ctx: AppContext) { + getUnreadCount(server, ctx) + listNotifications(server, ctx) + registerPush(server, ctx) + updateSeen(server, ctx) +} diff --git a/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts b/packages/pds/src/api/app/bsky/notification/listNotifications.ts similarity index 83% rename from packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts rename to packages/pds/src/api/app/bsky/notification/listNotifications.ts index eefb3d29a48..2f667172a57 100644 --- a/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts +++ b/packages/pds/src/api/app/bsky/notification/listNotifications.ts @@ -1,5 +1,5 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.notification.listNotifications({ diff --git a/packages/pds/src/app-view/api/app/bsky/notification/registerPush.ts b/packages/pds/src/api/app/bsky/notification/registerPush.ts similarity index 93% rename from packages/pds/src/app-view/api/app/bsky/notification/registerPush.ts rename to packages/pds/src/api/app/bsky/notification/registerPush.ts index bb2d329e8e7..1abe5b83ed7 100644 --- a/packages/pds/src/app-view/api/app/bsky/notification/registerPush.ts +++ b/packages/pds/src/api/app/bsky/notification/registerPush.ts @@ -1,5 +1,5 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' import { getNotif } from '@atproto/identity' import { InvalidRequestError } from '@atproto/xrpc-server' import { AtpAgent } from '@atproto/api' diff --git a/packages/pds/src/app-view/api/app/bsky/notification/updateSeen.ts b/packages/pds/src/api/app/bsky/notification/updateSeen.ts similarity index 92% rename from packages/pds/src/app-view/api/app/bsky/notification/updateSeen.ts rename to packages/pds/src/api/app/bsky/notification/updateSeen.ts index 0524a1e8581..2c115a61f67 100644 --- a/packages/pds/src/app-view/api/app/bsky/notification/updateSeen.ts +++ b/packages/pds/src/api/app/bsky/notification/updateSeen.ts @@ -1,6 +1,6 @@ -import { Server } from '../../../../../lexicon' import { InvalidRequestError } from '@atproto/xrpc-server' -import AppContext from '../../../../../context' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.notification.updateSeen({ diff --git a/packages/pds/src/api/app/bsky/unspecced/getPopular.ts b/packages/pds/src/api/app/bsky/unspecced/getPopular.ts new file mode 100644 index 00000000000..c4b4736cdc0 --- /dev/null +++ b/packages/pds/src/api/app/bsky/unspecced/getPopular.ts @@ -0,0 +1,23 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +// THIS IS A TEMPORARY UNSPECCED ROUTE +export default function (server: Server, ctx: AppContext) { + server.app.bsky.unspecced.getPopular({ + auth: ctx.accessVerifier, + handler: async ({ auth, params }) => { + const requester = auth.credentials.did + const HOT_CLASSIC_URI = + 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/hot-classic' + const HOT_CLASSIC_DID = 'did:plc:5fllqkujj6kqp5izd5jg7gox' + const res = await ctx.appviewAgent.api.app.bsky.feed.getFeed( + { feed: HOT_CLASSIC_URI, limit: params.limit, cursor: params.cursor }, + await ctx.serviceAuthHeaders(requester, HOT_CLASSIC_DID), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts new file mode 100644 index 00000000000..3f41f18560c --- /dev/null +++ b/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -0,0 +1,21 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +// THIS IS A TEMPORARY UNSPECCED ROUTE +export default function (server: Server, ctx: AppContext) { + server.app.bsky.unspecced.getPopularFeedGenerators({ + auth: ctx.accessVerifier, + handler: async ({ auth, params }) => { + const requester = auth.credentials.did + const res = + await ctx.appviewAgent.api.app.bsky.unspecced.getPopularFeedGenerators( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/unspecced/index.ts b/packages/pds/src/api/app/bsky/unspecced/index.ts new file mode 100644 index 00000000000..6951400863d --- /dev/null +++ b/packages/pds/src/api/app/bsky/unspecced/index.ts @@ -0,0 +1,10 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import getPopular from './getPopular' +import getPopularFeedGenerators from './getPopularFeedGenerators' + +// THIS IS A TEMPORARY UNSPECCED ROUTE +export default function (server: Server, ctx: AppContext) { + getPopular(server, ctx) + getPopularFeedGenerators(server, ctx) +} diff --git a/packages/pds/src/app-view/api/app/bsky/util/read-after-write.ts b/packages/pds/src/api/app/bsky/util/read-after-write.ts similarity index 91% rename from packages/pds/src/app-view/api/app/bsky/util/read-after-write.ts rename to packages/pds/src/api/app/bsky/util/read-after-write.ts index b09a30cd2a1..b834a91c1b7 100644 --- a/packages/pds/src/app-view/api/app/bsky/util/read-after-write.ts +++ b/packages/pds/src/api/app/bsky/util/read-after-write.ts @@ -1,7 +1,7 @@ import { Headers } from '@atproto/xrpc' -import { readStickyLogger as log } from '../../../../../logger' -import { LocalRecords } from '../../../../../services/local' -import AppContext from '../../../../../context' +import { readStickyLogger as log } from '../../../../logger' +import { LocalRecords } from '../../../../services/local' +import AppContext from '../../../../context' export type ApiRes = { headers: Headers diff --git a/packages/pds/src/app-view/api/app/bsky/util/resolver.ts b/packages/pds/src/api/app/bsky/util/resolver.ts similarity index 93% rename from packages/pds/src/app-view/api/app/bsky/util/resolver.ts rename to packages/pds/src/api/app/bsky/util/resolver.ts index 9be31998439..eac4f916424 100644 --- a/packages/pds/src/app-view/api/app/bsky/util/resolver.ts +++ b/packages/pds/src/api/app/bsky/util/resolver.ts @@ -1,5 +1,5 @@ import { DidDocument, PoorlyFormattedDidDocumentError } from '@atproto/identity' -import AppContext from '../../../../../context' +import AppContext from '../../../../context' import { InvalidRequestError } from '@atproto/xrpc-server' // provides http-friendly errors during did resolution 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 69cf8432224..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/index.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' -import getTimeline from './feed/getTimeline' -import getActorFeeds from './feed/getActorFeeds' -import getSuggestedFeeds from './feed/getSuggestedFeeds' -import getAuthorFeed from './feed/getAuthorFeed' -import getFeedGenerator from './feed/getFeedGenerator' -import getFeedGenerators from './feed/getFeedGenerators' -import getFeed from './feed/getFeed' -import getLikes from './feed/getLikes' -import getListFeed from './feed/getListFeed' -import getPostThread from './feed/getPostThread' -import getPosts from './feed/getPosts' -import getActorLikes from './feed/getActorLikes' -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 getListBlocks from './graph/getListBlocks' -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 getSuggestedFollowsByActor from './graph/getSuggestedFollowsByActor' -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 registerPush from './notification/registerPush' -import unspecced from './unspecced' - -export default function (server: Server, ctx: AppContext) { - getTimeline(server, ctx) - getActorFeeds(server, ctx) - getSuggestedFeeds(server, ctx) - getAuthorFeed(server, ctx) - getFeedGenerator(server, ctx) - getFeedGenerators(server, ctx) - getFeed(server, ctx) - getLikes(server, ctx) - getListFeed(server, ctx) - getPostThread(server, ctx) - getPosts(server, ctx) - getActorLikes(server, ctx) - getProfile(server, ctx) - getProfiles(server, ctx) - getRepostedBy(server, ctx) - getBlocks(server, ctx) - getFollowers(server, ctx) - getFollows(server, ctx) - getList(server, ctx) - getListBlocks(server, ctx) - getListMutes(server, ctx) - getLists(server, ctx) - getMutes(server, ctx) - muteActor(server, ctx) - muteActorList(server, ctx) - unmuteActor(server, ctx) - unmuteActorList(server, ctx) - getSuggestedFollowsByActor(server, ctx) - getUsersSearch(server, ctx) - getUsersTypeahead(server, ctx) - getSuggestions(server, ctx) - listNotifications(server, ctx) - getUnreadCount(server, ctx) - updateSeen(server, ctx) - registerPush(server, ctx) - unspecced(server, ctx) -} 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 8708b8fe92f..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/unspecced.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../lexicon' -import { GenericKeyset } from '../../../../db/pagination' -import AppContext from '../../../../context' - -// THIS IS A TEMPORARY UNSPECCED ROUTE -export default function (server: Server, ctx: AppContext) { - server.app.bsky.unspecced.getPopular({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const HOT_CLASSIC_URI = - 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/hot-classic' - const HOT_CLASSIC_DID = 'did:plc:5fllqkujj6kqp5izd5jg7gox' - const res = await ctx.appviewAgent.api.app.bsky.feed.getFeed( - { feed: HOT_CLASSIC_URI, limit: params.limit, cursor: params.cursor }, - await ctx.serviceAuthHeaders(requester, HOT_CLASSIC_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 ctx.appviewAgent.api.app.bsky.unspecced.getPopularFeedGenerators( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) -} - -type Result = { likeCount: number; cid: string } -type LabeledResult = { primary: number; secondary: string } -export class LikeCountKeyset extends GenericKeyset { - labelResult(result: Result) { - return { - primary: result.likeCount, - secondary: result.cid, - } - } - labeledResultToCursor(labeled: LabeledResult) { - return { - primary: labeled.primary.toString(), - secondary: labeled.secondary, - } - } - cursorToLabeledResult(cursor: { primary: string; secondary: string }) { - const likes = parseInt(cursor.primary, 10) - if (isNaN(likes)) { - throw new InvalidRequestError('Malformed cursor') - } - return { - primary: likes, - secondary: cursor.secondary, - } - } -} 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/logger.ts b/packages/pds/src/app-view/logger.ts deleted file mode 100644 index 57bca80b1e3..00000000000 --- a/packages/pds/src/app-view/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { subsystemLogger } from '@atproto/common' - -export const appViewLogger = subsystemLogger('app-view') diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index fb08ef375e9..687ec9699af 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -21,7 +21,6 @@ import { Options as XrpcServerOptions, } from '@atproto/xrpc-server' import { DAY, HOUR, MINUTE } from '@atproto/common' -import inProcessAppView from './app-view/api' import API from './api' import * as basicRoutes from './basic-routes' import * as wellKnown from './well-known' @@ -219,7 +218,6 @@ export class PDS { let server = createServer(xrpcOpts) server = API(server, ctx) - server = inProcessAppView(server, ctx) app.use(basicRoutes.createRouter(ctx)) app.use(wellKnown.createRouter(ctx)) From 35b616cd82232879937afc88d3f77d20c6395276 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 26 Sep 2023 11:53:12 -0500 Subject: [PATCH 43/46] Strip leading `#` from from detected tag facets (#1674) ensure # is removed from facets --- .changeset/wet-mayflies-turn.md | 5 +++ packages/api/src/rich-text/detection.ts | 2 +- .../api/tests/rich-text-detection.test.ts | 34 +++++++++---------- 3 files changed, 23 insertions(+), 18 deletions(-) create mode 100644 .changeset/wet-mayflies-turn.md diff --git a/.changeset/wet-mayflies-turn.md b/.changeset/wet-mayflies-turn.md new file mode 100644 index 00000000000..26d00d014d1 --- /dev/null +++ b/.changeset/wet-mayflies-turn.md @@ -0,0 +1,5 @@ +--- +'@atproto/api': patch +--- + +Strip leading `#` from from detected tag facets diff --git a/packages/api/src/rich-text/detection.ts b/packages/api/src/rich-text/detection.ts index 503866d7df8..58a08e861e9 100644 --- a/packages/api/src/rich-text/detection.ts +++ b/packages/api/src/rich-text/detection.ts @@ -90,7 +90,7 @@ export function detectFacets(text: UnicodeString): Facet[] | undefined { features: [ { $type: 'app.bsky.richtext.facet#tag', - tag, + tag: tag.replace(/^#/, ''), }, ], }) diff --git a/packages/api/tests/rich-text-detection.test.ts b/packages/api/tests/rich-text-detection.test.ts index df2aed84889..52b10acb7d6 100644 --- a/packages/api/tests/rich-text-detection.test.ts +++ b/packages/api/tests/rich-text-detection.test.ts @@ -216,28 +216,28 @@ describe('detectFacets', () => { string[], { byteStart: number; byteEnd: number }[], ][] = [ - ['#a', ['#a'], [{ byteStart: 0, byteEnd: 2 }]], + ['#a', ['a'], [{ byteStart: 0, byteEnd: 2 }]], [ '#a #b', - ['#a', '#b'], + ['a', 'b'], [ { byteStart: 0, byteEnd: 2 }, { byteStart: 3, byteEnd: 5 }, ], ], ['#1', [], []], - ['#tag', ['#tag'], [{ byteStart: 0, byteEnd: 4 }]], - ['body #tag', ['#tag'], [{ byteStart: 5, byteEnd: 9 }]], - ['#tag body', ['#tag'], [{ byteStart: 0, byteEnd: 4 }]], - ['body #tag body', ['#tag'], [{ byteStart: 5, byteEnd: 9 }]], + ['#tag', ['tag'], [{ byteStart: 0, byteEnd: 4 }]], + ['body #tag', ['tag'], [{ byteStart: 5, byteEnd: 9 }]], + ['#tag body', ['tag'], [{ byteStart: 0, byteEnd: 4 }]], + ['body #tag body', ['tag'], [{ byteStart: 5, byteEnd: 9 }]], ['body #1', [], []], - ['body #a1', ['#a1'], [{ byteStart: 5, byteEnd: 8 }]], + ['body #a1', ['a1'], [{ byteStart: 5, byteEnd: 8 }]], ['#', [], []], ['text #', [], []], ['text # text', [], []], [ 'body #thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - ['#thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'], + ['thisisa64characterstring_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'], [{ byteStart: 5, byteEnd: 71 }], ], [ @@ -247,19 +247,19 @@ describe('detectFacets', () => { ], [ 'its a #double#rainbow', - ['#double#rainbow'], + ['double#rainbow'], [{ byteStart: 6, byteEnd: 21 }], ], - ['##hashash', ['##hashash'], [{ byteStart: 0, byteEnd: 9 }]], - ['some #n0n3s@n5e!', ['#n0n3s@n5e'], [{ byteStart: 5, byteEnd: 15 }]], + ['##hashash', ['#hashash'], [{ byteStart: 0, byteEnd: 9 }]], + ['some #n0n3s@n5e!', ['n0n3s@n5e'], [{ byteStart: 5, byteEnd: 15 }]], [ 'works #with,punctuation', - ['#with,punctuation'], + ['with,punctuation'], [{ byteStart: 6, byteEnd: 23 }], ], [ 'strips trailing #punctuation, #like. #this!', - ['#punctuation', '#like', '#this'], + ['punctuation', 'like', 'this'], [ { byteStart: 16, byteEnd: 28 }, { byteStart: 30, byteEnd: 35 }, @@ -268,12 +268,12 @@ describe('detectFacets', () => { ], [ 'strips #multi_trailing___...', - ['#multi_trailing'], + ['multi_trailing'], [{ byteStart: 7, byteEnd: 22 }], ], [ 'works with #🦋 emoji, and #butter🦋fly', - ['#🦋', '#butter🦋fly'], + ['🦋', 'butter🦋fly'], [ { byteStart: 11, byteEnd: 16 }, { byteStart: 28, byteEnd: 42 }, @@ -281,7 +281,7 @@ describe('detectFacets', () => { ], [ '#same #same #but #diff', - ['#same', '#same', '#but', '#diff'], + ['same', 'same', 'but', 'diff'], [ { byteStart: 0, byteEnd: 5 }, { byteStart: 6, byteEnd: 11 }, @@ -298,7 +298,7 @@ describe('detectFacets', () => { let detectedTags: string[] = [] let detectedIndices: { byteStart: number; byteEnd: number }[] = [] - for (const { facet } of rt.segments()) { + for (const { facet, ...rest } of rt.segments()) { if (!facet) continue for (const feature of facet.features) { if (isTag(feature)) { From 7dacb9d6f5afd969fd851698bb068ea726a7e43d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 26 Sep 2023 11:56:02 -0500 Subject: [PATCH 44/46] Version packages (#1675) Co-authored-by: github-actions[bot] --- .changeset/wet-mayflies-turn.md | 5 ----- packages/api/CHANGELOG.md | 6 ++++++ packages/api/package.json | 2 +- packages/bsky/CHANGELOG.md | 7 +++++++ packages/bsky/package.json | 2 +- packages/dev-env/CHANGELOG.md | 9 +++++++++ packages/dev-env/package.json | 2 +- packages/pds/CHANGELOG.md | 7 +++++++ packages/pds/package.json | 2 +- 9 files changed, 33 insertions(+), 9 deletions(-) delete mode 100644 .changeset/wet-mayflies-turn.md diff --git a/.changeset/wet-mayflies-turn.md b/.changeset/wet-mayflies-turn.md deleted file mode 100644 index 26d00d014d1..00000000000 --- a/.changeset/wet-mayflies-turn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@atproto/api': patch ---- - -Strip leading `#` from from detected tag facets diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index 1d6d7788839..35fa620859e 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -1,5 +1,11 @@ # @atproto/api +## 0.6.19 + +### Patch Changes + +- [#1674](https://github.com/bluesky-social/atproto/pull/1674) [`35b616cd`](https://github.com/bluesky-social/atproto/commit/35b616cd82232879937afc88d3f77d20c6395276) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Strip leading `#` from from detected tag facets + ## 0.6.18 ### Patch Changes diff --git a/packages/api/package.json b/packages/api/package.json index 13624588c0c..d40f0dfd8c8 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.6.18", + "version": "0.6.19", "license": "MIT", "description": "Client library for atproto and Bluesky", "keywords": [ diff --git a/packages/bsky/CHANGELOG.md b/packages/bsky/CHANGELOG.md index 9d85cfa4a70..f797a848ce9 100644 --- a/packages/bsky/CHANGELOG.md +++ b/packages/bsky/CHANGELOG.md @@ -1,5 +1,12 @@ # @atproto/bsky +## 0.0.10 + +### Patch Changes + +- Updated dependencies [[`35b616cd`](https://github.com/bluesky-social/atproto/commit/35b616cd82232879937afc88d3f77d20c6395276)]: + - @atproto/api@0.6.19 + ## 0.0.9 ### Patch Changes diff --git a/packages/bsky/package.json b/packages/bsky/package.json index 5281805279f..1ea154701b5 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/bsky", - "version": "0.0.9", + "version": "0.0.10", "license": "MIT", "description": "Reference implementation of app.bsky App View (Bluesky API)", "keywords": [ diff --git a/packages/dev-env/CHANGELOG.md b/packages/dev-env/CHANGELOG.md index 67523be96e0..1bc9601f4c9 100644 --- a/packages/dev-env/CHANGELOG.md +++ b/packages/dev-env/CHANGELOG.md @@ -1,5 +1,14 @@ # @atproto/dev-env +## 0.2.10 + +### Patch Changes + +- Updated dependencies [[`35b616cd`](https://github.com/bluesky-social/atproto/commit/35b616cd82232879937afc88d3f77d20c6395276)]: + - @atproto/api@0.6.19 + - @atproto/bsky@0.0.10 + - @atproto/pds@0.1.19 + ## 0.2.9 ### Patch Changes diff --git a/packages/dev-env/package.json b/packages/dev-env/package.json index 6ae8cc5d151..dde8dfb3e38 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/dev-env", - "version": "0.2.9", + "version": "0.2.10", "license": "MIT", "description": "Local development environment helper for atproto development", "keywords": [ diff --git a/packages/pds/CHANGELOG.md b/packages/pds/CHANGELOG.md index 2680326d054..88c86c867d1 100644 --- a/packages/pds/CHANGELOG.md +++ b/packages/pds/CHANGELOG.md @@ -1,5 +1,12 @@ # @atproto/pds +## 0.1.19 + +### Patch Changes + +- Updated dependencies [[`35b616cd`](https://github.com/bluesky-social/atproto/commit/35b616cd82232879937afc88d3f77d20c6395276)]: + - @atproto/api@0.6.19 + ## 0.1.18 ### Patch Changes diff --git a/packages/pds/package.json b/packages/pds/package.json index a7d98578f35..b439e1c72c5 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/pds", - "version": "0.1.18", + "version": "0.1.19", "license": "MIT", "description": "Reference implementation of atproto Personal Data Server (PDS)", "keywords": [ From aa2bc5a4cc9395e7739cab9657b09c5f84f13a96 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Tue, 26 Sep 2023 13:52:11 -0500 Subject: [PATCH 45/46] Proxy search queries (#1676) * proxy search * tweak profile resp * fix admin.searchRepos --- .../src/api/app/bsky/actor/searchActors.ts | 57 ++++++++++++------- .../app/bsky/actor/searchActorsTypeahead.ts | 40 +++++++------ .../src/api/com/atproto/admin/searchRepos.ts | 13 ++--- packages/bsky/src/config.ts | 7 +++ packages/bsky/src/context.ts | 6 ++ packages/bsky/src/index.ts | 5 ++ packages/bsky/src/services/actor/index.ts | 18 +++--- packages/bsky/src/services/util/search.ts | 47 +++++++-------- 8 files changed, 114 insertions(+), 79 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/actor/searchActors.ts b/packages/bsky/src/api/app/bsky/actor/searchActors.ts index d4ae0a8d264..4fcb29de089 100644 --- a/packages/bsky/src/api/app/bsky/actor/searchActors.ts +++ b/packages/bsky/src/api/app/bsky/actor/searchActors.ts @@ -2,7 +2,7 @@ import { sql } from 'kysely' import AppContext from '../../../../context' import { Server } from '../../../../lexicon' import { - cleanTerm, + cleanQuery, getUserSearchQuery, SearchKeyset, } from '../../../../services/util/search' @@ -11,37 +11,50 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.searchActors({ auth: ctx.authOptionalVerifier, handler: async ({ auth, params }) => { - let { cursor, limit, term: rawTerm, q: rawQ } = params + const { cursor, limit } = params const requester = auth.credentials.did - - // prefer new 'q' query param over deprecated 'term' - if (rawQ) { - rawTerm = rawQ - } - - const term = cleanTerm(rawTerm || '') - + const rawQuery = params.q ?? params.term + const query = cleanQuery(rawQuery || '') const db = ctx.db.getReplica('search') - const results = term - ? await getUserSearchQuery(db, { term, limit, cursor }) - .select('distance') - .selectAll('actor') - .execute() - : [] - const keyset = new SearchKeyset(sql``, sql``) + let results: string[] + let resCursor: string | undefined + if (ctx.searchAgent) { + const res = + await ctx.searchAgent.api.app.bsky.unspecced.searchActorsSkeleton({ + q: query, + cursor, + limit, + }) + results = res.data.actors.map((a) => a.did) + resCursor = res.data.cursor + } else { + const res = query + ? await getUserSearchQuery(db, { query, limit, cursor }) + .select('distance') + .selectAll('actor') + .execute() + : [] + results = res.map((a) => a.did) + const keyset = new SearchKeyset(sql``, sql``) + resCursor = keyset.packFromResult(res) + } const actors = await ctx.services .actor(db) - .views.profilesList(results, requester) - const filtered = actors.filter( - (actor) => !actor.viewer?.blocking && !actor.viewer?.blockedBy, - ) + .views.profiles(results, requester) + + const SKIP = [] + const filtered = results.flatMap((did) => { + const actor = actors[did] + if (actor.viewer?.blocking || actor.viewer?.blockedBy) return SKIP + return actor + }) return { encoding: 'application/json', body: { - cursor: keyset.packFromResult(results), + cursor: resCursor, actors: filtered, }, } diff --git a/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts b/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts index c438c4d2324..9c09a54ac7e 100644 --- a/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts @@ -1,7 +1,7 @@ import AppContext from '../../../../context' import { Server } from '../../../../lexicon' import { - cleanTerm, + cleanQuery, getUserSearchQuerySimple, } from '../../../../services/util/search' @@ -9,31 +9,37 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.searchActorsTypeahead({ auth: ctx.authOptionalVerifier, handler: async ({ params, auth }) => { - let { limit, term: rawTerm, q: rawQ } = params + const { limit } = params const requester = auth.credentials.did - - // prefer new 'q' query param over deprecated 'term' - if (rawQ) { - rawTerm = rawQ - } - - const term = cleanTerm(rawTerm || '') - + const rawQuery = params.q ?? params.term + const query = cleanQuery(rawQuery || '') const db = ctx.db.getReplica('search') - const results = term - ? await getUserSearchQuerySimple(db, { term, limit }) - .selectAll('actor') - .execute() - : [] + let results: string[] + if (ctx.searchAgent) { + const res = + await ctx.searchAgent.api.app.bsky.unspecced.searchActorsSkeleton({ + q: query, + typeahead: true, + limit, + }) + results = res.data.actors.map((a) => a.did) + } else { + const res = query + ? await getUserSearchQuerySimple(db, { query, limit }) + .selectAll('actor') + .execute() + : [] + results = res.map((a) => a.did) + } const actors = await ctx.services .actor(db) .views.profilesBasic(results, requester, { omitLabels: true }) const SKIP = [] - const filtered = results.flatMap((res) => { - const actor = actors[res.did] + const filtered = results.flatMap((did) => { + const actor = actors[did] if (actor.viewer?.blocking || actor.viewer?.blockedBy) return SKIP return actor }) diff --git a/packages/bsky/src/api/com/atproto/admin/searchRepos.ts b/packages/bsky/src/api/com/atproto/admin/searchRepos.ts index a17421e90cd..8faf041f589 100644 --- a/packages/bsky/src/api/com/atproto/admin/searchRepos.ts +++ b/packages/bsky/src/api/com/atproto/admin/searchRepos.ts @@ -8,23 +8,20 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params }) => { const db = ctx.db.getPrimary() const moderationService = ctx.services.moderation(db) - const { invitedBy } = params + const { invitedBy, limit, cursor } = params if (invitedBy) { throw new InvalidRequestError('The invitedBy parameter is unsupported') } // prefer new 'q' query param over deprecated 'term' - const { q } = params - if (q) { - params.term = q - } + const query = params.q ?? params.term - const { results, cursor } = await ctx.services + const { results, cursor: resCursor } = await ctx.services .actor(db) - .getSearchResults({ ...params, includeSoftDeleted: true }) + .getSearchResults({ query, limit, cursor, includeSoftDeleted: true }) return { encoding: 'application/json', body: { - cursor, + cursor: resCursor, repos: await moderationService.views.repo(results), }, } diff --git a/packages/bsky/src/config.ts b/packages/bsky/src/config.ts index 2ef7e1edf6c..38d9c883760 100644 --- a/packages/bsky/src/config.ts +++ b/packages/bsky/src/config.ts @@ -18,6 +18,7 @@ export interface ServerConfigValues { handleResolveNameservers?: string[] imgUriEndpoint?: string blobCacheLocation?: string + searchEndpoint?: string labelerDid: string adminPassword: string moderatorPassword?: string @@ -51,6 +52,7 @@ export class ServerConfig { : [] const imgUriEndpoint = process.env.IMG_URI_ENDPOINT const blobCacheLocation = process.env.BLOB_CACHE_LOC + const searchEndpoint = process.env.SEARCH_ENDPOINT const dbPrimaryPostgresUrl = overrides?.dbPrimaryPostgresUrl || process.env.DB_PRIMARY_POSTGRES_URL let dbReplicaPostgresUrls = overrides?.dbReplicaPostgresUrls @@ -97,6 +99,7 @@ export class ServerConfig { handleResolveNameservers, imgUriEndpoint, blobCacheLocation, + searchEndpoint, labelerDid, adminPassword, moderatorPassword, @@ -183,6 +186,10 @@ export class ServerConfig { return this.cfg.blobCacheLocation } + get searchEndpoint() { + return this.cfg.searchEndpoint + } + get labelerDid() { return this.cfg.labelerDid } diff --git a/packages/bsky/src/context.ts b/packages/bsky/src/context.ts index 343d105ce1a..42cbfecf218 100644 --- a/packages/bsky/src/context.ts +++ b/packages/bsky/src/context.ts @@ -10,6 +10,7 @@ import { BackgroundQueue } from './background' import { MountedAlgos } from './feed-gen/types' import { LabelCache } from './label-cache' import { NotificationServer } from './notifications' +import { AtpAgent } from '@atproto/api' export class AppContext { constructor( @@ -22,6 +23,7 @@ export class AppContext { didCache: DidSqlCache labelCache: LabelCache backgroundQueue: BackgroundQueue + searchAgent?: AtpAgent algos: MountedAlgos notifServer: NotificationServer }, @@ -63,6 +65,10 @@ export class AppContext { return this.opts.notifServer } + get searchAgent(): AtpAgent | undefined { + return this.opts.searchAgent + } + get authVerifier() { return auth.authVerifier(this.idResolver, { aud: this.cfg.serverDid }) } diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index b469c2f7b17..8ef2109218e 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -25,6 +25,7 @@ import { BackgroundQueue } from './background' import { MountedAlgos } from './feed-gen/types' import { LabelCache } from './label-cache' import { NotificationServer } from './notifications' +import { AtpAgent } from '@atproto/api' export type { ServerConfigValues } from './config' export type { MountedAlgos } from './feed-gen/types' @@ -100,6 +101,9 @@ export class BskyAppView { const backgroundQueue = new BackgroundQueue(db.getPrimary()) const labelCache = new LabelCache(db.getPrimary()) const notifServer = new NotificationServer(db.getPrimary()) + const searchAgent = config.searchEndpoint + ? new AtpAgent({ service: config.searchEndpoint }) + : undefined const services = createServices({ imgUriBuilder, @@ -116,6 +120,7 @@ export class BskyAppView { didCache, labelCache, backgroundQueue, + searchAgent, algos, notifServer, }) diff --git a/packages/bsky/src/services/actor/index.ts b/packages/bsky/src/services/actor/index.ts index 54bfb714146..0f90e550f3c 100644 --- a/packages/bsky/src/services/actor/index.ts +++ b/packages/bsky/src/services/actor/index.ts @@ -83,15 +83,15 @@ export class ActorService { async getSearchResults({ cursor, limit = 25, - term = '', + query = '', includeSoftDeleted, }: { cursor?: string limit?: number - term?: string + query?: string includeSoftDeleted?: boolean }) { - const searchField = term.startsWith('did:') ? 'did' : 'handle' + const searchField = query.startsWith('did:') ? 'did' : 'handle' let paginatedBuilder const { ref } = this.db.db.dynamic const paginationOptions = { @@ -101,10 +101,10 @@ export class ActorService { } let keyset - if (term && searchField === 'handle') { + if (query && searchField === 'handle') { keyset = new SearchKeyset(sql``, sql``) paginatedBuilder = getUserSearchQuery(this.db, { - term, + query, includeSoftDeleted, ...paginationOptions, }).select('distance') @@ -114,10 +114,10 @@ export class ActorService { .select([sql`0`.as('distance')]) keyset = new ListKeyset(ref('indexedAt'), ref('did')) - // When searchField === 'did', the term will always be a valid string because - // searchField is set to 'did' after checking that the term is a valid did - if (term && searchField === 'did') { - paginatedBuilder = paginatedBuilder.where('actor.did', '=', term) + // When searchField === 'did', the query will always be a valid string because + // searchField is set to 'did' after checking that the query is a valid did + if (query && searchField === 'did') { + paginatedBuilder = paginatedBuilder.where('actor.did', '=', query) } paginatedBuilder = paginate(paginatedBuilder, { keyset, diff --git a/packages/bsky/src/services/util/search.ts b/packages/bsky/src/services/util/search.ts index 9dfebadb613..994d2f43879 100644 --- a/packages/bsky/src/services/util/search.ts +++ b/packages/bsky/src/services/util/search.ts @@ -7,17 +7,17 @@ import { GenericKeyset, paginate } from '../../db/pagination' export const getUserSearchQuery = ( db: Database, opts: { - term: string + query: string limit: number cursor?: string includeSoftDeleted?: boolean }, ) => { const { ref } = db.db.dynamic - const { term, limit, cursor, includeSoftDeleted } = opts + const { query, limit, cursor, includeSoftDeleted } = opts // Matching user accounts based on handle - const distanceAccount = distance(term, ref('handle')) - let accountsQb = getMatchingAccountsQb(db, { term, includeSoftDeleted }) + const distanceAccount = distance(query, ref('handle')) + let accountsQb = getMatchingAccountsQb(db, { query, includeSoftDeleted }) accountsQb = paginate(accountsQb, { limit, cursor, @@ -25,8 +25,8 @@ export const getUserSearchQuery = ( keyset: new SearchKeyset(distanceAccount, ref('actor.did')), }) // Matching profiles based on display name - const distanceProfile = distance(term, ref('displayName')) - let profilesQb = getMatchingProfilesQb(db, { term, includeSoftDeleted }) + const distanceProfile = distance(query, ref('displayName')) + let profilesQb = getMatchingProfilesQb(db, { query, includeSoftDeleted }) profilesQb = paginate(profilesQb, { limit, cursor, @@ -46,18 +46,18 @@ export const getUserSearchQuery = ( export const getUserSearchQuerySimple = ( db: Database, opts: { - term: string + query: string limit: number }, ) => { const { ref } = db.db.dynamic - const { term, limit } = opts + const { query, limit } = opts // Matching user accounts based on handle - const accountsQb = getMatchingAccountsQb(db, { term }) + const accountsQb = getMatchingAccountsQb(db, { query }) .orderBy('distance', 'asc') .limit(limit) // Matching profiles based on display name - const profilesQb = getMatchingProfilesQb(db, { term }) + const profilesQb = getMatchingProfilesQb(db, { query }) .orderBy('distance', 'asc') .limit(limit) // Combine and paginate result set @@ -71,29 +71,29 @@ export const getUserSearchQuerySimple = ( // Matching user accounts based on handle const getMatchingAccountsQb = ( db: Database, - opts: { term: string; includeSoftDeleted?: boolean }, + opts: { query: string; includeSoftDeleted?: boolean }, ) => { const { ref } = db.db.dynamic - const { term, includeSoftDeleted } = opts - const distanceAccount = distance(term, ref('handle')) + const { query, includeSoftDeleted } = opts + const distanceAccount = distance(query, ref('handle')) return db.db .selectFrom('actor') .if(!includeSoftDeleted, (qb) => qb.where(notSoftDeletedClause(ref('actor'))), ) .where('actor.handle', 'is not', null) - .where(similar(term, ref('handle'))) // Coarse filter engaging trigram index + .where(similar(query, ref('handle'))) // Coarse filter engaging trigram index .select(['actor.did as did', distanceAccount.as('distance')]) } // Matching profiles based on display name const getMatchingProfilesQb = ( db: Database, - opts: { term: string; includeSoftDeleted?: boolean }, + opts: { query: string; includeSoftDeleted?: boolean }, ) => { const { ref } = db.db.dynamic - const { term, includeSoftDeleted } = opts - const distanceProfile = distance(term, ref('displayName')) + const { query, includeSoftDeleted } = opts + const distanceProfile = distance(query, ref('displayName')) return db.db .selectFrom('profile') .innerJoin('actor', 'actor.did', 'profile.creator') @@ -101,7 +101,7 @@ const getMatchingProfilesQb = ( qb.where(notSoftDeletedClause(ref('actor'))), ) .where('actor.handle', 'is not', null) - .where(similar(term, ref('displayName'))) // Coarse filter engaging trigram index + .where(similar(query, ref('displayName'))) // Coarse filter engaging trigram index .select(['profile.creator as did', distanceProfile.as('distance')]) } @@ -133,15 +133,16 @@ const combineAccountsAndProfilesQb = ( } // Remove leading @ in case a handle is input that way -export const cleanTerm = (term: string) => term.trim().replace(/^@/g, '') +export const cleanQuery = (query: string) => query.trim().replace(/^@/g, '') -// Uses pg_trgm strict word similarity to check similarity between a search term and a stored value -const distance = (term: string, ref: DbRef) => - sql`(${term} <<-> ${ref})` +// Uses pg_trgm strict word similarity to check similarity between a search query and a stored value +const distance = (query: string, ref: DbRef) => + sql`(${query} <<-> ${ref})` // Can utilize trigram index to match on strict word similarity. // The word_similarity_threshold is set to .4 (i.e. distance < .6) in db/index.ts. -const similar = (term: string, ref: DbRef) => sql`(${term} <% ${ref})` +const similar = (query: string, ref: DbRef) => + sql`(${query} <% ${ref})` type Result = { distance: number; did: string } type LabeledResult = { primary: number; secondary: string } From 62711985e73ca21c7a2f91a1dfafadfd55022268 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Tue, 26 Sep 2023 15:27:41 -0500 Subject: [PATCH 46/46] Proxy search bugfix (#1679) proxy search bugfix --- packages/bsky/src/api/app/bsky/actor/searchActors.ts | 1 + packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/bsky/src/api/app/bsky/actor/searchActors.ts b/packages/bsky/src/api/app/bsky/actor/searchActors.ts index 4fcb29de089..66e934ac0b3 100644 --- a/packages/bsky/src/api/app/bsky/actor/searchActors.ts +++ b/packages/bsky/src/api/app/bsky/actor/searchActors.ts @@ -47,6 +47,7 @@ export default function (server: Server, ctx: AppContext) { const SKIP = [] const filtered = results.flatMap((did) => { const actor = actors[did] + if (!actor) return SKIP if (actor.viewer?.blocking || actor.viewer?.blockedBy) return SKIP return actor }) diff --git a/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts b/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts index 9c09a54ac7e..ff674edf898 100644 --- a/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts @@ -40,6 +40,7 @@ export default function (server: Server, ctx: AppContext) { const SKIP = [] const filtered = results.flatMap((did) => { const actor = actors[did] + if (!actor) return SKIP if (actor.viewer?.blocking || actor.viewer?.blockedBy) return SKIP return actor })