From e71a5e31edc701eec806e7e090baee4dccc7fc7b Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 12 Jun 2023 17:36:24 -0500 Subject: [PATCH 001/105] rm tables --- packages/pds/src/app-view/db/index.ts | 40 ------------------- .../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 -------------- .../src/app-view/db/tables/post-hierarchy.ts | 10 ----- 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 ------ 20 files changed, 301 deletions(-) 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-hierarchy.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 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 e0eefa54edd..00000000000 --- a/packages/pds/src/app-view/db/index.ts +++ /dev/null @@ -1,40 +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 postHierarchy from './tables/post-hierarchy' -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 & - postHierarchy.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-hierarchy.ts b/packages/pds/src/app-view/db/tables/post-hierarchy.ts deleted file mode 100644 index 70c44f67ce4..00000000000 --- a/packages/pds/src/app-view/db/tables/post-hierarchy.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const tableName = 'post_hierarchy' -export interface PostHierarchy { - uri: string - ancestorUri: string - depth: number -} - -export type PartialDB = { - [tableName]: PostHierarchy -} 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 } From a141d44e90f20f638238e82628ffc6f9f192813d Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 12 Jun 2023 17:39:54 -0500 Subject: [PATCH 002/105] rm event-stream & proxied --- .../src/app-view/event-stream/consumers.ts | 38 -- packages/pds/src/app-view/proxied/index.ts | 605 ------------------ 2 files changed, 643 deletions(-) delete mode 100644 packages/pds/src/app-view/event-stream/consumers.ts delete mode 100644 packages/pds/src/app-view/proxied/index.ts diff --git a/packages/pds/src/app-view/event-stream/consumers.ts b/packages/pds/src/app-view/event-stream/consumers.ts deleted file mode 100644 index 48fc963fb09..00000000000 --- a/packages/pds/src/app-view/event-stream/consumers.ts +++ /dev/null @@ -1,38 +0,0 @@ -import AppContext from '../../context' -import Database from '../../db' -import { - DeleteRecord, - IndexRecord, - DeleteRepo, -} from '../../event-stream/messages' - -// Used w/ in-process PDS as alternative to the repo subscription -export const listen = (ctx: AppContext) => { - ctx.messageDispatcher.listen('index_record', { - async listener(input: { db: Database; message: IndexRecord }) { - const { db, message } = input - const indexingService = ctx.services.appView.indexing(db) - await indexingService.indexRecord( - message.uri, - message.cid, - message.obj, - message.action, - message.timestamp, - ) - }, - }) - ctx.messageDispatcher.listen('delete_record', { - async listener(input: { db: Database; message: DeleteRecord }) { - const { db, message } = input - const indexingService = ctx.services.appView.indexing(db) - await indexingService.deleteRecord(message.uri, message.cascading) - }, - }) - ctx.messageDispatcher.listen('delete_repo', { - async listener(input: { db: Database; message: DeleteRepo }) { - const { db, message } = input - const indexingService = ctx.services.appView.indexing(db) - await indexingService.deleteForUser(message.did) - }, - }) -} diff --git a/packages/pds/src/app-view/proxied/index.ts b/packages/pds/src/app-view/proxied/index.ts deleted file mode 100644 index abd343a5029..00000000000 --- a/packages/pds/src/app-view/proxied/index.ts +++ /dev/null @@ -1,605 +0,0 @@ -import { AtpAgent } from '@atproto/api' -import { dedupeStrs } from '@atproto/common' -import { - InvalidRequestError, - createServiceAuthHeaders, -} from '@atproto/xrpc-server' -import { Server } from '../../lexicon' -import AppContext from '../../context' -import { - FeedViewPost, - ThreadViewPost, - isPostView, - isReasonRepost, - isThreadViewPost, -} from '../../lexicon/types/app/bsky/feed/defs' - -export default function (server: Server, ctx: AppContext) { - const appviewEndpoint = ctx.cfg.bskyAppViewEndpoint - const appviewDid = ctx.cfg.bskyAppViewDid - if (!appviewEndpoint) { - throw new Error('Could not find bsky appview endpoint') - } - if (!appviewDid) { - throw new Error('Could not find bsky appview did') - } - - const agent = new AtpAgent({ service: appviewEndpoint }) - - const headers = async (did: string, aud?: string) => { - return createServiceAuthHeaders({ - iss: did, - aud: aud ?? appviewDid, - keypair: ctx.repoSigningKey, - }) - } - - server.app.bsky.actor.getProfile({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.actor.getProfile( - params, - await headers(requester), - ) - const profile = res.data - const muted = await ctx.services - .account(ctx.db) - .getMute(requester, profile.did) - profile.viewer ??= {} - profile.viewer.muted = muted - return { - encoding: 'application/json', - body: profile, - } - }, - }) - - server.app.bsky.actor.getProfiles({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.actor.getProfiles( - params, - await headers(requester), - ) - const profiles = res.data.profiles - - const dids = profiles.map((profile) => profile.did) - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - - for (const profile of profiles) { - profile.viewer ??= {} - profile.viewer.muted = mutes[profile.did] ?? false - } - - return { - encoding: 'application/json', - body: { - profiles, - }, - } - }, - }) - - server.app.bsky.actor.getSuggestions({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.actor.getSuggestions( - params, - await headers(requester), - ) - const { cursor, actors } = res.data - const dids = actors.map((actor) => actor.did) - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - for (const actor of actors) { - actor.viewer ??= {} - actor.viewer.muted = mutes[actor.did] ?? false - } - - return { - encoding: 'application/json', - body: { - cursor, - actors, - }, - } - }, - }) - - server.app.bsky.actor.searchActors({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.actor.searchActors( - params, - await headers(requester), - ) - - const { cursor, actors } = res.data - const dids = actors.map((actor) => actor.did) - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - for (const actor of actors) { - actor.viewer ??= {} - actor.viewer.muted = mutes[actor.did] ?? false - } - - return { - encoding: 'application/json', - body: { - cursor, - actors, - }, - } - }, - }) - - server.app.bsky.actor.searchActorsTypeahead({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.actor.searchActorsTypeahead( - params, - await headers(requester), - ) - - const { actors } = res.data - const dids = actors.map((actor) => actor.did) - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - for (const actor of actors) { - actor.viewer ??= {} - actor.viewer.muted = mutes[actor.did] ?? false - } - - return { - encoding: 'application/json', - body: { actors }, - } - }, - }) - - server.app.bsky.feed.getAuthorFeed({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.feed.getAuthorFeed( - params, - await headers(requester), - ) - const { cursor, feed } = res.data - const dids = didsForFeedViewPosts(feed) - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - const hydrated = processFeedViewPostMutes(feed, mutes, (post) => { - // eliminate posts by a muted account that have been reposted by account of feed - return ( - post.reason !== undefined && - isReasonRepost(post.reason) && - mutes[post.post.author.did] === true - ) - }) - - return { - encoding: 'application/json', - body: { - cursor, - feed: hydrated, - }, - } - }, - }) - - server.app.bsky.feed.getLikes({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.feed.getLikes( - params, - await headers(requester), - ) - - const { cursor, uri, cid, likes } = res.data - const dids = likes.map((like) => like.actor.did) - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - - for (const like of likes) { - like.actor.viewer ??= {} - like.actor.viewer.muted = mutes[like.actor.did] ?? false - } - - return { - encoding: 'application/json', - body: { - cursor, - uri, - cid, - likes, - }, - } - }, - }) - - server.app.bsky.feed.getPostThread({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.feed.getPostThread( - params, - await headers(requester), - ) - const { thread } = res.data - if (!isThreadViewPost(thread)) { - return { - encoding: 'application/json', - body: { thread }, - } - } - - const dids = isThreadViewPost(thread) ? didsForThread(thread) : [] - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - const hydrated = processThreadViewMutes(thread, mutes) - - return { - encoding: 'application/json', - body: { - thread: hydrated, - }, - } - }, - }) - - server.app.bsky.feed.getPosts({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.feed.getPosts( - params, - await headers(requester), - ) - const { posts } = res.data - - const dids = posts.map((p) => p.author.did) - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - for (const post of posts) { - post.author.viewer ??= {} - post.author.viewer.muted = mutes[post.author.did] ?? false - } - - return { - encoding: 'application/json', - body: { - posts, - }, - } - }, - }) - - server.app.bsky.feed.getRepostedBy({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.feed.getRepostedBy( - params, - await headers(requester), - ) - - const { cursor, uri, cid, repostedBy } = res.data - const dids = repostedBy.map((repost) => repost.did) - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - - for (const repost of repostedBy) { - repost.viewer ??= {} - repost.viewer.muted = mutes[repost.did] ?? false - } - - return { - encoding: 'application/json', - body: { - cursor, - uri, - cid, - repostedBy, - }, - } - }, - }) - - server.app.bsky.feed.getTimeline({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.feed.getTimeline( - params, - await headers(requester), - ) - const { cursor, feed } = res.data - const dids = didsForFeedViewPosts(feed) - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - const hydrated = processFeedViewPostMutes(feed, mutes, (post) => { - // remove posts & reposts from muted accounts - return ( - mutes[post.post.author.did] === true || - (post.reason !== undefined && - isReasonRepost(post.reason) && - mutes[post.reason.by.did]) === true - ) - }) - - return { - encoding: 'application/json', - body: { - cursor, - feed: hydrated, - }, - } - }, - }) - - server.app.bsky.graph.getFollowers({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.graph.getFollowers( - params, - await headers(requester), - ) - - const { cursor, subject, followers } = res.data - const dids = [subject.did, ...followers.map((follower) => follower.did)] - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - - for (const follower of followers) { - follower.viewer ??= {} - follower.viewer.muted = mutes[follower.did] ?? false - } - subject.viewer ??= {} - subject.viewer.muted = mutes[subject.did] ?? false - - return { - encoding: 'application/json', - body: { - cursor, - subject, - followers, - }, - } - }, - }) - - server.app.bsky.graph.getFollows({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.graph.getFollows( - params, - await headers(requester), - ) - const { cursor, subject, follows } = res.data - const dids = [subject.did, ...follows.map((follow) => follow.did)] - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - - for (const follow of follows) { - follow.viewer ??= {} - follow.viewer.muted = mutes[follow.did] ?? false - } - subject.viewer ??= {} - subject.viewer.muted = mutes[subject.did] ?? false - - return { - encoding: 'application/json', - body: { - cursor, - subject, - follows, - }, - } - }, - }) - - // @NOTE currently relies on the hot-classic feed being configured on the pds - server.app.bsky.unspecced.getPopular({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const hotClassicUri = Object.keys(ctx.algos).find((uri) => - uri.endsWith('/hot-classic'), - ) - if (!hotClassicUri) { - return { - encoding: 'application/json', - body: { feed: [] }, - } - } - const requester = auth.credentials.did - // @TODO cache the feedgen did lookup - const { data: feed } = await agent.api.app.bsky.feed.getFeedGenerator( - { feed: hotClassicUri }, - await headers(requester), - ) - const res = await agent.api.app.bsky.feed.getFeed( - { ...params, feed: hotClassicUri }, - await headers(requester, feed.view.did), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.unspecced.getPopularFeedGenerators({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await agent.api.app.bsky.unspecced.getPopularFeedGenerators( - params, - await headers(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.notification.getUnreadCount({ - auth: ctx.accessVerifier, - handler: async ({ auth }) => { - const requester = auth.credentials.did - const seenAt = await ctx.services - .account(ctx.db) - .getLastSeenNotifs(requester) - const res = await agent.api.app.bsky.notification.getUnreadCount( - { seenAt }, - await headers(requester), - ) - const { count } = res.data - return { - encoding: 'application/json', - body: { - count, - }, - } - }, - }) - - server.app.bsky.notification.listNotifications({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const seenAt = await ctx.services - .account(ctx.db) - .getLastSeenNotifs(requester) - const res = await agent.api.app.bsky.notification.listNotifications( - { ...params, seenAt }, - await headers(requester), - ) - const { cursor, notifications } = res.data - const dids = notifications.map((notif) => notif.author.did) - const mutes = await ctx.services.account(ctx.db).getMutes(requester, dids) - const filtered = notifications.filter( - (notif) => mutes[notif.author.did] !== true, - ) - for (const notif of filtered) { - notif.isRead = seenAt !== undefined && seenAt >= notif.indexedAt - notif.author.viewer ??= {} - notif.author.viewer.muted = mutes[notif.author.did] ?? false - } - return { - encoding: 'application/json', - body: { - cursor, - notifications: filtered, - }, - } - }, - }) - - server.app.bsky.feed.getFeed({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - // @TODO cache the feedgen did lookup - const { data: feed } = await agent.api.app.bsky.feed.getFeedGenerator( - { feed: params.feed }, - await headers(requester), - ) - const res = await agent.api.app.bsky.feed.getFeed( - params, - await headers(requester, feed.view.did), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - return server -} - -const didsForFeedViewPosts = (feed: FeedViewPost[]): string[] => { - const dids: string[] = [] - for (const item of feed) { - dids.push(item.post.author.did) - if (item.reply) { - if (isPostView(item.reply.parent)) { - dids.push(item.reply.parent.author.did) - } - if (isPostView(item.reply.root)) { - dids.push(item.reply.root.author.did) - } - } - if (item.reason && isReasonRepost(item.reason)) { - dids.push(item.reason.by.did) - } - } - return dedupeStrs(dids) -} - -const processFeedViewPostMutes = ( - feed: FeedViewPost[], - mutes: Record, - shouldRemove: (post: FeedViewPost) => boolean, -): FeedViewPost[] => { - const hydrated: FeedViewPost[] = [] - for (const item of feed) { - if (shouldRemove(item)) continue - item.post.author.viewer ??= {} - item.post.author.viewer.muted = false - if (item.reply) { - if (isPostView(item.reply.parent)) { - item.reply.parent.author.viewer ??= {} - item.reply.parent.author.viewer.muted = - mutes[item.reply.parent.author.did] ?? false - } - if (isPostView(item.reply.root)) { - item.reply.root.author.viewer ??= {} - item.reply.root.author.viewer.muted = - mutes[item.reply.root.author.did] ?? false - } - } - if (item.reason && isReasonRepost(item.reason)) { - item.reason.by.viewer ??= {} - item.reason.by.viewer.muted = mutes[item.reason.by.did] ?? false - } - hydrated.push(item) - } - return hydrated -} - -const didsForThread = (thread: ThreadViewPost): string[] => { - return dedupeStrs(didsForThreadRecurse(thread)) -} - -const didsForThreadRecurse = (thread: ThreadViewPost): string[] => { - let forParent: string[] = [] - let forReplies: string[] = [] - if (isThreadViewPost(thread.parent)) { - forParent = didsForThread(thread.parent) - } - if (thread.replies) { - forReplies = thread.replies - .map((reply) => (isThreadViewPost(reply) ? didsForThread(reply) : [])) - .flat() - } - return [thread.post.author.did, ...forParent, ...forReplies] -} - -const processThreadViewMutes = ( - thread: ThreadViewPost, - mutes: Record, -): ThreadViewPost => { - thread.post.author.viewer ??= {} - thread.post.author.viewer.muted = mutes[thread.post.author.did] ?? false - if (isThreadViewPost(thread.parent)) { - processThreadViewMutes(thread.parent, mutes) - } - if (thread.replies) { - for (const reply of thread.replies) { - if (isThreadViewPost(reply)) { - processThreadViewMutes(reply, mutes) - } - } - } - return thread -} From 3b32f2b1469144b41b9caaa6ed6683957df7f30c Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Mon, 12 Jun 2023 18:48:41 -0400 Subject: [PATCH 003/105] Remove appview services, move label service to pds --- .../atproto/admin/reverseModerationAction.ts | 2 +- .../com/atproto/admin/takeModerationAction.ts | 2 +- .../pds/src/app-view/services/actor/index.ts | 68 --- .../pds/src/app-view/services/actor/views.ts | 296 ----------- .../pds/src/app-view/services/feed/index.ts | 498 ------------------ .../pds/src/app-view/services/feed/types.ts | 69 --- .../pds/src/app-view/services/feed/views.ts | 194 ------- .../pds/src/app-view/services/graph/index.ts | 124 ----- .../src/app-view/services/indexing/index.ts | 128 ----- .../services/indexing/plugins/block.ts | 97 ---- .../indexing/plugins/feed-generator.ts | 89 ---- .../services/indexing/plugins/follow.ts | 138 ----- .../services/indexing/plugins/like.ts | 124 ----- .../services/indexing/plugins/list-item.ts | 102 ---- .../services/indexing/plugins/list.ts | 86 --- .../services/indexing/plugins/post.ts | 361 ------------- .../services/indexing/plugins/profile.ts | 82 --- .../services/indexing/plugins/repost.ts | 149 ------ .../app-view/services/indexing/processor.ts | 242 --------- .../src/app-view/services/indexing/util.ts | 9 - packages/pds/src/services/index.ts | 22 +- .../{app-view => }/services/label/index.ts | 53 +- 22 files changed, 7 insertions(+), 2928 deletions(-) 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/views.ts delete mode 100644 packages/pds/src/app-view/services/graph/index.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 rename packages/pds/src/{app-view => }/services/label/index.ts (54%) diff --git a/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts b/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts index 0cfc2fd1f20..18691d89e96 100644 --- a/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts +++ b/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts @@ -14,7 +14,7 @@ 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 labelTxn = services.label(dbTxn) const now = new Date() const existing = await moderationTxn.getAction(id) diff --git a/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts b/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts index 7a0931f6871..5eb48f98e59 100644 --- a/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts +++ b/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts @@ -38,7 +38,7 @@ 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 labelTxn = services.label(dbTxn) const result = await moderationTxn.logAction({ action: getAction(action), 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 8ed28e4691b..00000000000 --- a/packages/pds/src/app-view/services/actor/index.ts +++ /dev/null @@ -1,68 +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' - -export class ActorService { - constructor(public db: Database, public imgUriBuilder: ImageUriBuilder) {} - - static creator(imgUriBuilder: ImageUriBuilder) { - return (db: Database) => new ActorService(db, imgUriBuilder) - } - - views = new ActorViews(this.db, this.imgUriBuilder) - - 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 2d6a19fdee5..00000000000 --- a/packages/pds/src/app-view/services/actor/views.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { ArrayEl } from '@atproto/common' -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 } from '../label' -import { ListViewBasic } from '../../../lexicon/types/app/bsky/graph/defs' - -export class ActorViews { - constructor(private db: Database, private imgUriBuilder: ImageUriBuilder) {} - - services = { - label: LabelService.creator(), - } - - profileDetailed( - result: ActorResult, - viewer: string, - ): Promise - profileDetailed( - result: ActorResult[], - viewer: string, - ): Promise - async profileDetailed( - result: ActorResult | ActorResult[], - viewer: string, - ): Promise { - const results = Array.isArray(result) ? result : [result] - if (results.length === 0) return [] - - const { ref } = this.db.db.dynamic - - const dids = results.map((r) => r.did) - - const profileInfosQb = this.db.db - .selectFrom('did_handle') - .where('did_handle.did', 'in', dids) - .leftJoin('profile', 'profile.creator', 'did_handle.did') - .leftJoin('profile_agg', 'profile_agg.did', 'did_handle.did') - .select([ - 'did_handle.did as did', - 'profile.uri as profileUri', - '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', - 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'), - ]) - - const [profileInfos, labels, listMutes] = await Promise.all([ - profileInfosQb.execute(), - this.services.label(this.db).getLabelsForSubjects(dids), - this.getListMutes(dids, viewer), - ]) - - const profileInfoByDid = profileInfos.reduce((acc, info) => { - return Object.assign(acc, { [info.did]: info }) - }, {} as Record>) - - const views = results.map((result) => { - const profileInfo = profileInfoByDid[result.did] - const avatar = profileInfo?.avatarCid - ? this.imgUriBuilder.getCommonSignedUri('avatar', profileInfo.avatarCid) - : undefined - const banner = profileInfo?.bannerCid - ? this.imgUriBuilder.getCommonSignedUri('banner', profileInfo.bannerCid) - : undefined - return { - did: result.did, - handle: result.handle, - displayName: truncateUtf8(profileInfo?.displayName, 64) || undefined, - description: truncateUtf8(profileInfo?.description, 256) || undefined, - avatar, - banner, - followsCount: profileInfo?.followsCount || 0, - followersCount: profileInfo?.followersCount || 0, - postsCount: profileInfo?.postsCount || 0, - indexedAt: profileInfo?.indexedAt || undefined, - viewer: { - muted: !!profileInfo?.requesterMuted || !!listMutes[result.did], - mutedByList: listMutes[result.did], - blockedBy: !!profileInfo.requesterBlockedBy, - blocking: profileInfo.requesterBlocking || undefined, - following: profileInfo?.requesterFollowing || undefined, - followedBy: profileInfo?.requesterFollowedBy || undefined, - }, - labels: labels[result.did] ?? [], - } - }) - - return Array.isArray(result) ? views : views[0] - } - - profile(result: ActorResult, viewer: string): Promise - profile(result: ActorResult[], viewer: string): Promise - async profile( - result: ActorResult | ActorResult[], - viewer: string, - ): Promise { - const results = Array.isArray(result) ? result : [result] - if (results.length === 0) return [] - - const { ref } = this.db.db.dynamic - const dids = results.map((r) => r.did) - - const profileInfosQb = this.db.db - .selectFrom('did_handle') - .where('did_handle.did', 'in', dids) - .leftJoin('profile', 'profile.creator', 'did_handle.did') - .select([ - 'did_handle.did as did', - 'profile.uri as profileUri', - 'profile.displayName as displayName', - 'profile.description as description', - 'profile.avatarCid as avatarCid', - 'profile.indexedAt as indexedAt', - 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'), - ]) - - const [profileInfos, labels, listMutes] = await Promise.all([ - profileInfosQb.execute(), - this.services.label(this.db).getLabelsForSubjects(dids), - this.getListMutes(dids, viewer), - ]) - - const profileInfoByDid = profileInfos.reduce((acc, info) => { - return Object.assign(acc, { [info.did]: info }) - }, {} as Record>) - - const views = results.map((result) => { - const profileInfo = profileInfoByDid[result.did] - const avatar = profileInfo?.avatarCid - ? this.imgUriBuilder.getCommonSignedUri('avatar', profileInfo.avatarCid) - : undefined - return { - did: result.did, - handle: result.handle, - displayName: truncateUtf8(profileInfo?.displayName, 64) || undefined, - description: truncateUtf8(profileInfo?.description, 256) || undefined, - avatar, - indexedAt: profileInfo?.indexedAt || undefined, - viewer: { - muted: !!profileInfo?.requesterMuted || !!listMutes[result.did], - mutedByList: listMutes[result.did], - blockedBy: !!profileInfo.requesterBlockedBy, - blocking: profileInfo.requesterBlocking || undefined, - following: profileInfo?.requesterFollowing || undefined, - followedBy: profileInfo?.requesterFollowedBy || undefined, - }, - labels: labels[result.did] ?? [], - } - }) - - return Array.isArray(result) ? views : views[0] - } - - // @NOTE keep in sync with feedService.getActorViews() - profileBasic(result: ActorResult, viewer: string): Promise - profileBasic( - result: ActorResult[], - viewer: string, - ): Promise - async profileBasic( - result: ActorResult | ActorResult[], - viewer: string, - ): Promise { - const results = Array.isArray(result) ? result : [result] - if (results.length === 0) return [] - - const profiles = await this.profile(results, viewer) - const views = profiles.map((view) => ({ - did: view.did, - handle: view.handle, - displayName: truncateUtf8(view.displayName, 64) || undefined, - avatar: view.avatar, - viewer: view.viewer, - labels: view.labels, - })) - - return Array.isArray(result) ? views : views[0] - } - - async getListMutes( - subjects: string[], - mutedBy: string, - ): Promise> { - if (subjects.length < 1) return {} - const res = await this.db.db - .selectFrom('list_item') - .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') - .innerJoin('list', 'list.uri', 'list_item.listUri') - .where('list_mute.mutedByDid', '=', mutedBy) - .where('list_item.subjectDid', 'in', subjects) - .selectAll('list') - .select('list_item.subjectDid as subjectDid') - .execute() - return res.reduce( - (acc, cur) => ({ - ...acc, - [cur.subjectDid]: { - uri: cur.uri, - name: cur.name, - purpose: cur.purpose, - avatar: cur.avatarCid - ? this.imgUriBuilder.getCommonSignedUri('avatar', cur.avatarCid) - : undefined, - viewer: { - muted: true, - }, - indexedAt: cur.indexedAt, - }, - }), - {} as Record, - ) - } -} - -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 005d20a10b7..00000000000 --- a/packages/pds/src/app-view/services/feed/index.ts +++ /dev/null @@ -1,498 +0,0 @@ -import { sql } from 'kysely' -import { AtUri } from '@atproto/uri' -import { dedupeStrs } from '@atproto/common' -import Database from '../../../db' -import { countAll, notSoftDeletedClause } from '../../../db/util' -import { ImageUriBuilder } from '../../../image/uri' -import { ids } from '../../../lexicon/lexicons' -import { isView as isViewImages } from '../../../lexicon/types/app/bsky/embed/images' -import { isView as isViewExternal } from '../../../lexicon/types/app/bsky/embed/external' -import { - ViewBlocked, - ViewNotFound, - ViewRecord, - View as RecordEmbedView, -} from '../../../lexicon/types/app/bsky/embed/record' -import { - FeedViewPost, - PostView, -} from '../../../lexicon/types/app/bsky/feed/defs' -import { - ActorViewMap, - FeedEmbeds, - PostInfoMap, - FeedItemType, - FeedRow, - FeedGenInfoMap, -} from './types' -import { LabelService } from '../label' -import { FeedViews } from './views' -import { ActorService } from '../actor' - -export * from './types' - -export class FeedService { - constructor(public db: Database, public imgUriBuilder: ImageUriBuilder) {} - - static creator(imgUriBuilder: ImageUriBuilder) { - return (db: Database) => new FeedService(db, imgUriBuilder) - } - - views = new FeedViews(this.db, this.imgUriBuilder) - services = { - label: LabelService.creator()(this.db), - actor: ActorService.creator(this.imgUriBuilder)(this.db), - } - - selectPostQb() { - const { ref } = this.db.db.dynamic - return this.db.db - .selectFrom('post') - .innerJoin('repo_root as author_repo', 'author_repo.did', 'post.creator') - .innerJoin('record', 'record.uri', 'post.uri') - .where(notSoftDeletedClause(ref('author_repo'))) - .where(notSoftDeletedClause(ref('record'))) - .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() { - const { ref } = this.db.db.dynamic - return this.db.db - .selectFrom('feed_item') - .innerJoin('post', 'post.uri', 'feed_item.postUri') - .innerJoin('repo_root as author_repo', 'author_repo.did', 'post.creator') - .innerJoin( - 'repo_root as originator_repo', - 'originator_repo.did', - 'feed_item.originatorDid', - ) - .innerJoin( - 'record as post_record', - 'post_record.uri', - 'feed_item.postUri', - ) - .where(notSoftDeletedClause(ref('author_repo'))) - .where(notSoftDeletedClause(ref('originator_repo'))) - .where(notSoftDeletedClause(ref('post_record'))) - .selectAll('feed_item') - .select([ - 'post.replyRoot', - 'post.replyParent', - 'post.creator as postAuthorDid', - ]) - } - - selectFeedGeneratorQb(requester: string) { - return this.db.db - .selectFrom('feed_generator') - .innerJoin('did_handle', 'did_handle.did', 'feed_generator.creator') - .selectAll() - .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 getActorViews( - dids: string[], - requester: string, - opts?: { skipLabels?: boolean }, // @NOTE used by hydrateFeed() to batch label hydration - ): Promise { - if (dids.length < 1) return {} - const { ref } = this.db.db.dynamic - const { skipLabels } = opts ?? {} - const [actors, labels, listMutes] = await Promise.all([ - this.db.db - .selectFrom('did_handle') - .where('did_handle.did', 'in', dids) - .leftJoin('profile', 'profile.creator', 'did_handle.did') - .selectAll('did_handle') - .select([ - 'profile.uri as profileUri', - 'profile.displayName as displayName', - 'profile.description as description', - 'profile.avatarCid as avatarCid', - 'profile.indexedAt as indexedAt', - 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'), - ]) - .execute(), - this.services.label.getLabelsForSubjects(skipLabels ? [] : dids), - this.services.actor.views.getListMutes(dids, requester), - ]) - return actors.reduce((acc, cur) => { - const actorLabels = labels[cur.did] ?? [] - return { - ...acc, - [cur.did]: { - did: cur.did, - handle: cur.handle, - displayName: truncateUtf8(cur.displayName, 64) || undefined, - avatar: cur.avatarCid - ? this.imgUriBuilder.getCommonSignedUri('avatar', cur.avatarCid) - : undefined, - viewer: { - muted: !!cur?.requesterMuted || !!listMutes[cur.did], - mutedByList: listMutes[cur.did], - blockedBy: !!cur?.requesterBlockedBy, - blocking: cur?.requesterBlocking || undefined, - following: cur?.requesterFollowing || undefined, - followedBy: cur?.requesterFollowedBy || undefined, - }, - labels: skipLabels ? undefined : actorLabels, - }, - } - }, {} as ActorViewMap) - } - - async getPostViews( - postUris: string[], - requester: string, - ): Promise { - if (postUris.length < 1) return {} - const db = this.db.db - const { ref } = db.dynamic - const posts = await 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') - .where(notSoftDeletedClause(ref('repo_root'))) // Ensures post reply parent/roots get omitted from views when taken down - .where(notSoftDeletedClause(ref('record'))) - .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', - 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) => ({ - ...acc, - [cur.uri]: cur, - }), - {} as PostInfoMap, - ) - } - - async getFeedGeneratorViews(generatorUris: string[], requester: string) { - if (generatorUris.length < 1) return {} - const feedGens = await this.selectFeedGeneratorQb(requester) - .where('uri', 'in', generatorUris) - .execute() - return feedGens.reduce( - (acc, cur) => ({ - ...acc, - [cur.uri]: cur, - }), - {} as FeedGenInfoMap, - ) - } - - async embedsForPosts( - uris: string[], - requester: string, - _depth = 0, - ): Promise { - if (uris.length < 1 || _depth > 1) { - // If a post has a record embed which contains additional embeds, the depth check - // above ensures that we don't recurse indefinitely into those additional embeds. - // In short, you receive up to two layers of embeds for the post: this allows us to - // handle the case that a post has a record embed, which in turn has images embedded in it. - return {} - } - const imgPromise = this.db.db - .selectFrom('post_embed_image') - .selectAll() - .where('postUri', 'in', uris) - .orderBy('postUri') - .orderBy('position') - .execute() - const extPromise = this.db.db - .selectFrom('post_embed_external') - .selectAll() - .where('postUri', 'in', uris) - .execute() - const recordPromise = this.db.db - .selectFrom('post_embed_record') - .innerJoin('record as embed', 'embed.uri', 'embedUri') - .where('postUri', 'in', uris) - .select(['postUri', 'embed.uri as uri', 'embed.did as did']) - .execute() - const [images, externals, records] = await Promise.all([ - imgPromise, - extPromise, - recordPromise, - ]) - const nestedUris = dedupeStrs(records.map((p) => p.uri)) - const nestedDids = dedupeStrs(records.map((p) => p.did)) - const nestedPostUris = nestedUris.filter( - (uri) => new AtUri(uri).collection === ids.AppBskyFeedPost, - ) - const nestedFeedGenUris = nestedUris.filter( - (uri) => new AtUri(uri).collection === ids.AppBskyFeedGenerator, - ) - const [postViews, actorViews, deepEmbedViews, labelViews, feedGenViews] = - await Promise.all([ - this.getPostViews(nestedPostUris, requester), - this.getActorViews(nestedDids, requester, { skipLabels: true }), - this.embedsForPosts(nestedPostUris, requester, _depth + 1), - this.services.label.getLabelsForSubjects([ - ...nestedPostUris, - ...nestedDids, - ]), - this.getFeedGeneratorViews(nestedFeedGenUris, requester), - ]) - let embeds = images.reduce((acc, cur) => { - const embed = (acc[cur.postUri] ??= { - $type: 'app.bsky.embed.images#view', - images: [], - }) - if (!isViewImages(embed)) return acc - embed.images.push({ - thumb: this.imgUriBuilder.getCommonSignedUri( - 'feed_thumbnail', - cur.imageCid, - ), - fullsize: this.imgUriBuilder.getCommonSignedUri( - 'feed_fullsize', - cur.imageCid, - ), - alt: cur.alt, - }) - return acc - }, {} as FeedEmbeds) - embeds = externals.reduce((acc, cur) => { - if (!acc[cur.postUri]) { - acc[cur.postUri] = { - $type: 'app.bsky.embed.external#view', - external: { - uri: cur.uri, - title: cur.title, - description: cur.description, - thumb: cur.thumbCid - ? this.imgUriBuilder.getCommonSignedUri( - 'feed_thumbnail', - cur.thumbCid, - ) - : undefined, - }, - } - } - return acc - }, embeds) - embeds = records.reduce((acc, cur) => { - const collection = new AtUri(cur.uri).collection - let recordEmbed: RecordEmbedView - if (collection === ids.AppBskyFeedGenerator && feedGenViews[cur.uri]) { - recordEmbed = { - record: { - $type: 'app.bsky.feed.defs#generatorView', - ...this.views.formatFeedGeneratorView( - feedGenViews[cur.uri], - actorViews, - labelViews, - ), - }, - } - } else if (collection === ids.AppBskyFeedPost && postViews[cur.uri]) { - const formatted = this.views.formatPostView( - cur.uri, - actorViews, - postViews, - deepEmbedViews, - labelViews, - ) - let deepEmbeds: ViewRecord['embeds'] | undefined - if (_depth < 1) { - // Omit field entirely when too deep: e.g. don't include it on the embeds within a record embed. - // Otherwise list any embeds that appear within the record. A consumer may discover an embed - // within the raw record, then look within this array to find the presented view of it. - deepEmbeds = formatted?.embed ? [formatted.embed] : [] - } - recordEmbed = { - record: getRecordEmbedView(cur.uri, formatted, deepEmbeds), - } - } else { - recordEmbed = { - record: { - $type: 'app.bsky.embed.record#viewNotFound', - uri: cur.uri, - }, - } - } - if (acc[cur.postUri]) { - const mediaEmbed = acc[cur.postUri] - if (isViewImages(mediaEmbed) || isViewExternal(mediaEmbed)) { - acc[cur.postUri] = { - $type: 'app.bsky.embed.recordWithMedia#view', - record: recordEmbed, - media: mediaEmbed, - } - } - } else { - acc[cur.postUri] = { - $type: 'app.bsky.embed.record#view', - ...recordEmbed, - } - } - return acc - }, embeds) - return embeds - } - - async hydrateFeed( - items: FeedRow[], - requester: string, - // @TODO (deprecated) remove this once all clients support the blocked/not-found union on post views - usePostViewUnion?: boolean, - ): 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, embeds, labels] = await Promise.all([ - this.getActorViews(Array.from(actorDids), requester, { - skipLabels: true, - }), - this.getPostViews(Array.from(postUris), requester), - this.embedsForPosts(Array.from(postUris), requester), - this.services.label.getLabelsForSubjects([...postUris, ...actorDids]), - ]) - - return this.views.formatFeed( - items, - actors, - posts, - embeds, - labels, - usePostViewUnion, - ) - } -} - -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 -} - -function getRecordEmbedView( - uri: string, - post?: PostView, - embeds?: ViewRecord['embeds'], -): (ViewRecord | ViewNotFound | ViewBlocked) & { $type: string } { - if (!post) { - return { - $type: 'app.bsky.embed.record#viewNotFound', - uri, - } - } - if (post.author.viewer?.blocking || post.author.viewer?.blockedBy) { - return { - $type: 'app.bsky.embed.record#viewBlocked', - uri, - } - } - 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, - } -} 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 3e76c46bee9..00000000000 --- a/packages/pds/src/app-view/services/feed/types.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { View as ViewImages } from '../../../lexicon/types/app/bsky/embed/images' -import { View as ViewExternal } from '../../../lexicon/types/app/bsky/embed/external' -import { View as ViewRecord } from '../../../lexicon/types/app/bsky/embed/record' -import { View as ViewRecordWithMedia } from '../../../lexicon/types/app/bsky/embed/recordWithMedia' -import { - BlockedPost, - 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' - -export type FeedEmbeds = { - [uri: string]: ViewImages | ViewExternal | ViewRecord | ViewRecordWithMedia -} - -export type PostInfo = { - uri: string - cid: string - creator: string - recordBytes: Uint8Array - indexedAt: string - likeCount: number | null - repostCount: number | null - replyCount: number | null - requesterRepost: string | null - requesterLike: string | null -} - -export type PostInfoMap = { [uri: string]: PostInfo } - -export type ActorView = { - did: string - handle: string - displayName?: string - avatar?: string - viewer?: { - muted?: boolean - blockedBy?: boolean - blocking?: string - following?: string - followedBy?: string - } - labels?: Label[] -} -export type ActorViewMap = { [did: string]: ActorView } - -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 MaybePostView = PostView | NotFoundPost | BlockedPost 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 8176b643d43..00000000000 --- a/packages/pds/src/app-view/services/feed/views.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { cborToLexRecord } from '@atproto/repo' -import Database from '../../../db' -import { - FeedViewPost, - GeneratorView, - PostView, -} from '../../../lexicon/types/app/bsky/feed/defs' -import { - ActorViewMap, - FeedEmbeds, - FeedGenInfo, - FeedRow, - MaybePostView, - PostInfoMap, -} from './types' -import { Labels } from '../label' -import { ProfileView } from '../../../lexicon/types/app/bsky/actor/defs' -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: Record, - labels?: Labels, - ): GeneratorView { - const profile = profiles[info.creator] - if (profile) { - // If the creator labels are not hydrated yet, attempt to pull them - // from labels: e.g. compatible with embedsForPosts() batching label hydration. - profile.labels ??= labels?.[info.creator] ?? [] - } - 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: ActorViewMap, - posts: PostInfoMap, - embeds: FeedEmbeds, - labels: Labels, - usePostViewUnion?: boolean, - ): FeedViewPost[] { - const feed: FeedViewPost[] = [] - for (const item of items) { - const post = this.formatPostView( - item.postUri, - actors, - posts, - embeds, - labels, - ) - // skip over not found & blocked posts - if (!post) { - continue - } - const feedPost = { post } - if (item.type === 'repost') { - const originator = actors[item.originatorDid] - if (originator) { - feedPost['reason'] = { - $type: 'app.bsky.feed.defs#reasonRepost', - by: { - ...originator, - labels: labels[item.originatorDid] ?? [], - }, - indexedAt: item.sortAt, - } - } - } - if (item.replyParent && item.replyRoot) { - const replyParent = this.formatMaybePostView( - item.replyParent, - actors, - posts, - embeds, - labels, - usePostViewUnion, - ) - const replyRoot = this.formatMaybePostView( - item.replyRoot, - actors, - posts, - embeds, - labels, - usePostViewUnion, - ) - if (replyRoot && replyParent) { - feedPost['reply'] = { - root: replyRoot, - parent: replyParent, - } - } - } - feed.push(feedPost) - } - return feed - } - - formatPostView( - uri: string, - actors: ActorViewMap, - posts: PostInfoMap, - embeds: FeedEmbeds, - labels: Labels, - ): 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. - author.labels ??= labels[author.did] ?? [] - return { - uri: post.uri, - cid: post.cid, - author: author, - record: cborToLexRecord(post.recordBytes), - 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: labels[uri] ?? [], - } - } - - formatMaybePostView( - uri: string, - actors: ActorViewMap, - posts: PostInfoMap, - embeds: FeedEmbeds, - labels: Labels, - usePostViewUnion?: boolean, - ): MaybePostView | undefined { - const post = this.formatPostView(uri, actors, posts, embeds, labels) - if (!post) { - if (!usePostViewUnion) return - return this.notFoundPost(uri) - } - if (post.author.viewer?.blockedBy || post.author.viewer?.blocking) { - if (!usePostViewUnion) return - return this.blockedPost(uri) - } - return { - $type: 'app.bsky.feed.defs#postView', - ...post, - } - } - - blockedPost(uri: string) { - return { - $type: 'app.bsky.feed.defs#blockedPost', - uri: uri, - blocked: true as const, - } - } - - notFoundPost(uri: string) { - return { - $type: 'app.bsky.feed.defs#notFoundPost', - uri: uri, - notFound: true as const, - } - } -} 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 58184b0663b..00000000000 --- a/packages/pds/src/app-view/services/graph/index.ts +++ /dev/null @@ -1,124 +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) { - 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, 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']) - } - - 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, - } - } - - formatListView(list: ListInfo, profiles: Record) { - return { - uri: list.uri, - 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, - }, - } - } -} - -type ListInfo = List & { - viewerMuted: string | null -} 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 0fac4d16190..00000000000 --- a/packages/pds/src/app-view/services/indexing/index.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { CID } from 'multiformats/cid' -import { WriteOpAction } from '@atproto/repo' -import { AtUri } from '@atproto/uri' -import Database from '../../../db' -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' -import { BackgroundQueue } from '../../../event-stream/background-queue' - -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 - 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), - 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 79214ad3db2..00000000000 --- a/packages/pds/src/app-view/services/indexing/plugins/block.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { AtUri } from '@atproto/uri' -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 546dd833b39..00000000000 --- a/packages/pds/src/app-view/services/indexing/plugins/feed-generator.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { AtUri } from '@atproto/uri' -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 75513bed3c3..00000000000 --- a/packages/pds/src/app-view/services/indexing/plugins/follow.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { AtUri } from '@atproto/uri' -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 3adb68cee0c..00000000000 --- a/packages/pds/src/app-view/services/indexing/plugins/like.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { AtUri } from '@atproto/uri' -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) - return [ - { - 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 812eca3be15..00000000000 --- a/packages/pds/src/app-view/services/indexing/plugins/list-item.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { AtUri } from '@atproto/uri' -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 b24e83dfe1f..00000000000 --- a/packages/pds/src/app-view/services/indexing/plugins/list.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { AtUri } from '@atproto/uri' -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 91fa5d902f3..00000000000 --- a/packages/pds/src/app-view/services/indexing/plugins/post.ts +++ /dev/null @@ -1,361 +0,0 @@ -import { sql } from 'kysely' -import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/uri' -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 { PostHierarchy } from '../../../db/tables/post-hierarchy' -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 IndexedPost = { - post: Post - facets: { type: 'mention' | 'link'; value: string }[] - embeds?: (PostEmbedImage[] | PostEmbedExternal | PostEmbedRecord)[] - ancestors: PostHierarchy[] -} - -const lexId = lex.ids.AppBskyFeedPost - -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() - } - } - // Thread index - await db - .insertInto('post_hierarchy') - .values({ - uri: post.uri, - ancestorUri: post.uri, - depth: 0, - }) - .onConflict((oc) => oc.doNothing()) // Supports post updates - .execute() - let ancestors: PostHierarchy[] = [] - if (post.replyParent) { - ancestors = await db - .insertInto('post_hierarchy') - .columns(['uri', 'ancestorUri', 'depth']) - .expression( - db - .selectFrom('post_hierarchy as parent_hierarchy') - .where('parent_hierarchy.uri', '=', post.replyParent) - .select([ - sql`${post.uri}`.as('uri'), - 'ancestorUri', - sql`depth + 1`.as('depth'), - ]), - ) - .onConflict((oc) => oc.doNothing()) // Supports post updates - .returningAll() - .execute() - } - return { post: insertedPost, facets, embeds, ancestors } -} - -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, - }) - } - } - } - const ancestors = [...obj.ancestors].sort((a, b) => a.depth - b.depth) - const BLESSED_HELL_THREAD = - 'at://did:plc:wgaezxqi2spqm3mhrb5xvkzi/app.bsky.feed.post/3juzlwllznd24' - for (const relation of ancestors) { - if (relation.depth < 5 || obj.post.replyRoot === BLESSED_HELL_THREAD) { - const ancestorUri = new AtUri(relation.ancestorUri) - 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) - } - // Do not delete, maintain thread hierarchy even if post no longer exists - const ancestors = await db - .selectFrom('post_hierarchy') - .where('uri', '=', uriStr) - .where('depth', '>', 0) - .selectAll() - .execute() - return deleted - ? { - post: deleted, - facets: [], // Not used - embeds: deletedEmbeds, - ancestors, - } - : 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 d17d971875f..00000000000 --- a/packages/pds/src/app-view/services/indexing/plugins/profile.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { AtUri } from '@atproto/uri' -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 70acadf84de..00000000000 --- a/packages/pds/src/app-view/services/indexing/plugins/repost.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/uri' -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) - return [ - { - 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 2440c562e0b..00000000000 --- a/packages/pds/src/app-view/services/indexing/processor.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/uri' -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 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 d0a5755d589..00000000000 --- a/packages/pds/src/app-view/services/indexing/util.ts +++ /dev/null @@ -1,9 +0,0 @@ -// 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() - } - return date.toISOString() // YYYY-MM-DDTHH:mm:ss.sssZ -} diff --git a/packages/pds/src/services/index.ts b/packages/pds/src/services/index.ts index dd23198a8fa..50566ad73ed 100644 --- a/packages/pds/src/services/index.ts +++ b/packages/pds/src/services/index.ts @@ -9,12 +9,8 @@ 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 { LabelService } from './label' import { Labeler } from '../labeler' -import { LabelService } from '../app-view/services/label' import { BackgroundQueue } from '../event-stream/background-queue' import { Crawlers } from '../crawlers' @@ -56,13 +52,7 @@ export function createServices(resources: { imgUriBuilder, imgInvalidator, ), - appView: { - actor: ActorService.creator(imgUriBuilder), - graph: GraphService.creator(imgUriBuilder), - feed: FeedService.creator(imgUriBuilder), - indexing: IndexingService.creator(backgroundQueue), - label: LabelService.creator(), - }, + label: LabelService.creator(), } } @@ -72,13 +62,7 @@ export type Services = { record: FromDb repo: FromDb moderation: FromDb - appView: { - feed: FromDb - indexing: FromDb - actor: FromDb - graph: FromDb - label: FromDb - } + label: FromDb } type FromDb = (db: Database) => T diff --git a/packages/pds/src/app-view/services/label/index.ts b/packages/pds/src/services/label/index.ts similarity index 54% rename from packages/pds/src/app-view/services/label/index.ts rename to packages/pds/src/services/label/index.ts index 9de4670c16e..926f1c77869 100644 --- a/packages/pds/src/app-view/services/label/index.ts +++ b/packages/pds/src/services/label/index.ts @@ -1,8 +1,6 @@ -import { AtUri } from '@atproto/uri' -import Database from '../../../db' -import { Label } from '../../../lexicon/types/com/atproto/label/defs' -import { ids } from '../../../lexicon/lexicons' import { sql } from 'kysely' +import Database from '../../db' +import { Label } from '../../lexicon/types/com/atproto/label/defs' export type Labels = Record @@ -81,51 +79,4 @@ export class LabelService { return acc }, {} as Labels) } - - // gets labels for any record. when did is present, combine labels for both did & profile record. - async getLabelsForSubjects( - subjects: string[], - includeNeg?: 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, includeNeg) - 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, includeNeg?: boolean): Promise { - const labels = await this.getLabelsForUris([subject], includeNeg) - return labels[subject] ?? [] - } - - async getLabelsForProfile( - did: string, - includeNeg?: boolean, - ): Promise { - const labels = await this.getLabelsForSubjects([did], includeNeg) - return labels[did] ?? [] - } } From 44e4715ec7ce90d4c75b7415d09bf8201e0f1c5f Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 12 Jun 2023 17:53:11 -0500 Subject: [PATCH 004/105] only proxy appview stuff --- packages/pds/src/api/app/bsky/index.ts | 2 + packages/pds/src/api/app/bsky/proxied.ts | 457 ++++++++++++++++++ .../app-view/api/app/bsky/actor/getProfile.ts | 44 -- .../api/app/bsky/actor/getProfiles.ts | 37 -- .../api/app/bsky/actor/getSuggestions.ts | 79 --- .../api/app/bsky/actor/searchActors.ts | 90 ---- .../app/bsky/actor/searchActorsTypeahead.ts | 83 ---- .../app/bsky/feed/describeFeedGenerator.ts | 21 - .../api/app/bsky/feed/getActorFeeds.ts | 66 --- .../api/app/bsky/feed/getAuthorFeed.ts | 95 ---- .../src/app-view/api/app/bsky/feed/getFeed.ts | 207 -------- .../api/app/bsky/feed/getFeedGenerator.ts | 79 --- .../api/app/bsky/feed/getFeedGenerators.ts | 42 -- .../app-view/api/app/bsky/feed/getLikes.ts | 77 --- .../api/app/bsky/feed/getPostThread.ts | 251 ---------- .../app-view/api/app/bsky/feed/getPosts.ts | 58 --- .../api/app/bsky/feed/getRepostedBy.ts | 74 --- .../app-view/api/app/bsky/feed/getTimeline.ts | 88 ---- .../app-view/api/app/bsky/graph/getBlocks.ts | 70 --- .../api/app/bsky/graph/getFollowers.ts | 77 --- .../app-view/api/app/bsky/graph/getFollows.ts | 77 --- .../app-view/api/app/bsky/graph/getList.ts | 97 ---- .../api/app/bsky/graph/getListMutes.ts | 69 --- .../app-view/api/app/bsky/graph/getLists.ts | 66 --- .../app-view/api/app/bsky/graph/getMutes.ts | 68 --- .../app-view/api/app/bsky/graph/muteActor.ts | 34 -- .../api/app/bsky/graph/muteActorList.ts | 33 -- .../api/app/bsky/graph/unmuteActor.ts | 31 -- .../api/app/bsky/graph/unmuteActorList.ts | 24 - .../pds/src/app-view/api/app/bsky/index.ts | 67 --- .../app/bsky/notification/getUnreadCount.ts | 59 --- .../bsky/notification/listNotifications.ts | 162 ------- .../api/app/bsky/notification/updateSeen.ts | 41 -- .../src/app-view/api/app/bsky/unspecced.ts | 177 ------- .../src/app-view/api/app/bsky/util/feed.ts | 19 - packages/pds/src/app-view/api/index.ts | 8 - packages/pds/src/db/database-schema.ts | 4 +- 37 files changed, 460 insertions(+), 2573 deletions(-) create mode 100644 packages/pds/src/api/app/bsky/proxied.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/feed/describeFeedGenerator.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/graph/getList.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/graph/getLists.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/graph/muteActor.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/graph/unmuteActor.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/graph/unmuteActorList.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/index.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/notification/updateSeen.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/unspecced.ts delete mode 100644 packages/pds/src/app-view/api/app/bsky/util/feed.ts delete mode 100644 packages/pds/src/app-view/api/index.ts diff --git a/packages/pds/src/api/app/bsky/index.ts b/packages/pds/src/api/app/bsky/index.ts index ad7675caeb6..3f3aaed1ded 100644 --- a/packages/pds/src/api/app/bsky/index.ts +++ b/packages/pds/src/api/app/bsky/index.ts @@ -1,7 +1,9 @@ import { Server } from '../../../lexicon' import AppContext from '../../../context' import actor from './actor' +import proxied from './proxied' export default function (server: Server, ctx: AppContext) { actor(server, ctx) + proxied(server, ctx) } diff --git a/packages/pds/src/api/app/bsky/proxied.ts b/packages/pds/src/api/app/bsky/proxied.ts new file mode 100644 index 00000000000..356280b31fc --- /dev/null +++ b/packages/pds/src/api/app/bsky/proxied.ts @@ -0,0 +1,457 @@ +import { Server } from '../../../lexicon' +import AppContext from '../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.actor.getProfile({ + auth: ctx.accessVerifier, + handler: async ({ auth, params }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.actor.getProfile( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.actor.getProfiles({ + auth: ctx.accessVerifier, + handler: async ({ auth, params }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.actor.getProfiles( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.actor.getSuggestions({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.actor.getSuggestions( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.actor.searchActors({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.actor.searchActors( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.actor.searchActorsTypeahead({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = + await ctx.appviewAgent.api.app.bsky.actor.searchActorsTypeahead( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.feed.getActorFeeds({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.feed.getActorFeeds( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.feed.getAuthorFeed({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.feed.getAuthorFeed( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.feed.getFeed({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + 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, + } + }, + }) + + server.app.bsky.feed.getFeedGenerator({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.feed.getFeedGenerators({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerators( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.feed.getPosts({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.feed.getPosts( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.feed.getPostThread({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.feed.getPostThread( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.feed.getPostThread({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.feed.getPostThread( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.feed.getRepostedBy({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.feed.getRepostedBy( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.feed.getTimeline({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.feed.getTimeline( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.graph.getBlocks({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.graph.getBlocks( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.graph.getFollowers({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.graph.getFollowers( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.graph.getFollows({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.graph.getFollows( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.graph.getList({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.graph.getList( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.graph.getListMutes({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.graph.getListMutes( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.graph.getLists({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.graph.getLists( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.graph.getMutes({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.graph.getMutes( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.graph.muteActor({ + auth: ctx.accessVerifier, + handler: async ({ input, auth }) => { + const requester = auth.credentials.did + await ctx.appviewAgent.api.app.bsky.graph.muteActor(input.body, { + ...(await ctx.serviceAuthHeaders(requester)), + encoding: 'application/json', + }) + }, + }) + + server.app.bsky.graph.muteActorList({ + auth: ctx.accessVerifier, + handler: async ({ input, auth }) => { + const requester = auth.credentials.did + await ctx.appviewAgent.api.app.bsky.graph.muteActorList(input.body, { + ...(await ctx.serviceAuthHeaders(requester)), + encoding: 'application/json', + }) + }, + }) + + server.app.bsky.graph.unmuteActor({ + auth: ctx.accessVerifier, + handler: async ({ input, auth }) => { + const requester = auth.credentials.did + await ctx.appviewAgent.api.app.bsky.graph.unmuteActor(input.body, { + ...(await ctx.serviceAuthHeaders(requester)), + encoding: 'application/json', + }) + }, + }) + + server.app.bsky.graph.unmuteActorList({ + auth: ctx.accessVerifier, + handler: async ({ input, auth }) => { + const requester = auth.credentials.did + await ctx.appviewAgent.api.app.bsky.graph.unmuteActorList(input.body, { + ...(await ctx.serviceAuthHeaders(requester)), + encoding: 'application/json', + }) + }, + }) + + server.app.bsky.notification.getUnreadCount({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = + await ctx.appviewAgent.api.app.bsky.notification.getUnreadCount( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.notification.listNotifications({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = + await ctx.appviewAgent.api.app.bsky.notification.listNotifications( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) + + server.app.bsky.notification.updateSeen({ + auth: ctx.accessVerifier, + handler: async ({ input, auth }) => { + const requester = auth.credentials.did + await ctx.appviewAgent.api.app.bsky.notification.updateSeen(input.body, { + ...(await ctx.serviceAuthHeaders(requester)), + encoding: 'application/json', + }) + }, + }) + + // @TODO maybe drop this? + server.app.bsky.unspecced.getPopular({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const hotClassicUri = Object.keys(ctx.algos).find((uri) => + uri.endsWith('/hot-classic'), + ) + if (!hotClassicUri) { + return { + encoding: 'application/json', + body: { feed: [] }, + } + } + const { data: feed } = + await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( + { feed: hotClassicUri }, + await ctx.serviceAuthHeaders(requester), + ) + const res = await ctx.appviewAgent.api.app.bsky.feed.getFeed( + { ...params, feed: hotClassicUri }, + await ctx.serviceAuthHeaders(requester, feed.view.did), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts b/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts deleted file mode 100644 index 850bd1015de..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../../lexicon' -import { softDeleted } from '../../../../../db/util' -import AppContext from '../../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.actor.getProfile({ - auth: ctx.accessVerifier, - handler: async ({ req, auth, params }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.actor.getProfile( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { actor } = params - const { db, services } = ctx - const actorService = services.appView.actor(db) - - const actorRes = await actorService.getActor(actor, true) - - if (!actorRes) { - throw new InvalidRequestError('Profile not found') - } - if (softDeleted(actorRes)) { - throw new InvalidRequestError( - 'Account has been taken down', - 'AccountTakedown', - ) - } - - return { - encoding: 'application/json', - body: await actorService.views.profileDetailed(actorRes, requester), - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts b/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts deleted file mode 100644 index bcc129b8de2..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.actor.getProfiles({ - auth: ctx.accessVerifier, - handler: async ({ req, auth, params }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.actor.getProfiles( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { actors } = params - const { db, services } = ctx - const actorService = services.appView.actor(db) - - const actorsRes = await actorService.getActors(actors) - - return { - encoding: 'application/json', - body: { - profiles: await actorService.views.profileDetailed( - actorsRes, - requester, - ), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts b/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts deleted file mode 100644 index 7b7f8d1289d..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts +++ /dev/null @@ -1,79 +0,0 @@ -import AppContext from '../../../../../context' -import { notSoftDeletedClause } from '../../../../../db/util' -import { Server } from '../../../../../lexicon' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.actor.getSuggestions({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.actor.getSuggestions( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { limit, cursor } = params - - const db = ctx.db.db - const { services } = ctx - const { ref } = db.dynamic - - const graphService = ctx.services.appView.graph(ctx.db) - - let suggestionsQb = db - .selectFrom('suggested_follow') - .innerJoin('did_handle', 'suggested_follow.did', 'did_handle.did') - .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') - .innerJoin('profile_agg', 'profile_agg.did', 'did_handle.did') - .where(notSoftDeletedClause(ref('repo_root'))) - .where('did_handle.did', '!=', requester) - .whereNotExists((qb) => - qb - .selectFrom('follow') - .selectAll() - .where('creator', '=', requester) - .whereRef('subjectDid', '=', ref('did_handle.did')), - ) - .whereNotExists( - graphService.blockQb(requester, [ref('did_handle.did')]), - ) - .selectAll('did_handle') - .select('profile_agg.postsCount as postsCount') - .limit(limit) - .orderBy('suggested_follow.order', 'asc') - - if (cursor) { - const cursorRow = await db - .selectFrom('suggested_follow') - .where('did', '=', cursor) - .selectAll() - .executeTakeFirst() - if (cursorRow) { - suggestionsQb = suggestionsQb.where( - 'suggested_follow.order', - '>', - cursorRow.order, - ) - } - } - - const suggestionsRes = await suggestionsQb.execute() - - return { - encoding: 'application/json', - body: { - cursor: suggestionsRes.at(-1)?.did, - actors: await services.appView - .actor(ctx.db) - .views.profile(suggestionsRes, requester), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts b/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts deleted file mode 100644 index f3f95460686..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { sql } from 'kysely' -import AppContext from '../../../../../context' -import Database from '../../../../../db' -import { DidHandle } from '../../../../../db/tables/did-handle' -import { Server } from '../../../../../lexicon' -import * as Method from '../../../../../lexicon/types/app/bsky/actor/searchActors' -import { - cleanTerm, - getUserSearchQueryPg, - getUserSearchQuerySqlite, - SearchKeyset, -} from '../../../../../services/util/search' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.actor.searchActors({ - auth: ctx.accessVerifier, - handler: async ({ req, auth, params }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.actor.searchActors( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { services, db } = ctx - let { term, limit } = params - const { cursor } = params - - term = cleanTerm(term || '') - limit = Math.min(limit ?? 25, 100) - - if (!term) { - return { - encoding: 'application/json', - body: { - actors: [], - }, - } - } - - const results = - db.dialect === 'pg' - ? await getResultsPg(db, { term, limit, cursor }) - : await getResultsSqlite(db, { term, limit, cursor }) - - const keyset = new SearchKeyset(sql``, sql``) - - const actors = await services.appView - .actor(db) - .views.profile(results, requester) - - const filtered = actors.filter( - (actor) => !actor.viewer?.blocking && !actor.viewer?.blockedBy, - ) - - return { - encoding: 'application/json', - body: { - cursor: keyset.packFromResult(results), - actors: filtered, - }, - } - }, - }) -} - -const getResultsPg: GetResultsFn = async (db, { term, limit, cursor }) => { - return await getUserSearchQueryPg(db, { term: term || '', limit, cursor }) - .select('distance') - .selectAll('did_handle') - .execute() -} - -const getResultsSqlite: GetResultsFn = async (db, { term, limit, cursor }) => { - return await getUserSearchQuerySqlite(db, { term: term || '', limit, cursor }) - .leftJoin('profile', 'profile.creator', 'did_handle.did') - .select(sql`0`.as('distance')) - .selectAll('did_handle') - .execute() -} - -type GetResultsFn = ( - db: Database, - opts: Method.QueryParams & { limit: number }, -) => Promise<(DidHandle & { distance: number })[]> diff --git a/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts b/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts deleted file mode 100644 index fc34adba5a0..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts +++ /dev/null @@ -1,83 +0,0 @@ -import AppContext from '../../../../../context' -import Database from '../../../../../db' -import { Server } from '../../../../../lexicon' -import * as Method from '../../../../../lexicon/types/app/bsky/actor/searchActorsTypeahead' -import { - cleanTerm, - getUserSearchQueryPg, - getUserSearchQuerySqlite, -} from '../../../../../services/util/search' -import { DidHandle } from '../../../../../db/tables/did-handle' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.actor.searchActorsTypeahead({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = - await ctx.appviewAgent.api.app.bsky.actor.searchActorsTypeahead( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { services, db } = ctx - let { term, limit } = params - - term = cleanTerm(term || '') - limit = Math.min(limit ?? 25, 100) - - if (!term) { - return { - encoding: 'application/json', - body: { - actors: [], - }, - } - } - - const results = - ctx.db.dialect === 'pg' - ? await getResultsPg(ctx.db, { term, limit }) - : await getResultsSqlite(ctx.db, { term, limit }) - - const actors = await services.appView - .actor(db) - .views.profileBasic(results, requester) - - const filtered = actors.filter( - (actor) => !actor.viewer?.blocking && !actor.viewer?.blockedBy, - ) - - return { - encoding: 'application/json', - body: { - actors: filtered, - }, - } - }, - }) -} - -const getResultsPg: GetResultsFn = async (db, { term, limit }) => { - return await getUserSearchQueryPg(db, { term: term || '', limit }) - .selectAll('did_handle') - .execute() -} - -const getResultsSqlite: GetResultsFn = async (db, { term, limit }) => { - return await getUserSearchQuerySqlite(db, { term: term || '', limit }) - .leftJoin('profile', 'profile.creator', 'did_handle.did') - .selectAll('did_handle') - .execute() -} - -type GetResultsFn = ( - db: Database, - opts: Method.QueryParams & { limit: number }, -) => Promise diff --git a/packages/pds/src/app-view/api/app/bsky/feed/describeFeedGenerator.ts b/packages/pds/src/app-view/api/app/bsky/feed/describeFeedGenerator.ts deleted file mode 100644 index 89a8a1f48dd..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/feed/describeFeedGenerator.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' -import { MethodNotImplementedError } from '@atproto/xrpc-server' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.describeFeedGenerator(async () => { - if (!ctx.cfg.feedGenDid) { - throw new MethodNotImplementedError() - } - - const feeds = Object.keys(ctx.algos).map((uri) => ({ uri })) - - return { - encoding: 'application/json', - body: { - did: ctx.cfg.feedGenDid, - feeds, - }, - } - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts b/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts deleted file mode 100644 index fe6ac23416f..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' -import { TimeCidKeyset, paginate } from '../../../../../db/pagination' -import { InvalidRequestError } from '@atproto/xrpc-server' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.getActorFeeds({ - auth: ctx.accessVerifier, - handler: async ({ req, auth, params }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getActorFeeds( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { actor, limit, cursor } = params - - const actorService = ctx.services.appView.actor(ctx.db) - const feedService = ctx.services.appView.feed(ctx.db) - - const creatorRes = await actorService.getActor(actor) - if (!creatorRes) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } - - const { ref } = ctx.db.db.dynamic - let feedsQb = feedService - .selectFeedGeneratorQb(requester) - .where('feed_generator.creator', '=', creatorRes.did) - - const keyset = new TimeCidKeyset( - ref('feed_generator.createdAt'), - ref('feed_generator.cid'), - ) - feedsQb = paginate(feedsQb, { - limit, - cursor, - keyset, - }) - - const [feedsRes, creatorProfile] = await Promise.all([ - feedsQb.execute(), - actorService.views.profile(creatorRes, requester), - ]) - const profiles = { [creatorProfile.did]: creatorProfile } - - const feeds = feedsRes.map((row) => - feedService.views.formatFeedGeneratorView(row, profiles), - ) - - return { - encoding: 'application/json', - body: { - cursor: keyset.packFromResult(feedsRes), - feeds, - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts deleted file mode 100644 index f30e5d2773a..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { Server } from '../../../../../lexicon' -import { FeedKeyset } from '../util/feed' -import { paginate } from '../../../../../db/pagination' -import AppContext from '../../../../../context' -import { FeedRow } from '../../../../services/feed' -import { InvalidRequestError } from '@atproto/xrpc-server' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.getAuthorFeed({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getAuthorFeed( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { actor, limit, cursor } = params - const db = ctx.db.db - const { ref } = db.dynamic - - // first verify there is not a block between requester & subject - const blocks = await ctx.services.appView - .graph(ctx.db) - .getBlocks(requester, actor) - if (blocks.blocking) { - throw new InvalidRequestError( - `Requester has blocked actor: ${actor}`, - 'BlockedActor', - ) - } else if (blocks.blockedBy) { - throw new InvalidRequestError( - `Requester is blocked by actor: $${actor}`, - 'BlockedByActor', - ) - } - - const accountService = ctx.services.account(ctx.db) - const feedService = ctx.services.appView.feed(ctx.db) - const graphService = ctx.services.appView.graph(ctx.db) - - const userLookupCol = actor.startsWith('did:') - ? 'did_handle.did' - : 'did_handle.handle' - const actorDidQb = db - .selectFrom('did_handle') - .select('did') - .where(userLookupCol, '=', actor) - .limit(1) - - let feedItemsQb = feedService - .selectFeedItemQb() - .where('originatorDid', '=', actorDidQb) - .where((qb) => - // Hide reposts of muted content - qb - .where('type', '=', 'post') - .orWhere((qb) => - accountService.whereNotMuted(qb, requester, [ - ref('post.creator'), - ]), - ), - ) - .whereNotExists(graphService.blockQb(requester, [ref('post.creator')])) - - const keyset = new FeedKeyset( - ref('feed_item.sortAt'), - ref('feed_item.cid'), - ) - - feedItemsQb = paginate(feedItemsQb, { - limit, - cursor, - keyset, - }) - - const feedItems: FeedRow[] = await feedItemsQb.execute() - const feed = await feedService.hydrateFeed(feedItems, requester) - - return { - encoding: 'application/json', - body: { - feed, - cursor: keyset.packFromResult(feedItems), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts deleted file mode 100644 index ca1ea03c495..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { - InvalidRequestError, - UpstreamFailureError, - createServiceAuthHeaders, - ServerTimer, - serverTimingHeader, -} from '@atproto/xrpc-server' -import { ResponseType, XRPCError } from '@atproto/xrpc' -import { - DidDocument, - PoorlyFormattedDidDocumentError, - getFeedGen, -} from '@atproto/identity' -import { AtpAgent, AppBskyFeedGetFeedSkeleton } from '@atproto/api' -import { SkeletonFeedPost } from '../../../../../lexicon/types/app/bsky/feed/defs' -import { QueryParams as GetFeedParams } from '../../../../../lexicon/types/app/bsky/feed/getFeed' -import { OutputSchema as SkeletonOutput } from '../../../../../lexicon/types/app/bsky/feed/getFeedSkeleton' -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' -import { FeedRow } from '../../../../services/feed' -import { AlgoResponse } from '../../../../../feed-gen/types' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.getFeed({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const { data: feed } = - await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( - { feed: params.feed }, - await ctx.serviceAuthHeaders(requester), - ) - const res = await ctx.appviewAgent.api.app.bsky.feed.getFeed( - params, - await ctx.serviceAuthHeaders(requester, feed.view.did), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { feed } = params - const feedService = ctx.services.appView.feed(ctx.db) - const localAlgo = ctx.algos[feed] - - const timerSkele = new ServerTimer('skele').start() - const { feedItems, ...rest } = - localAlgo !== undefined - ? await localAlgo(ctx, params, requester) - : await skeletonFromFeedGen(ctx, params, requester) - timerSkele.stop() - - const timerHydr = new ServerTimer('hydr').start() - const hydrated = await feedService.hydrateFeed(feedItems, requester) - timerHydr.stop() - - return { - encoding: 'application/json', - body: { - ...rest, - feed: hydrated, - }, - headers: { - 'server-timing': serverTimingHeader([timerSkele, timerHydr]), - }, - } - }, - }) -} - -async function skeletonFromFeedGen( - ctx: AppContext, - params: GetFeedParams, - requester: string, -): Promise { - const { feed } = params - // Resolve and fetch feed skeleton - const found = await ctx.db.db - .selectFrom('feed_generator') - .where('uri', '=', feed) - .select('feedDid') - .executeTakeFirst() - if (!found) { - throw new InvalidRequestError('could not find feed') - } - const feedDid = found.feedDid - - let resolved: DidDocument | null - try { - resolved = await ctx.idResolver.did.resolve(feedDid) - } catch (err) { - if (err instanceof PoorlyFormattedDidDocumentError) { - throw new InvalidRequestError(`invalid did document: ${feedDid}`) - } - throw err - } - if (!resolved) { - throw new InvalidRequestError(`could not resolve did document: ${feedDid}`) - } - - const fgEndpoint = getFeedGen(resolved) - if (!fgEndpoint) { - throw new InvalidRequestError( - `invalid feed generator service details in did document: ${feedDid}`, - ) - } - - const agent = new AtpAgent({ service: fgEndpoint }) - const headers = await createServiceAuthHeaders({ - iss: requester, - aud: feedDid, - keypair: ctx.repoSigningKey, - }) - - let skeleton: SkeletonOutput - try { - const result = await agent.api.app.bsky.feed.getFeedSkeleton( - params, - headers, - ) - skeleton = result.data - } catch (err) { - if (err instanceof AppBskyFeedGetFeedSkeleton.UnknownFeedError) { - throw new InvalidRequestError(err.message, 'UnknownFeed') - } - if (err instanceof XRPCError) { - if (err.status === ResponseType.Unknown) { - throw new UpstreamFailureError('feed unavailable') - } - if (err.status === ResponseType.InvalidResponse) { - throw new UpstreamFailureError( - 'feed provided an invalid response', - 'InvalidFeedResponse', - ) - } - } - throw err - } - - const { feed: skeletonFeed, ...rest } = skeleton - - // Hydrate feed skeleton - const { ref } = ctx.db.db.dynamic - const feedService = ctx.services.appView.feed(ctx.db) - const graphService = ctx.services.appView.graph(ctx.db) - const accountService = ctx.services.account(ctx.db) - const feedItemUris = skeletonFeed.map(getSkeleFeedItemUri) - - const feedItems = feedItemUris.length - ? await feedService - .selectFeedItemQb() - .where('feed_item.uri', 'in', feedItemUris) - .where((qb) => - // Hide posts and reposts of or by muted actors - accountService.whereNotMuted(qb, requester, [ - ref('post.creator'), - ref('originatorDid'), - ]), - ) - .whereNotExists( - graphService.blockQb(requester, [ - ref('post.creator'), - ref('originatorDid'), - ]), - ) - .execute() - : [] - - const orderedItems = getOrderedFeedItems(skeletonFeed, feedItems, params) - return { - ...rest, - feedItems: orderedItems, - } -} - -function getSkeleFeedItemUri(item: SkeletonFeedPost) { - if (typeof item.reason?.repost === 'string') { - return item.reason.repost - } - return item.post -} - -function getOrderedFeedItems( - skeletonItems: SkeletonFeedPost[], - feedItems: FeedRow[], - params: GetFeedParams, -) { - const SKIP = [] - const feedItemsByUri = feedItems.reduce((acc, item) => { - return Object.assign(acc, { [item.uri]: item }) - }, {} as Record) - // enforce limit param in the case that the feedgen does not - if (skeletonItems.length > params.limit) { - skeletonItems = skeletonItems.slice(0, params.limit) - } - return skeletonItems.flatMap((item) => { - const uri = getSkeleFeedItemUri(item) - const feedItem = feedItemsByUri[uri] - if (!feedItem || item.post !== feedItem.postUri) { - // Couldn't find the record, or skeleton repost referenced the wrong post - return SKIP - } - return feedItem - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts b/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts deleted file mode 100644 index 1e7ebff6aca..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { - DidDocument, - PoorlyFormattedDidDocumentError, - getFeedGen, -} from '@atproto/identity' -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.getFeedGenerator({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { feed } = params - - const feedService = ctx.services.appView.feed(ctx.db) - - const got = await feedService.getFeedGeneratorViews([feed], requester) - const feedInfo = got[feed] - if (!feedInfo) { - throw new InvalidRequestError('could not find feed') - } - - const feedDid = feedInfo.feedDid - let resolved: DidDocument | null - try { - resolved = await ctx.idResolver.did.resolve(feedDid) - } catch (err) { - if (err instanceof PoorlyFormattedDidDocumentError) { - throw new InvalidRequestError(`invalid did document: ${feedDid}`) - } - throw err - } - if (!resolved) { - throw new InvalidRequestError( - `could not resolve did document: ${feedDid}`, - ) - } - - const fgEndpoint = getFeedGen(resolved) - if (!fgEndpoint) { - throw new InvalidRequestError( - `invalid feed generator service details in did document: ${feedDid}`, - ) - } - - const profiles = await feedService.getActorViews( - [feedInfo.creator], - requester, - ) - const feedView = feedService.views.formatFeedGeneratorView( - feedInfo, - profiles, - ) - - return { - encoding: 'application/json', - body: { - view: feedView, - // @TODO temporarily hard-coding to true while external feedgens catch-up on describeFeedGenerator - isOnline: true, - isValid: true, - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts b/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts deleted file mode 100644 index 3052c642a46..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.getFeedGenerators({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerators( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { feeds } = params - - const feedService = ctx.services.appView.feed(ctx.db) - - const genViews = await feedService.getFeedGeneratorViews(feeds, requester) - const genList = Object.values(genViews) - - const creators = genList.map((gen) => gen.creator) - const profiles = await feedService.getActorViews(creators, requester) - - const feedViews = genList.map((gen) => - feedService.views.formatFeedGeneratorView(gen, profiles), - ) - - return { - encoding: 'application/json', - body: { - feeds: feedViews, - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts b/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts deleted file mode 100644 index a29f69a8be7..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Server } from '../../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../../db/pagination' -import AppContext from '../../../../../context' -import { notSoftDeletedClause } from '../../../../../db/util' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.getLikes({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getLikes( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { uri, limit, cursor, cid } = params - const { services, db } = ctx - const { ref } = db.db.dynamic - - const graphService = ctx.services.appView.graph(ctx.db) - - let builder = db.db - .selectFrom('like') - .where('like.subject', '=', uri) - .innerJoin('did_handle as creator', 'creator.did', 'like.creator') - .innerJoin( - 'repo_root as creator_repo', - 'creator_repo.did', - 'like.creator', - ) - .where(notSoftDeletedClause(ref('creator_repo'))) - .whereNotExists(graphService.blockQb(requester, [ref('like.creator')])) - .selectAll('creator') - .select([ - 'like.cid as cid', - 'like.createdAt as createdAt', - 'like.indexedAt as indexedAt', - ]) - - if (cid) { - builder = builder.where('like.subjectCid', '=', cid) - } - - const keyset = new TimeCidKeyset(ref('like.createdAt'), ref('like.cid')) - builder = paginate(builder, { - limit, - cursor, - keyset, - }) - - const likesRes = await builder.execute() - const actors = await services.appView - .actor(db) - .views.profile(likesRes, requester) - - return { - encoding: 'application/json', - body: { - uri, - cid, - cursor: keyset.packFromResult(likesRes), - likes: likesRes.map((row, i) => ({ - createdAt: row.createdAt, - indexedAt: row.indexedAt, - actor: actors[i], - })), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts b/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts deleted file mode 100644 index d2fac10d67f..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' -import { - ActorViewMap, - FeedEmbeds, - FeedRow, - FeedService, - PostInfoMap, -} from '../../../../services/feed' -import { Labels } from '../../../../services/label' -import { - BlockedPost, - NotFoundPost, - ThreadViewPost, - isNotFoundPost, -} from '../../../../../lexicon/types/app/bsky/feed/defs' - -export type PostThread = { - post: FeedRow - parent?: PostThread | ParentNotFoundError - replies?: PostThread[] -} - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.getPostThread({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getPostThread( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { uri, depth, parentHeight } = params - - const feedService = ctx.services.appView.feed(ctx.db) - const labelService = ctx.services.appView.label(ctx.db) - - const threadData = await getThreadData( - feedService, - uri, - depth, - parentHeight, - ) - if (!threadData) { - throw new InvalidRequestError(`Post not found: ${uri}`, 'NotFound') - } - const relevant = getRelevantIds(threadData) - const [actors, posts, embeds, labels] = await Promise.all([ - feedService.getActorViews(Array.from(relevant.dids), requester, { - skipLabels: true, - }), - feedService.getPostViews(Array.from(relevant.uris), requester), - feedService.embedsForPosts(Array.from(relevant.uris), requester), - labelService.getLabelsForSubjects([...relevant.uris, ...relevant.dids]), - ]) - - const thread = composeThread( - threadData, - feedService, - posts, - actors, - embeds, - labels, - ) - - if (isNotFoundPost(thread)) { - // @TODO technically this could be returned as a NotFoundPost based on lexicon - throw new InvalidRequestError(`Post not found: ${uri}`, 'NotFound') - } - - return { - encoding: 'application/json', - body: { thread }, - } - }, - }) -} - -const composeThread = ( - threadData: PostThread, - feedService: FeedService, - posts: PostInfoMap, - actors: ActorViewMap, - embeds: FeedEmbeds, - labels: Labels, -): ThreadViewPost | NotFoundPost | BlockedPost => { - const post = feedService.views.formatPostView( - threadData.post.postUri, - actors, - posts, - embeds, - labels, - ) - - if (!post) { - return { - $type: 'app.bsky.feed.defs#notFoundPost', - uri: threadData.post.postUri, - notFound: true, - } - } - - if (post.author.viewer?.blocking || post.author.viewer?.blockedBy) { - return { - $type: 'app.bsky.feed.defs#blockedPost', - uri: threadData.post.postUri, - blocked: true, - } - } - - let parent: ThreadViewPost | NotFoundPost | BlockedPost | undefined - if (threadData.parent) { - if (threadData.parent instanceof ParentNotFoundError) { - parent = { - $type: 'app.bsky.feed.defs#notFoundPost', - uri: threadData.parent.uri, - notFound: true, - } - } else { - parent = composeThread( - threadData.parent, - feedService, - posts, - actors, - embeds, - labels, - ) - } - } - - let replies: (ThreadViewPost | NotFoundPost | BlockedPost)[] | undefined - if (threadData.replies) { - replies = threadData.replies.map((reply) => - composeThread(reply, feedService, posts, actors, embeds, labels), - ) - } - - return { - $type: 'app.bsky.feed.defs#threadViewPost', - post, - parent, - replies, - } -} - -const getRelevantIds = ( - thread: PostThread, -): { dids: Set; uris: Set } => { - const dids = new Set() - const uris = new Set() - if (thread.parent && !(thread.parent instanceof ParentNotFoundError)) { - const fromParent = getRelevantIds(thread.parent) - fromParent.dids.forEach((did) => dids.add(did)) - fromParent.uris.forEach((uri) => uris.add(uri)) - } - if (thread.replies) { - for (const reply of thread.replies) { - const fromChild = getRelevantIds(reply) - fromChild.dids.forEach((did) => dids.add(did)) - fromChild.uris.forEach((uri) => uris.add(uri)) - } - } - dids.add(thread.post.postAuthorDid) - uris.add(thread.post.postUri) - return { dids, uris } -} - -const getThreadData = async ( - feedService: FeedService, - uri: string, - depth: number, - parentHeight: number, -): Promise => { - const [parents, children] = await Promise.all([ - feedService - .selectPostQb() - .innerJoin('post_hierarchy', 'post_hierarchy.ancestorUri', 'post.uri') - .where('post_hierarchy.uri', '=', uri) - .execute(), - feedService - .selectPostQb() - .innerJoin('post_hierarchy', 'post_hierarchy.uri', 'post.uri') - .where('post_hierarchy.uri', '!=', uri) - .where('post_hierarchy.ancestorUri', '=', uri) - .where('depth', '<=', depth) - .orderBy('post.createdAt', 'desc') - .execute(), - ]) - const parentsByUri = parents.reduce((acc, parent) => { - return Object.assign(acc, { [parent.postUri]: parent }) - }, {} as Record) - const childrenByParentUri = children.reduce((acc, child) => { - if (!child.replyParent) return acc - acc[child.replyParent] ??= [] - acc[child.replyParent].push(child) - return acc - }, {} as Record) - const post = parentsByUri[uri] - if (!post) return null - return { - post, - parent: post.replyParent - ? getParentData(parentsByUri, post.replyParent, parentHeight) - : undefined, - replies: getChildrenData(childrenByParentUri, uri, depth), - } -} - -const getParentData = ( - postsByUri: Record, - uri: string, - depth: number, -): PostThread | ParentNotFoundError | undefined => { - if (depth === 0) return undefined - const post = postsByUri[uri] - if (!post) return new ParentNotFoundError(uri) - return { - post, - parent: post.replyParent - ? getParentData(postsByUri, post.replyParent, depth - 1) - : undefined, - replies: [], - } -} - -const getChildrenData = ( - childrenByParentUri: Record, - uri: string, - depth: number, -): PostThread[] | undefined => { - if (depth === 0) return undefined - const children = childrenByParentUri[uri] ?? [] - return children.map((row) => ({ - post: row, - replies: getChildrenData(childrenByParentUri, row.postUri, depth - 1), - })) -} - -class ParentNotFoundError extends Error { - constructor(public uri: string) { - super(`Parent not found: ${uri}`) - } -} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts b/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts deleted file mode 100644 index 0f5df994e8e..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts +++ /dev/null @@ -1,58 +0,0 @@ -import * as common from '@atproto/common' -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' -import { AtUri } from '@atproto/uri' -import { PostView } from '@atproto/api/src/client/types/app/bsky/feed/defs' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.getPosts({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getPosts( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const feedService = ctx.services.appView.feed(ctx.db) - const labelService = ctx.services.appView.label(ctx.db) - - const uris = common.dedupeStrs(params.uris) - const dids = common.dedupeStrs( - params.uris.map((uri) => new AtUri(uri).hostname), - ) - - const [actors, postViews, embeds, labels] = await Promise.all([ - feedService.getActorViews(dids, requester, { skipLabels: true }), - feedService.getPostViews(uris, requester), - feedService.embedsForPosts(uris, requester), - labelService.getLabelsForSubjects([...uris, ...dids]), - ]) - - const posts: PostView[] = [] - for (const uri of uris) { - const post = feedService.views.formatPostView( - uri, - actors, - postViews, - embeds, - labels, - ) - if (post) { - posts.push(post) - } - } - - return { - encoding: 'application/json', - body: { posts }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts b/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts deleted file mode 100644 index 5d5948954b5..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Server } from '../../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../../db/pagination' -import AppContext from '../../../../../context' -import { notSoftDeletedClause } from '../../../../../db/util' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.getRepostedBy({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getRepostedBy( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { uri, limit, cursor, cid } = params - const { services, db } = ctx - const { ref } = db.db.dynamic - - const graphService = ctx.services.appView.graph(ctx.db) - - let builder = db.db - .selectFrom('repost') - .where('repost.subject', '=', uri) - .innerJoin('did_handle as creator', 'creator.did', 'repost.creator') - .innerJoin( - 'repo_root as creator_repo', - 'creator_repo.did', - 'repost.creator', - ) - .where(notSoftDeletedClause(ref('creator_repo'))) - .whereNotExists( - graphService.blockQb(requester, [ref('repost.creator')]), - ) - .selectAll('creator') - .select(['repost.cid as cid', 'repost.createdAt as createdAt']) - - if (cid) { - builder = builder.where('repost.subjectCid', '=', cid) - } - - const keyset = new TimeCidKeyset( - ref('repost.createdAt'), - ref('repost.cid'), - ) - builder = paginate(builder, { - limit, - cursor, - keyset, - }) - - const repostedByRes = await builder.execute() - const repostedBy = await services.appView - .actor(db) - .views.profile(repostedByRes, requester) - - return { - encoding: 'application/json', - body: { - uri, - cid, - repostedBy, - cursor: keyset.packFromResult(repostedByRes), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts b/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts deleted file mode 100644 index 8da47aab4af..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../../lexicon' -import { FeedAlgorithm, FeedKeyset, getFeedDateThreshold } from '../util/feed' -import { paginate } from '../../../../../db/pagination' -import AppContext from '../../../../../context' -import { FeedRow } from '../../../../services/feed' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.getTimeline({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getTimeline( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { algorithm, limit, cursor } = params - const db = ctx.db.db - const { ref } = db.dynamic - - if (algorithm && algorithm !== FeedAlgorithm.ReverseChronological) { - throw new InvalidRequestError(`Unsupported algorithm: ${algorithm}`) - } - - const accountService = ctx.services.account(ctx.db) - const feedService = ctx.services.appView.feed(ctx.db) - const graphService = ctx.services.appView.graph(ctx.db) - - const followingIdsSubquery = db - .selectFrom('follow') - .select('follow.subjectDid') - .where('follow.creator', '=', requester) - - const keyset = new FeedKeyset( - ref('feed_item.sortAt'), - ref('feed_item.cid'), - ) - const sortFrom = keyset.unpack(cursor)?.primary - - let feedItemsQb = feedService - .selectFeedItemQb() - .where((qb) => - qb - .where('originatorDid', '=', requester) - .orWhere('originatorDid', 'in', followingIdsSubquery), - ) - .where((qb) => - // Hide posts and reposts of or by muted actors - accountService.whereNotMuted(qb, requester, [ - ref('post.creator'), - ref('originatorDid'), - ]), - ) - .whereNotExists( - graphService.blockQb(requester, [ - ref('post.creator'), - ref('originatorDid'), - ]), - ) - .where('feed_item.sortAt', '>', getFeedDateThreshold(sortFrom)) - - feedItemsQb = paginate(feedItemsQb, { - limit, - cursor, - keyset, - tryIndex: true, - }) - - const feedItems: FeedRow[] = await feedItemsQb.execute() - const feed = await feedService.hydrateFeed(feedItems, requester) - - return { - encoding: 'application/json', - body: { - feed, - cursor: keyset.packFromResult(feedItems), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts b/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts deleted file mode 100644 index e14a12bd37a..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Server } from '../../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../../db/pagination' -import AppContext from '../../../../../context' -import { notSoftDeletedClause } from '../../../../../db/util' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.graph.getBlocks({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.graph.getBlocks( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { limit, cursor } = params - const { services, db } = ctx - const { ref } = db.db.dynamic - - let blocksReq = ctx.db.db - .selectFrom('actor_block') - .where('actor_block.creator', '=', requester) - .innerJoin( - 'did_handle as subject', - 'subject.did', - 'actor_block.subjectDid', - ) - .innerJoin( - 'repo_root as subject_repo', - 'subject_repo.did', - 'actor_block.subjectDid', - ) - .where(notSoftDeletedClause(ref('subject_repo'))) - .selectAll('subject') - .select([ - 'actor_block.cid as cid', - 'actor_block.createdAt as createdAt', - ]) - - const keyset = new TimeCidKeyset( - ref('actor_block.createdAt'), - ref('actor_block.cid'), - ) - blocksReq = paginate(blocksReq, { - limit, - cursor, - keyset, - }) - - const blocksRes = await blocksReq.execute() - - const actorService = services.appView.actor(db) - const blocks = await actorService.views.profile(blocksRes, requester) - - return { - encoding: 'application/json', - body: { - blocks, - cursor: keyset.packFromResult(blocksRes), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts b/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts deleted file mode 100644 index 3b3281edb41..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../../db/pagination' -import AppContext from '../../../../../context' -import { notSoftDeletedClause } from '../../../../../db/util' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.graph.getFollowers({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.graph.getFollowers( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { actor, limit, cursor } = params - const { services, db } = ctx - const { ref } = db.db.dynamic - - const actorService = services.appView.actor(db) - const graphService = services.appView.graph(db) - - const subjectRes = await actorService.getActor(actor) - if (!subjectRes) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } - - let followersReq = ctx.db.db - .selectFrom('follow') - .where('follow.subjectDid', '=', subjectRes.did) - .innerJoin('did_handle as creator', 'creator.did', 'follow.creator') - .innerJoin( - 'repo_root as creator_repo', - 'creator_repo.did', - 'follow.creator', - ) - .where(notSoftDeletedClause(ref('creator_repo'))) - .whereNotExists( - graphService.blockQb(requester, [ref('follow.subjectDid')]), - ) - .selectAll('creator') - .select(['follow.cid as cid', 'follow.createdAt as createdAt']) - - const keyset = new TimeCidKeyset( - ref('follow.createdAt'), - ref('follow.cid'), - ) - followersReq = paginate(followersReq, { - limit, - cursor, - keyset, - }) - - const followersRes = await followersReq.execute() - const [followers, subject] = await Promise.all([ - actorService.views.profile(followersRes, requester), - actorService.views.profile(subjectRes, requester), - ]) - - return { - encoding: 'application/json', - body: { - subject, - followers, - cursor: keyset.packFromResult(followersRes), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts b/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts deleted file mode 100644 index 4a8bf888fc6..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../../db/pagination' -import AppContext from '../../../../../context' -import { notSoftDeletedClause } from '../../../../../db/util' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.graph.getFollows({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.graph.getFollows( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { actor, limit, cursor } = params - const { services, db } = ctx - const { ref } = db.db.dynamic - - const actorService = services.appView.actor(db) - const graphService = services.appView.graph(db) - - const creatorRes = await actorService.getActor(actor) - if (!creatorRes) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } - - let followsReq = ctx.db.db - .selectFrom('follow') - .where('follow.creator', '=', creatorRes.did) - .innerJoin('did_handle as subject', 'subject.did', 'follow.subjectDid') - .innerJoin( - 'repo_root as subject_repo', - 'subject_repo.did', - 'follow.subjectDid', - ) - .where(notSoftDeletedClause(ref('subject_repo'))) - .whereNotExists( - graphService.blockQb(requester, [ref('follow.creator')]), - ) - .selectAll('subject') - .select(['follow.cid as cid', 'follow.createdAt as createdAt']) - - const keyset = new TimeCidKeyset( - ref('follow.createdAt'), - ref('follow.cid'), - ) - followsReq = paginate(followsReq, { - limit, - cursor, - keyset, - }) - - const followsRes = await followsReq.execute() - const [follows, subject] = await Promise.all([ - actorService.views.profile(followsRes, requester), - actorService.views.profile(creatorRes, requester), - ]) - - return { - encoding: 'application/json', - body: { - subject, - follows, - cursor: keyset.packFromResult(followsRes), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getList.ts b/packages/pds/src/app-view/api/app/bsky/graph/getList.ts deleted file mode 100644 index f9e393ba3f8..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/graph/getList.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../../db/pagination' -import AppContext from '../../../../../context' -import { ProfileView } from '../../../../../lexicon/types/app/bsky/actor/defs' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.graph.getList({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.graph.getList( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { list, limit, cursor } = params - const { services, db } = ctx - const { ref } = db.db.dynamic - - const graphService = ctx.services.appView.graph(ctx.db) - - const listRes = await graphService - .getListsQb(requester) - .where('list.uri', '=', list) - .executeTakeFirst() - if (!listRes) { - throw new InvalidRequestError(`List not found: ${list}`) - } - - let itemsReq = graphService - .getListItemsQb() - .where('list_item.listUri', '=', list) - .where('list_item.creator', '=', listRes.creator) - - const keyset = new TimeCidKeyset( - ref('list_item.createdAt'), - ref('list_item.cid'), - ) - itemsReq = paginate(itemsReq, { - limit, - cursor, - keyset, - }) - const itemsRes = await itemsReq.execute() - - const actorService = services.appView.actor(db) - const profiles = await actorService.views.profile(itemsRes, requester) - const profilesMap = profiles.reduce( - (acc, cur) => ({ - ...acc, - [cur.did]: cur, - }), - {} as Record, - ) - - const items = itemsRes.map((item) => ({ - subject: profilesMap[item.did], - })) - - const creator = await actorService.views.profile(listRes, requester) - - const subject = { - uri: listRes.uri, - creator, - name: listRes.name, - purpose: listRes.purpose, - description: listRes.description ?? undefined, - descriptionFacets: listRes.descriptionFacets - ? JSON.parse(listRes.descriptionFacets) - : undefined, - avatar: listRes.avatarCid - ? ctx.imgUriBuilder.getCommonSignedUri('avatar', listRes.avatarCid) - : undefined, - indexedAt: listRes.indexedAt, - viewer: { - muted: !!listRes.viewerMuted, - }, - } - - return { - encoding: 'application/json', - body: { - items, - list: subject, - cursor: keyset.packFromResult(itemsRes), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts b/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts deleted file mode 100644 index e3a486ccf0b..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Server } from '../../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../../db/pagination' -import AppContext from '../../../../../context' -import { ProfileView } from '../../../../../lexicon/types/app/bsky/actor/defs' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.graph.getListMutes({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.graph.getListMutes( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { limit, cursor } = params - const { db } = ctx - const { ref } = db.db.dynamic - - const graphService = ctx.services.appView.graph(ctx.db) - - let listsReq = graphService - .getListsQb(requester) - .whereExists( - ctx.db.db - .selectFrom('list_mute') - .where('list_mute.mutedByDid', '=', requester) - .whereRef('list_mute.listUri', '=', ref('list.uri')) - .selectAll(), - ) - - const keyset = new TimeCidKeyset(ref('list.createdAt'), ref('list.cid')) - listsReq = paginate(listsReq, { - limit, - cursor, - keyset, - }) - const listsRes = await listsReq.execute() - - const actorService = ctx.services.appView.actor(ctx.db) - const profiles = await actorService.views.profile(listsRes, requester) - const profilesMap = profiles.reduce( - (acc, cur) => ({ - ...acc, - [cur.did]: cur, - }), - {} as Record, - ) - - const lists = listsRes.map((row) => - graphService.formatListView(row, profilesMap), - ) - - return { - encoding: 'application/json', - body: { - lists, - cursor: keyset.packFromResult(listsRes), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts b/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts deleted file mode 100644 index cb77e2bf2e8..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../../db/pagination' -import AppContext from '../../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.graph.getLists({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.graph.getLists( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { actor, limit, cursor } = params - const { services, db } = ctx - const { ref } = db.db.dynamic - - const actorService = services.appView.actor(db) - const graphService = services.appView.graph(db) - - const creatorRes = await actorService.getActor(actor) - if (!creatorRes) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } - - let listsReq = graphService - .getListsQb(requester) - .where('list.creator', '=', creatorRes.did) - - const keyset = new TimeCidKeyset(ref('list.createdAt'), ref('list.cid')) - listsReq = paginate(listsReq, { - limit, - cursor, - keyset, - }) - - const [listsRes, creator] = await Promise.all([ - listsReq.execute(), - actorService.views.profile(creatorRes, requester), - ]) - const profileMap = { - [creator.did]: creator, - } - - const lists = listsRes.map((row) => - graphService.formatListView(row, profileMap), - ) - - return { - encoding: 'application/json', - body: { - lists, - cursor: keyset.packFromResult(listsRes), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts b/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts deleted file mode 100644 index 98218309800..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Server } from '../../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../../db/pagination' -import AppContext from '../../../../../context' -import { notSoftDeletedClause } from '../../../../../db/util' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.graph.getMutes({ - auth: ctx.accessVerifier, - handler: async ({ req, auth, params }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.graph.getMutes( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { limit, cursor } = params - const { services, db } = ctx - const { ref } = ctx.db.db.dynamic - - let mutesReq = ctx.db.db - .selectFrom('mute') - .innerJoin('did_handle as actor', 'actor.did', 'mute.did') - .innerJoin('repo_root', 'repo_root.did', 'mute.did') - .where(notSoftDeletedClause(ref('repo_root'))) - .where('mute.mutedByDid', '=', requester) - .selectAll('actor') - .select('mute.createdAt as createdAt') - - const keyset = new CreatedAtDidKeyset( - ref('mute.createdAt'), - ref('mute.did'), - ) - mutesReq = paginate(mutesReq, { - limit, - cursor, - keyset, - }) - - const mutesRes = await mutesReq.execute() - - // @NOTE calling into app-view, will eventually be replaced - const actorService = services.appView.actor(db) - - return { - encoding: 'application/json', - body: { - cursor: keyset.packFromResult(mutesRes), - mutes: await actorService.views.profile(mutesRes, requester), - }, - } - }, - }) -} - -export class CreatedAtDidKeyset extends TimeCidKeyset<{ - createdAt: string - did: string // dids are treated identically to cids in TimeCidKeyset -}> { - labelResult(result: { createdAt: string; did: string }) { - return { primary: result.createdAt, secondary: result.did } - } -} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/muteActor.ts b/packages/pds/src/app-view/api/app/bsky/graph/muteActor.ts deleted file mode 100644 index ce3dfcf4125..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/graph/muteActor.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.graph.muteActor({ - auth: ctx.accessVerifier, - handler: async ({ auth, input }) => { - const { actor } = input.body - const requester = auth.credentials.did - const { db, services } = ctx - - const subject = await services.account(db).getAccount(actor) - if (!subject) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } - if (subject.did === requester) { - throw new InvalidRequestError('Cannot mute oneself') - } - - await services.account(db).mute({ - did: subject.did, - mutedByDid: requester, - }) - - if (ctx.cfg.bskyAppViewEndpoint) { - await ctx.appviewAgent.api.app.bsky.graph.muteActor(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), - encoding: 'application/json', - }) - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts b/packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts deleted file mode 100644 index 982a652f7b8..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Server } from '../../../../../lexicon' -import * as lex from '../../../../../lexicon/lexicons' -import AppContext from '../../../../../context' -import { AtUri } from '@atproto/uri' -import { InvalidRequestError } from '@atproto/xrpc-server' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.graph.muteActorList({ - auth: ctx.accessVerifier, - handler: async ({ auth, input }) => { - const { list } = input.body - const requester = auth.credentials.did - - const listUri = new AtUri(list) - const collId = lex.ids.AppBskyGraphList - if (listUri.collection !== collId) { - throw new InvalidRequestError(`Invalid collection: expected: ${collId}`) - } - - await ctx.services.account(ctx.db).muteActorList({ - list, - mutedByDid: requester, - }) - - if (ctx.cfg.bskyAppViewEndpoint) { - await ctx.appviewAgent.api.app.bsky.graph.muteActorList(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), - encoding: 'application/json', - }) - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/unmuteActor.ts b/packages/pds/src/app-view/api/app/bsky/graph/unmuteActor.ts deleted file mode 100644 index ae9e8dac742..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/graph/unmuteActor.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Server } from '../../../../../lexicon' -import { InvalidRequestError } from '@atproto/xrpc-server' -import AppContext from '../../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.graph.unmuteActor({ - auth: ctx.accessVerifier, - handler: async ({ auth, input }) => { - const { actor } = input.body - const requester = auth.credentials.did - const { db, services } = ctx - - const subject = await services.account(db).getAccount(actor) - if (!subject) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } - - await services.account(db).unmute({ - did: subject.did, - mutedByDid: requester, - }) - - if (ctx.cfg.bskyAppViewEndpoint) { - await ctx.appviewAgent.api.app.bsky.graph.unmuteActor(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), - encoding: 'application/json', - }) - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/graph/unmuteActorList.ts b/packages/pds/src/app-view/api/app/bsky/graph/unmuteActorList.ts deleted file mode 100644 index 69012072100..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/graph/unmuteActorList.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Server } from '../../../../../lexicon' -import AppContext from '../../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.graph.unmuteActorList({ - auth: ctx.accessVerifier, - handler: async ({ auth, input }) => { - const { list } = input.body - const requester = auth.credentials.did - - await ctx.services.account(ctx.db).unmuteActorList({ - list, - mutedByDid: requester, - }) - - if (ctx.cfg.bskyAppViewEndpoint) { - await ctx.appviewAgent.api.app.bsky.graph.unmuteActorList(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), - encoding: 'application/json', - }) - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/index.ts b/packages/pds/src/app-view/api/app/bsky/index.ts deleted file mode 100644 index 9d895ea6667..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/index.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' -import getTimeline from './feed/getTimeline' -import getActorFeeds from './feed/getActorFeeds' -import getAuthorFeed from './feed/getAuthorFeed' -import getFeedGenerator from './feed/getFeedGenerator' -import getFeedGenerators from './feed/getFeedGenerators' -import describeFeedGenerator from './feed/describeFeedGenerator' -import getFeed from './feed/getFeed' -import getLikes from './feed/getLikes' -import getPostThread from './feed/getPostThread' -import getPosts from './feed/getPosts' -import getProfile from './actor/getProfile' -import getProfiles from './actor/getProfiles' -import getRepostedBy from './feed/getRepostedBy' -import getBlocks from './graph/getBlocks' -import getFollowers from './graph/getFollowers' -import getFollows from './graph/getFollows' -import getList from './graph/getList' -import getListMutes from './graph/getListMutes' -import getLists from './graph/getLists' -import getMutes from './graph/getMutes' -import muteActor from './graph/muteActor' -import muteActorList from './graph/muteActorList' -import unmuteActor from './graph/unmuteActor' -import unmuteActorList from './graph/unmuteActorList' -import getUsersSearch from './actor/searchActors' -import getUsersTypeahead from './actor/searchActorsTypeahead' -import getSuggestions from './actor/getSuggestions' -import listNotifications from './notification/listNotifications' -import getUnreadCount from './notification/getUnreadCount' -import updateSeen from './notification/updateSeen' -import unspecced from './unspecced' - -export default function (server: Server, ctx: AppContext) { - getTimeline(server, ctx) - getActorFeeds(server, ctx) - getAuthorFeed(server, ctx) - getFeedGenerator(server, ctx) - getFeedGenerators(server, ctx) - describeFeedGenerator(server, ctx) - getFeed(server, ctx) - getLikes(server, ctx) - getPostThread(server, ctx) - getPosts(server, ctx) - getProfile(server, ctx) - getProfiles(server, ctx) - getRepostedBy(server, ctx) - getBlocks(server, ctx) - getFollowers(server, ctx) - getFollows(server, ctx) - getList(server, ctx) - getListMutes(server, ctx) - getLists(server, ctx) - getMutes(server, ctx) - muteActor(server, ctx) - muteActorList(server, ctx) - unmuteActor(server, ctx) - unmuteActorList(server, ctx) - getUsersSearch(server, ctx) - getUsersTypeahead(server, ctx) - getSuggestions(server, ctx) - listNotifications(server, ctx) - getUnreadCount(server, ctx) - updateSeen(server, ctx) - unspecced(server, ctx) -} diff --git a/packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts b/packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts deleted file mode 100644 index e16e2be46ce..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../../lexicon' -import { countAll, notSoftDeletedClause } from '../../../../../db/util' -import AppContext from '../../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.notification.getUnreadCount({ - auth: ctx.accessVerifier, - handler: async ({ req, auth, params }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = - await ctx.appviewAgent.api.app.bsky.notification.getUnreadCount( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { seenAt } = params - const { ref } = ctx.db.db.dynamic - if (seenAt) { - throw new InvalidRequestError('The seenAt parameter is unsupported') - } - - const accountService = ctx.services.account(ctx.db) - - const result = await ctx.db.db - .selectFrom('user_notification as notif') - .select(countAll.as('count')) - .innerJoin('user_account', 'user_account.did', 'notif.userDid') - .innerJoin('user_state', 'user_state.did', 'user_account.did') - .innerJoin( - 'repo_root as author_repo', - 'author_repo.did', - 'notif.author', - ) - .innerJoin('record', 'record.uri', 'notif.recordUri') - .where(notSoftDeletedClause(ref('author_repo'))) - .where(notSoftDeletedClause(ref('record'))) - .where('notif.userDid', '=', requester) - .where((qb) => - accountService.whereNotMuted(qb, requester, [ref('notif.author')]), - ) - .whereRef('notif.indexedAt', '>', 'user_state.lastSeenNotifs') - .executeTakeFirst() - - const count = result?.count ?? 0 - - return { - encoding: 'application/json', - body: { count }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts b/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts deleted file mode 100644 index c9ec4885e74..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { sql } from 'kysely' -import { InvalidRequestError } from '@atproto/xrpc-server' -import * as common from '@atproto/common' -import { Server } from '../../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../../db/pagination' -import AppContext from '../../../../../context' -import { notSoftDeletedClause, valuesList } from '../../../../../db/util' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.notification.listNotifications({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const res = - await ctx.appviewAgent.api.app.bsky.notification.listNotifications( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { limit, cursor, seenAt } = params - const { ref } = ctx.db.db.dynamic - if (seenAt) { - throw new InvalidRequestError('The seenAt parameter is unsupported') - } - - const accountService = ctx.services.account(ctx.db) - const graphService = ctx.services.appView.graph(ctx.db) - - let notifBuilder = ctx.db.db - .selectFrom('user_notification as notif') - .innerJoin('did_handle as author', 'author.did', 'notif.author') - .innerJoin( - 'repo_root as author_repo', - 'author_repo.did', - 'notif.author', - ) - .innerJoin('record', 'record.uri', 'notif.recordUri') - .where(notSoftDeletedClause(ref('author_repo'))) - .where(notSoftDeletedClause(ref('record'))) - .where('notif.userDid', '=', requester) - .where((qb) => - accountService.whereNotMuted(qb, requester, [ref('notif.author')]), - ) - .whereNotExists(graphService.blockQb(requester, [ref('notif.author')])) - .where((clause) => - clause - .where('reasonSubject', 'is', null) - .orWhereExists( - ctx.db.db - .selectFrom('record as subject') - .selectAll() - .whereRef('subject.uri', '=', ref('notif.reasonSubject')), - ), - ) - .select([ - 'notif.recordUri as uri', - 'notif.recordCid as cid', - 'author.did as authorDid', - 'author.handle as authorHandle', - 'notif.reason as reason', - 'notif.reasonSubject as reasonSubject', - 'notif.indexedAt as indexedAt', - ]) - - const keyset = new NotifsKeyset( - ref('notif.indexedAt'), - ref('notif.recordCid'), - ) - notifBuilder = paginate(notifBuilder, { - cursor, - limit, - keyset, - }) - - const userStateQuery = ctx.db.db - .selectFrom('user_state') - .selectAll() - .where('did', '=', requester) - .executeTakeFirst() - - const [userState, notifs] = await Promise.all([ - userStateQuery, - notifBuilder.execute(), - ]) - - if (!userState) { - throw new InvalidRequestError(`Could not find user: ${requester}`) - } - - const recordTuples = notifs.map((notif) => { - return sql`${notif.authorDid}, ${notif.cid}` - }) - - const emptyBlocksResult: { cid: string; bytes: Uint8Array }[] = [] - const blocksQb = recordTuples.length - ? ctx.db.db - .selectFrom('ipld_block') - .whereRef(sql`(creator, cid)`, 'in', valuesList(recordTuples)) - .select(['cid', 'content as bytes']) - : null - - const actorService = ctx.services.appView.actor(ctx.db) - - // @NOTE calling into app-view, will eventually be replaced - const labelService = ctx.services.appView.label(ctx.db) - const recordUris = notifs.map((notif) => notif.uri) - const [blocks, authors, labels] = await Promise.all([ - blocksQb ? blocksQb.execute() : emptyBlocksResult, - actorService.views.profile( - notifs.map((notif) => ({ - did: notif.authorDid, - handle: notif.authorHandle, - })), - requester, - ), - labelService.getLabelsForUris(recordUris), - ]) - - const bytesByCid = blocks.reduce((acc, block) => { - acc[block.cid] = block.bytes - return acc - }, {} as Record) - - const notifications = notifs.flatMap((notif, i) => { - const bytes = bytesByCid[notif.cid] - if (!bytes) return [] // Filter out - return { - uri: notif.uri, - cid: notif.cid, - author: authors[i], - reason: notif.reason, - reasonSubject: notif.reasonSubject || undefined, - record: common.cborBytesToRecord(bytes), - isRead: notif.indexedAt <= userState.lastSeenNotifs, - indexedAt: notif.indexedAt, - labels: labels[notif.uri] ?? [], - } - }) - - return { - encoding: 'application/json', - body: { - notifications, - cursor: keyset.packFromResult(notifs), - }, - } - }, - }) -} - -type NotifRow = { indexedAt: string; cid: string } -class NotifsKeyset extends TimeCidKeyset { - labelResult(result: NotifRow) { - return { primary: result.indexedAt, secondary: result.cid } - } -} diff --git a/packages/pds/src/app-view/api/app/bsky/notification/updateSeen.ts b/packages/pds/src/app-view/api/app/bsky/notification/updateSeen.ts deleted file mode 100644 index 717f358f77c..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/notification/updateSeen.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Server } from '../../../../../lexicon' -import { InvalidRequestError } from '@atproto/xrpc-server' -import AppContext from '../../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.notification.updateSeen({ - auth: ctx.accessVerifier, - handler: async ({ input, auth }) => { - const { seenAt } = input.body - const requester = auth.credentials.did - - let parsed: string - try { - parsed = new Date(seenAt).toISOString() - } catch (_err) { - throw new InvalidRequestError('Invalid date') - } - - const user = await ctx.services.account(ctx.db).getAccount(requester) - if (!user) { - throw new InvalidRequestError(`Could not find user: ${requester}`) - } - - await ctx.db.db - .updateTable('user_state') - .set({ lastSeenNotifs: parsed }) - .where('did', '=', user.did) - .executeTakeFirst() - - if (ctx.cfg.bskyAppViewEndpoint) { - await ctx.appviewAgent.api.app.bsky.notification.updateSeen( - input.body, - { - ...(await ctx.serviceAuthHeaders(requester)), - encoding: 'application/json', - }, - ) - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/unspecced.ts b/packages/pds/src/app-view/api/app/bsky/unspecced.ts deleted file mode 100644 index defa197b709..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/unspecced.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { Server } from '../../../../lexicon' -import { FeedKeyset } from './util/feed' -import { paginate } from '../../../../db/pagination' -import AppContext from '../../../../context' -import { FeedRow } from '../../../services/feed' -import { isPostView } from '../../../../lexicon/types/app/bsky/feed/defs' -import { NotEmptyArray } from '@atproto/common' -import { isViewRecord } from '../../../../lexicon/types/app/bsky/embed/record' -import { countAll, valuesList } from '../../../../db/util' - -const NO_WHATS_HOT_LABELS: NotEmptyArray = [ - '!no-promote', - 'corpse', - 'self-harm', -] - -const NSFW_LABELS = ['porn', 'sexual', 'nudity', 'underwear'] - -// @NOTE currently relies on the hot-classic feed being configured on the pds -// THIS IS A TEMPORARY UNSPECCED ROUTE -export default function (server: Server, ctx: AppContext) { - server.app.bsky.unspecced.getPopular({ - auth: ctx.accessVerifier, - handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did - if (ctx.canProxy(req)) { - const hotClassicUri = Object.keys(ctx.algos).find((uri) => - uri.endsWith('/hot-classic'), - ) - if (!hotClassicUri) { - return { - encoding: 'application/json', - body: { feed: [] }, - } - } - const { data: feed } = - await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( - { feed: hotClassicUri }, - await ctx.serviceAuthHeaders(requester), - ) - const res = await ctx.appviewAgent.api.app.bsky.feed.getFeed( - { ...params, feed: hotClassicUri }, - await ctx.serviceAuthHeaders(requester, feed.view.did), - ) - return { - encoding: 'application/json', - body: res.data, - } - } - - const { limit, cursor, includeNsfw } = params - const db = ctx.db.db - const { ref } = db.dynamic - - const accountService = ctx.services.account(ctx.db) - const feedService = ctx.services.appView.feed(ctx.db) - const graphService = ctx.services.appView.graph(ctx.db) - - const labelsToFilter = includeNsfw - ? NO_WHATS_HOT_LABELS - : [...NO_WHATS_HOT_LABELS, ...NSFW_LABELS] - - const postsQb = feedService - .selectPostQb() - .leftJoin('post_agg', 'post_agg.uri', 'post.uri') - .where('post_agg.likeCount', '>=', 12) - .where('post.replyParent', 'is', null) - .whereNotExists((qb) => - qb - .selectFrom('label') - .selectAll() - .whereRef('val', 'in', valuesList(labelsToFilter)) - .where('neg', '=', 0) - .where((clause) => - clause - .whereRef('label.uri', '=', ref('post.creator')) - .orWhereRef('label.uri', '=', ref('post.uri')), - ), - ) - .where((qb) => - accountService.whereNotMuted(qb, requester, [ref('post.creator')]), - ) - .whereNotExists(graphService.blockQb(requester, [ref('post.creator')])) - - const keyset = new FeedKeyset(ref('sortAt'), ref('cid')) - - let feedQb = ctx.db.db.selectFrom(postsQb.as('feed_items')).selectAll() - feedQb = paginate(feedQb, { limit, cursor, keyset }) - - const feedItems: FeedRow[] = await feedQb.execute() - const feed = await feedService.hydrateFeed(feedItems, requester) - - // filter out any quote post where the internal post has a filtered label - const noLabeledQuotePosts = feed.filter((post) => { - const quoteView = post.post.embed?.record - if (!quoteView || !isViewRecord(quoteView)) return true - for (const label of quoteView.labels || []) { - if (labelsToFilter.includes(label.val)) return false - } - return true - }) - - // remove record embeds in our response - const noRecordEmbeds = noLabeledQuotePosts.map((post) => { - delete post.post.record['embed'] - if (post.reply) { - if (isPostView(post.reply.parent)) { - delete post.reply.parent.record['embed'] - } - if (isPostView(post.reply.root)) { - delete post.reply.root.record['embed'] - } - } - return post - }) - - return { - encoding: 'application/json', - body: { - feed: noRecordEmbeds, - cursor: keyset.packFromResult(feedItems), - }, - } - }, - }) - - server.app.bsky.unspecced.getPopularFeedGenerators({ - auth: ctx.accessVerifier, - handler: async ({ auth }) => { - const requester = auth.credentials.did - const db = ctx.db.db - const { ref } = db.dynamic - const feedService = ctx.services.appView.feed(ctx.db) - - const mostPopularFeeds = await ctx.db.db - .selectFrom('feed_generator') - .select([ - 'uri', - ctx.db.db - .selectFrom('like') - .whereRef('like.subject', '=', ref('feed_generator.uri')) - .select(countAll.as('count')) - .as('likeCount'), - ]) - .orderBy('likeCount', 'desc') - .orderBy('cid', 'desc') - .limit(50) - .execute() - - const genViews = await feedService.getFeedGeneratorViews( - mostPopularFeeds.map((feed) => feed.uri), - requester, - ) - - const genList = Object.values(genViews) - const creators = genList.map((gen) => gen.creator) - const profiles = await feedService.getActorViews(creators, requester) - - const feedViews = genList.map((gen) => - feedService.views.formatFeedGeneratorView(gen, profiles), - ) - - return { - encoding: 'application/json', - body: { - feeds: feedViews.sort((feedA, feedB) => { - const likeA = feedA.likeCount ?? 0 - const likeB = feedB.likeCount ?? 0 - const likeDiff = likeB - likeA - if (likeDiff !== 0) return likeDiff - return feedB.cid.localeCompare(feedA.cid) - }), - }, - } - }, - }) -} diff --git a/packages/pds/src/app-view/api/app/bsky/util/feed.ts b/packages/pds/src/app-view/api/app/bsky/util/feed.ts deleted file mode 100644 index da350cab4a3..00000000000 --- a/packages/pds/src/app-view/api/app/bsky/util/feed.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { TimeCidKeyset } from '../../../../../db/pagination' -import { FeedRow } from '../../../../services/feed' - -export enum FeedAlgorithm { - ReverseChronological = 'reverse-chronological', -} - -export class FeedKeyset extends TimeCidKeyset { - labelResult(result: FeedRow) { - return { primary: result.sortAt, secondary: result.cid } - } -} - -// For users with sparse feeds, avoid scanning more than one week for a single page -export const getFeedDateThreshold = (from: string | undefined, days = 7) => { - const timelineDateThreshold = from ? new Date(from) : new Date() - timelineDateThreshold.setDate(timelineDateThreshold.getDate() - days) - return timelineDateThreshold.toISOString() -} diff --git a/packages/pds/src/app-view/api/index.ts b/packages/pds/src/app-view/api/index.ts deleted file mode 100644 index 1f963088637..00000000000 --- a/packages/pds/src/app-view/api/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Server } from '../../lexicon' -import appBsky from './app/bsky' -import AppContext from '../../context' - -export default function (server: Server, ctx: AppContext) { - appBsky(server, ctx) - return server -} diff --git a/packages/pds/src/db/database-schema.ts b/packages/pds/src/db/database-schema.ts index 03ff0e2eca4..97c1031a4c4 100644 --- a/packages/pds/src/db/database-schema.ts +++ b/packages/pds/src/db/database-schema.ts @@ -23,10 +23,8 @@ 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 appView from '../app-view/db' -export type DatabaseSchemaType = appView.DatabaseSchemaType & - appMigration.PartialDB & +export type DatabaseSchemaType = appMigration.PartialDB & userAccount.PartialDB & userState.PartialDB & userPref.PartialDB & From e600fabf64a18b9b690bf52b53c48736f229f355 Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 12 Jun 2023 17:57:13 -0500 Subject: [PATCH 005/105] delete more tables --- packages/pds/src/app-view/logger.ts | 3 --- packages/pds/src/db/database-schema.ts | 10 --------- packages/pds/src/db/tables/label.ts | 12 ----------- packages/pds/src/db/tables/list-mute.ts | 9 -------- packages/pds/src/db/tables/mute.ts | 9 -------- .../pds/src/db/tables/user-notification.ts | 21 ------------------- packages/pds/src/db/tables/user-state.ts | 8 ------- 7 files changed, 72 deletions(-) delete mode 100644 packages/pds/src/app-view/logger.ts delete mode 100644 packages/pds/src/db/tables/label.ts delete mode 100644 packages/pds/src/db/tables/list-mute.ts delete mode 100644 packages/pds/src/db/tables/mute.ts delete mode 100644 packages/pds/src/db/tables/user-notification.ts delete mode 100644 packages/pds/src/db/tables/user-state.ts 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/db/database-schema.ts b/packages/pds/src/db/database-schema.ts index 97c1031a4c4..c9b481e1858 100644 --- a/packages/pds/src/db/database-schema.ts +++ b/packages/pds/src/db/database-schema.ts @@ -1,6 +1,5 @@ import { Kysely } from 'kysely' import * as userAccount from './tables/user-account' -import * as userState from './tables/user-state' import * as userPref from './tables/user-pref' import * as didHandle from './tables/did-handle' import * as repoRoot from './tables/repo-root' @@ -13,20 +12,15 @@ import * as repoCommitBlock from './tables/repo-commit-block' import * as repoCommitHistory from './tables/repo-commit-history' import * as ipldBlock from './tables/ipld-block' import * as inviteCode from './tables/invite-code' -import * as notification from './tables/user-notification' import * as blob from './tables/blob' import * as repoBlob from './tables/repo-blob' 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' export type DatabaseSchemaType = appMigration.PartialDB & userAccount.PartialDB & - userState.PartialDB & userPref.PartialDB & didHandle.PartialDB & refreshToken.PartialDB & @@ -41,14 +35,10 @@ export type DatabaseSchemaType = appMigration.PartialDB & repoCommitBlock.PartialDB & repoCommitHistory.PartialDB & inviteCode.PartialDB & - notification.PartialDB & blob.PartialDB & repoBlob.PartialDB & deleteAccountToken.PartialDB & moderation.PartialDB & - mute.PartialDB & - listMute.PartialDB & - label.PartialDB & repoSeq.PartialDB export type DatabaseSchema = Kysely 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/db/tables/list-mute.ts b/packages/pds/src/db/tables/list-mute.ts deleted file mode 100644 index 6678035805b..00000000000 --- a/packages/pds/src/db/tables/list-mute.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const tableName = 'list_mute' - -export interface ListMute { - listUri: string - mutedByDid: string - createdAt: string -} - -export type PartialDB = { [tableName]: ListMute } diff --git a/packages/pds/src/db/tables/mute.ts b/packages/pds/src/db/tables/mute.ts deleted file mode 100644 index 4790c7aeced..00000000000 --- a/packages/pds/src/db/tables/mute.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface Mute { - did: string - mutedByDid: string - createdAt: string -} - -export const tableName = 'mute' - -export type PartialDB = { [tableName]: Mute } diff --git a/packages/pds/src/db/tables/user-notification.ts b/packages/pds/src/db/tables/user-notification.ts deleted file mode 100644 index 547d27678f7..00000000000 --- a/packages/pds/src/db/tables/user-notification.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const tableName = 'user_notification' - -export type NotificationReason = - | 'like' - | 'repost' - | 'follow' - | 'mention' - | 'reply' - | 'quote' - -export interface UserNotification { - userDid: string - recordUri: string - recordCid: string - author: string - reason: NotificationReason - reasonSubject: string | null - indexedAt: string -} - -export type PartialDB = { [tableName]: UserNotification } diff --git a/packages/pds/src/db/tables/user-state.ts b/packages/pds/src/db/tables/user-state.ts deleted file mode 100644 index bbd2b0e1e07..00000000000 --- a/packages/pds/src/db/tables/user-state.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface UserState { - did: string - lastSeenNotifs: string -} - -export const tableName = 'user_state' - -export type PartialDB = { [tableName]: UserState } From dc240c263a533178890b652aa08f92642cb90392 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Mon, 12 Jun 2023 18:58:33 -0400 Subject: [PATCH 006/105] Start removing message dispatched from pds --- .../background-queue.ts => background.ts} | 4 +- .../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/labeler/base.ts | 2 +- packages/pds/src/labeler/hive.ts | 2 +- packages/pds/src/labeler/keyword.ts | 2 +- packages/pds/src/services/account/index.ts | 23 +-------- packages/pds/src/services/index.ts | 9 +--- packages/pds/src/services/moderation/index.ts | 15 ++---- packages/pds/src/services/moderation/views.ts | 5 +- packages/pds/src/services/record/index.ts | 31 +++-------- packages/pds/src/services/repo/blobs.ts | 2 +- packages/pds/src/services/repo/index.ts | 9 +--- 14 files changed, 23 insertions(+), 218 deletions(-) rename packages/pds/src/{event-stream/background-queue.ts => background.ts} (92%) 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 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 d23c8a49a38..a66ecf887b8 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/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 1fb07dc609b..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/uri' -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/labeler/base.ts b/packages/pds/src/labeler/base.ts index c2e54513fba..3beb85966a6 100644 --- a/packages/pds/src/labeler/base.ts +++ b/packages/pds/src/labeler/base.ts @@ -4,7 +4,7 @@ import { BlobStore, cidForRecord } from '@atproto/repo' import { dedupe, getFieldsFromRecord } from './util' import { AtUri } from '@atproto/uri' import { labelerLogger as log } from '../logger' -import { BackgroundQueue } from '../event-stream/background-queue' +import { BackgroundQueue } from '../background' export abstract class Labeler { public db: Database diff --git a/packages/pds/src/labeler/hive.ts b/packages/pds/src/labeler/hive.ts index 41bddf572a4..771f76a502a 100644 --- a/packages/pds/src/labeler/hive.ts +++ b/packages/pds/src/labeler/hive.ts @@ -5,7 +5,7 @@ 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 { BackgroundQueue } from '../background' const HIVE_ENDPOINT = 'https://api.thehive.ai/api/v2/task/sync' diff --git a/packages/pds/src/labeler/keyword.ts b/packages/pds/src/labeler/keyword.ts index 8838e87486a..0a6bf9ca3d2 100644 --- a/packages/pds/src/labeler/keyword.ts +++ b/packages/pds/src/labeler/keyword.ts @@ -2,7 +2,7 @@ import { BlobStore } from '@atproto/repo' import Database from '../db' import { Labeler } from './base' import { keywordLabeling } from './util' -import { BackgroundQueue } from '../event-stream/background-queue' +import { BackgroundQueue } from '../background' export class KeywordLabeler extends Labeler { keywords: Record diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts index d83e741594e..56f387fd8c3 100644 --- a/packages/pds/src/services/account/index.ts +++ b/packages/pds/src/services/account/index.ts @@ -1,4 +1,4 @@ -import { SelectQueryBuilder, WhereInterface, sql } from 'kysely' +import { WhereInterface, sql } from 'kysely' import { dbLogger as log } from '../../logger' import Database from '../../db' import * as scrypt from '../../db/scrypt' @@ -316,27 +316,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 diff --git a/packages/pds/src/services/index.ts b/packages/pds/src/services/index.ts index 50566ad73ed..449c9429f37 100644 --- a/packages/pds/src/services/index.ts +++ b/packages/pds/src/services/index.ts @@ -1,7 +1,6 @@ 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' @@ -11,12 +10,11 @@ import { RepoService } from './repo' import { ModerationService } from './moderation' import { LabelService } from './label' import { Labeler } from '../labeler' -import { BackgroundQueue } from '../event-stream/background-queue' +import { BackgroundQueue } from '../background' import { Crawlers } from '../crawlers' export function createServices(resources: { repoSigningKey: crypto.Keypair - messageDispatcher: MessageDispatcher blobstore: BlobStore imgUriBuilder: ImageUriBuilder imgInvalidator: ImageInvalidator @@ -26,7 +24,6 @@ export function createServices(resources: { }): Services { const { repoSigningKey, - messageDispatcher, blobstore, imgUriBuilder, imgInvalidator, @@ -37,17 +34,15 @@ 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, ), moderation: ModerationService.creator( - messageDispatcher, blobstore, imgUriBuilder, imgInvalidator, diff --git a/packages/pds/src/services/moderation/index.ts b/packages/pds/src/services/moderation/index.ts index 4a7b3c9eefc..8700ec9432d 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/uri' 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' @@ -15,32 +14,24 @@ import { ImageUriBuilder } from '../../image/uri' export class ModerationService { constructor( 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, ) { return (db: Database) => - new ModerationService( - db, - messageDispatcher, - blobstore, - imgUriBuilder, - imgInvalidator, - ) + new ModerationService(db, blobstore, imgUriBuilder, imgInvalidator) } - 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 35b597f70d9..f51372d701b 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/uri' 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 { @@ -24,11 +23,11 @@ import { RecordService } from '../record' import { ModerationReportRowWithHandle } from '.' 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): Promise diff --git a/packages/pds/src/services/record/index.ts b/packages/pds/src/services/record/index.ts index 5dd94914d43..e7e2f97f7fa 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( @@ -66,31 +60,20 @@ 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') + await this.db.db + .deleteFrom('backlink') .where('uri', '=', uri.toString()) .execute() await this.db.db - .deleteFrom('backlink') + .deleteFrom('record') .where('uri', '=', uri.toString()) .execute() - await Promise.all([ - this.messageDispatcher.send(this.db, deleteRecord(uri, cascading)), - deleteQuery, - ]) - log.info({ uri }, 'deleted indexed record') } diff --git a/packages/pds/src/services/repo/blobs.ts b/packages/pds/src/services/repo/blobs.ts index d0e92cd0298..acb89a2b9b1 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 a6e2a010c45..9d44f264afd 100644 --- a/packages/pds/src/services/repo/index.ts +++ b/packages/pds/src/services/repo/index.ts @@ -13,7 +13,6 @@ import * as repo from '@atproto/repo' import { AtUri } from '@atproto/uri' import { InvalidRequestError } from '@atproto/xrpc-server' import Database from '../../db' -import { MessageQueue } from '../../event-stream/types' import SqlRepoStorage from '../../sql-repo-storage' import { BadCommitSwapError, @@ -27,7 +26,7 @@ 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 { countAll } from '../../db/util' import { Crawlers } from '../../crawlers' @@ -37,7 +36,6 @@ export class RepoService { constructor( public db: Database, public repoSigningKey: crypto.Keypair, - public messageDispatcher: MessageQueue, public blobstore: BlobStore, public backgroundQueue: BackgroundQueue, public crawlers: Crawlers, @@ -48,7 +46,6 @@ export class RepoService { static creator( keypair: crypto.Keypair, - messageDispatcher: MessageQueue, blobstore: BlobStore, backgroundQueue: BackgroundQueue, crawlers: Crawlers, @@ -58,7 +55,6 @@ export class RepoService { new RepoService( db, keypair, - messageDispatcher, blobstore, backgroundQueue, crawlers, @@ -67,7 +63,7 @@ export class RepoService { } services = { - record: RecordService.creator(this.messageDispatcher), + record: RecordService.creator(), } private async serviceTx( @@ -78,7 +74,6 @@ export class RepoService { const srvc = new RepoService( dbTxn, this.repoSigningKey, - this.messageDispatcher, this.blobstore, this.backgroundQueue, this.crawlers, From ac359230d8d552c956fe377db4ec55cef71f491f Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Mon, 12 Jun 2023 19:12:53 -0400 Subject: [PATCH 007/105] more syncing-up removal of message dispatcher in pds --- packages/pds/src/context.ts | 8 +- packages/pds/src/db/database-schema.ts | 2 + packages/pds/src/db/tables/label.ts | 12 +++ packages/pds/src/index.ts | 10 +- packages/pds/src/services/account/index.ts | 107 +-------------------- packages/pds/src/services/record/index.ts | 5 - 6 files changed, 20 insertions(+), 124 deletions(-) create mode 100644 packages/pds/src/db/tables/label.ts diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index 39918d150e4..eee69cd23bf 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -11,10 +11,9 @@ import { ServerMailer } from './mailer' 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 { BackgroundQueue } from './background' import DidSqlCache from './did-cache' import { MountedAlgos } from './feed-gen/types' import { Crawlers } from './crawlers' @@ -35,7 +34,6 @@ export class AppContext { cfg: ServerConfig mailer: ServerMailer services: Services - messageDispatcher: MessageDispatcher sequencer: Sequencer sequencerLeader: SequencerLeader labeler: Labeler @@ -111,10 +109,6 @@ export class AppContext { return this.opts.services } - get messageDispatcher(): MessageDispatcher { - return this.opts.messageDispatcher - } - get sequencer(): Sequencer { return this.opts.sequencer } diff --git a/packages/pds/src/db/database-schema.ts b/packages/pds/src/db/database-schema.ts index c9b481e1858..60f556a9fd0 100644 --- a/packages/pds/src/db/database-schema.ts +++ b/packages/pds/src/db/database-schema.ts @@ -16,6 +16,7 @@ import * as blob from './tables/blob' import * as repoBlob from './tables/repo-blob' import * as deleteAccountToken from './tables/delete-account-token' import * as moderation from './tables/moderation' +import * as label from './tables/label' import * as repoSeq from './tables/repo-seq' import * as appMigration from './tables/app-migration' @@ -39,6 +40,7 @@ export type DatabaseSchemaType = appMigration.PartialDB & repoBlob.PartialDB & deleteAccountToken.PartialDB & moderation.PartialDB & + label.PartialDB & repoSeq.PartialDB export type DatabaseSchema = Kysely diff --git a/packages/pds/src/db/tables/label.ts b/packages/pds/src/db/tables/label.ts new file mode 100644 index 00000000000..1837faab1c8 --- /dev/null +++ b/packages/pds/src/db/tables/label.ts @@ -0,0 +1,12 @@ +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/index.ts b/packages/pds/src/index.ts index 67f337f2f41..6439735a27c 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -11,8 +11,6 @@ import events from 'events' import { createTransport } from 'nodemailer' import * as crypto from '@atproto/crypto' import { BlobStore } from '@atproto/repo' -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' import * as wellKnown from './well-known' @@ -23,7 +21,6 @@ import { dbLogger, loggerMiddleware } from './logger' import { ServerConfig } from './config' import { ServerMailer } from './mailer' 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' @@ -35,7 +32,7 @@ import { ImageProcessingServerInvalidator, } from './image/invalidator' import { Labeler, HiveLabeler, KeywordLabeler } from './labeler' -import { BackgroundQueue } from './event-stream/background-queue' +import { BackgroundQueue } from './background' import DidSqlCache from './did-cache' import { IdResolver } from '@atproto/identity' import { MountedAlgos } from './feed-gen/types' @@ -96,7 +93,6 @@ export class PDS { ) const idResolver = new IdResolver({ plcUrl: config.didPlcUrl, didCache }) - const messageDispatcher = new MessageDispatcher() const sequencer = new Sequencer(db) const sequencerLeader = new SequencerLeader( db, @@ -171,7 +167,6 @@ export class PDS { const services = createServices({ repoSigningKey, - messageDispatcher, blobstore, imgUriBuilder, imgInvalidator, @@ -189,7 +184,6 @@ export class PDS { didCache, cfg: config, auth, - messageDispatcher, sequencer, sequencerLeader, labeler, @@ -211,7 +205,6 @@ export class PDS { }) server = API(server, ctx) - server = inProcessAppView(server, ctx) app.use(basicRoutes.createRouter(ctx)) app.use(wellKnown.createRouter(ctx)) @@ -247,7 +240,6 @@ export class PDS { ) }, 10000) } - appviewConsumers.listen(this.ctx) this.ctx.sequencerLeader.run() await this.ctx.sequencer.start() await this.ctx.db.startListeningToChannels() diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts index 56f387fd8c3..a2e874baa4f 100644 --- a/packages/pds/src/services/account/index.ts +++ b/packages/pds/src/services/account/index.ts @@ -1,23 +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, - nullToZero, -} from '../../db/util' +import { countAll, notSoftDeletedClause, nullToZero } 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) {} @@ -116,22 +110,12 @@ export class AccountService { .onConflict((oc) => oc.doNothing()) .returning('handle') .executeTakeFirst() - const registerUserState = this.db.db - .insertInto('user_state') - .values({ - did, - lastSeenNotifs: new Date().toISOString(), - }) - .onConflict((oc) => oc.doNothing()) - .returning('did') - .executeTakeFirst() - const [res1, res2, res3] = await Promise.all([ + const [res1, res2] = await Promise.all([ registerUserAccnt, registerDidHandle, - registerUserState, ]) - if (!res1 || !res2 || !res3) { + if (!res1 || !res2) { throw new UserAlreadyExistsError() } log.info({ handle, email, did }, 'registered user') @@ -246,76 +230,6 @@ export class AccountService { .execute() } - async mute(info: { did: string; mutedByDid: string; createdAt?: Date }) { - const { did, mutedByDid, createdAt = new Date() } = info - await this.db.db - .insertInto('mute') - .values({ - did, - mutedByDid, - createdAt: createdAt.toISOString(), - }) - .onConflict((oc) => oc.doNothing()) - .execute() - } - - async unmute(info: { did: string; mutedByDid: string }) { - const { did, mutedByDid } = info - await this.db.db - .deleteFrom('mute') - .where('did', '=', did) - .where('mutedByDid', '=', mutedByDid) - .execute() - } - - async getMute(mutedBy: string, did: string): Promise { - const mutes = await this.getMutes(mutedBy, [did]) - return mutes[did] ?? false - } - - async getMutes( - mutedBy: string, - dids: string[], - ): Promise> { - if (dids.length === 0) return {} - const res = await this.db.db - .selectFrom('mute') - .where('mutedByDid', '=', mutedBy) - .where('did', 'in', dids) - .selectAll() - .execute() - return res.reduce((acc, cur) => { - acc[cur.did] = true - return acc - }, {} as Record) - } - - async muteActorList(info: { - list: string - mutedByDid: string - createdAt?: Date - }) { - const { list, mutedByDid, createdAt = new Date() } = info - await this.db.db - .insertInto('list_mute') - .values({ - listUri: list, - mutedByDid, - createdAt: createdAt.toISOString(), - }) - .onConflict((oc) => oc.doNothing()) - .execute() - } - - async unmuteActorList(info: { list: string; mutedByDid: string }) { - const { list, mutedByDid } = info - await this.db.db - .deleteFrom('list_mute') - .where('listUri', '=', list) - .where('mutedByDid', '=', mutedByDid) - .execute() - } - async search(opts: { searchField?: 'did' | 'handle' term: string @@ -400,10 +314,6 @@ export class AccountService { .deleteFrom('user_account') .where('user_account.did', '=', did) .execute() - await this.db.db - .deleteFrom('user_state') - .where('user_state.did', '=', did) - .execute() await this.db.db .deleteFrom('did_handle') .where('did_handle.did', '=', did) @@ -494,15 +404,6 @@ export class AccountService { }, {} as Record) } - async getLastSeenNotifs(did: string): Promise { - const res = await this.db.db - .selectFrom('user_state') - .where('did', '=', did) - .selectAll() - .executeTakeFirst() - return res?.lastSeenNotifs - } - async getPreferences( did: string, namespace?: string, diff --git a/packages/pds/src/services/record/index.ts b/packages/pds/src/services/record/index.ts index e7e2f97f7fa..4d6b901a1a8 100644 --- a/packages/pds/src/services/record/index.ts +++ b/packages/pds/src/services/record/index.ts @@ -212,12 +212,7 @@ 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') - .where('author', '=', did) - .execute() } async removeBacklinksByUri(uri: AtUri) { From b241fba5c7acfeddeb1df3a8e3a5e7b8a777947c Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 12 Jun 2023 18:14:08 -0500 Subject: [PATCH 008/105] merged --- packages/pds/src/context.ts | 12 -- packages/pds/src/image/invalidator.ts | 26 --- packages/pds/src/image/server.ts | 170 --------------- packages/pds/src/image/uri.ts | 202 ------------------ packages/pds/src/index.ts | 63 ------ packages/pds/src/labeler/base.ts | 75 ------- packages/pds/src/labeler/hive.ts | 192 ----------------- packages/pds/src/labeler/index.ts | 3 - packages/pds/src/labeler/keyword.ts | 29 --- packages/pds/src/labeler/util.ts | 109 ---------- packages/pds/src/services/index.ts | 23 +- packages/pds/src/services/moderation/index.ts | 29 +-- packages/pds/src/services/repo/index.ts | 15 -- 13 files changed, 7 insertions(+), 941 deletions(-) delete mode 100644 packages/pds/src/image/invalidator.ts delete mode 100644 packages/pds/src/image/server.ts delete mode 100644 packages/pds/src/image/uri.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 diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index eee69cd23bf..a01ebc254fb 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -9,10 +9,8 @@ import { ServerConfig } from './config' import * as auth from './auth' import { ServerMailer } from './mailer' import { BlobStore } from '@atproto/repo' -import { ImageUriBuilder } from './image/uri' import { Services } from './services' import { Sequencer, SequencerLeader } from './sequencer' -import { Labeler } from './labeler' import { BackgroundQueue } from './background' import DidSqlCache from './did-cache' import { MountedAlgos } from './feed-gen/types' @@ -30,13 +28,11 @@ export class AppContext { idResolver: IdResolver didCache: DidSqlCache auth: auth.ServerAuth - imgUriBuilder: ImageUriBuilder cfg: ServerConfig mailer: ServerMailer services: Services sequencer: Sequencer sequencerLeader: SequencerLeader - labeler: Labeler backgroundQueue: BackgroundQueue crawlers: Crawlers algos: MountedAlgos @@ -93,10 +89,6 @@ export class AppContext { return auth.moderatorVerifier(this.auth) } - get imgUriBuilder(): ImageUriBuilder { - return this.opts.imgUriBuilder - } - get cfg(): ServerConfig { return this.opts.cfg } @@ -117,10 +109,6 @@ export class AppContext { return this.opts.sequencerLeader } - get labeler(): Labeler { - return this.opts.labeler - } - get backgroundQueue(): BackgroundQueue { return this.opts.backgroundQueue } 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/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/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/index.ts b/packages/pds/src/index.ts index 6439735a27c..c119f834e1f 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -21,17 +21,10 @@ import { dbLogger, loggerMiddleware } from './logger' import { ServerConfig } from './config' import { ServerMailer } from './mailer' import { createServer } from './lexicon' -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 './background' import DidSqlCache from './did-cache' import { IdResolver } from '@atproto/identity' @@ -65,7 +58,6 @@ export class PDS { static create(opts: { db: Database blobstore: BlobStore - imgInvalidator?: ImageInvalidator repoSigningKey: crypto.Keypair plcRotationKey: crypto.Keypair algos?: MountedAlgos @@ -79,7 +71,6 @@ export class PDS { algos = {}, config, } = opts - let maybeImgInvalidator = opts.imgInvalidator const auth = new ServerAuth({ jwtSecret: config.jwtSecret, adminPass: config.adminPassword, @@ -110,67 +101,15 @@ export class PDS { app.use(cors()) app.use(loggerMiddleware) - 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, 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 services = createServices({ repoSigningKey, blobstore, - imgUriBuilder, - imgInvalidator, - labeler, backgroundQueue, crawlers, }) @@ -186,10 +125,8 @@ export class PDS { auth, sequencer, sequencerLeader, - labeler, services, mailer, - imgUriBuilder, backgroundQueue, crawlers, algos, diff --git a/packages/pds/src/labeler/base.ts b/packages/pds/src/labeler/base.ts deleted file mode 100644 index 3beb85966a6..00000000000 --- a/packages/pds/src/labeler/base.ts +++ /dev/null @@ -1,75 +0,0 @@ -import stream from 'stream' -import Database from '../db' -import { BlobStore, cidForRecord } from '@atproto/repo' -import { dedupe, getFieldsFromRecord } from './util' -import { AtUri } from '@atproto/uri' -import { labelerLogger as log } from '../logger' -import { BackgroundQueue } from '../background' - -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) => { - const stream = await this.blobstore.getStream(cid) - return this.labelImg(stream) - }), - ) - return dedupe([...txtLabels, ...imgLabels.flat()]) - } - - abstract labelText(text: string): Promise - abstract labelImg(img: stream.Readable): 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 771f76a502a..00000000000 --- a/packages/pds/src/labeler/hive.ts +++ /dev/null @@ -1,192 +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 '../background' - -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(img: stream.Readable): Promise { - return labelBlob(img, 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) { - // TODO: retaining 'underwear' label for a short time to help understand - // the impact of labeling all "underwear" as "sexual". This *will* be - // pulling in somewhat non-sexual content in to "sexual" label. - return ['sexual', 'underwear'] - } - } - - 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 0a6bf9ca3d2..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 '../background' - -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/services/index.ts b/packages/pds/src/services/index.ts index 449c9429f37..b239b2eee5d 100644 --- a/packages/pds/src/services/index.ts +++ b/packages/pds/src/services/index.ts @@ -1,36 +1,22 @@ import * as crypto from '@atproto/crypto' import { BlobStore } from '@atproto/repo' import Database from '../db' -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 { LabelService } from './label' -import { Labeler } from '../labeler' import { BackgroundQueue } from '../background' import { Crawlers } from '../crawlers' export function createServices(resources: { repoSigningKey: crypto.Keypair blobstore: BlobStore - imgUriBuilder: ImageUriBuilder - imgInvalidator: ImageInvalidator - labeler: Labeler backgroundQueue: BackgroundQueue crawlers: Crawlers }): Services { - const { - repoSigningKey, - blobstore, - imgUriBuilder, - imgInvalidator, - labeler, - backgroundQueue, - crawlers, - } = resources + const { repoSigningKey, blobstore, backgroundQueue, crawlers } = resources return { account: AccountService.creator(), auth: AuthService.creator(), @@ -40,13 +26,8 @@ export function createServices(resources: { blobstore, backgroundQueue, crawlers, - labeler, - ), - moderation: ModerationService.creator( - blobstore, - imgUriBuilder, - imgInvalidator, ), + moderation: ModerationService.creator(blobstore), label: LabelService.creator(), } } diff --git a/packages/pds/src/services/moderation/index.ts b/packages/pds/src/services/moderation/index.ts index 8700ec9432d..89044990e01 100644 --- a/packages/pds/src/services/moderation/index.ts +++ b/packages/pds/src/services/moderation/index.ts @@ -8,24 +8,12 @@ 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' export class ModerationService { - constructor( - public db: Database, - public blobstore: BlobStore, - public imgUriBuilder: ImageUriBuilder, - public imgInvalidator: ImageInvalidator, - ) {} - - static creator( - blobstore: BlobStore, - imgUriBuilder: ImageUriBuilder, - imgInvalidator: ImageInvalidator, - ) { - return (db: Database) => - new ModerationService(db, blobstore, imgUriBuilder, imgInvalidator) + constructor(public db: Database, public blobstore: BlobStore) {} + + static creator(blobstore: BlobStore) { + return (db: Database) => new ModerationService(db, blobstore) } views = new ModerationViews(this.db) @@ -393,14 +381,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/src/services/repo/index.ts b/packages/pds/src/services/repo/index.ts index 9d44f264afd..b3a8ffb39e9 100644 --- a/packages/pds/src/services/repo/index.ts +++ b/packages/pds/src/services/repo/index.ts @@ -24,7 +24,6 @@ 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 '../../background' import { countAll } from '../../db/util' @@ -39,7 +38,6 @@ export class RepoService { public blobstore: BlobStore, public backgroundQueue: BackgroundQueue, public crawlers: Crawlers, - public labeler: Labeler, ) { this.blobs = new RepoBlobs(db, blobstore, backgroundQueue) } @@ -49,7 +47,6 @@ export class RepoService { blobstore: BlobStore, backgroundQueue: BackgroundQueue, crawlers: Crawlers, - labeler: Labeler, ) { return (db: Database) => new RepoService( @@ -58,7 +55,6 @@ export class RepoService { blobstore, backgroundQueue, crawlers, - labeler, ) } @@ -77,7 +73,6 @@ export class RepoService { this.blobstore, this.backgroundQueue, this.crawlers, - this.labeler, ) return fn(srvc) }) @@ -228,16 +223,6 @@ export class RepoService { const seqEvt = await sequencer.formatSeqCommit(did, commitData, writes) await sequencer.sequenceEvt(this.db, seqEvt) - - // @TODO move to appview - writes.forEach((write) => { - if ( - write.action === WriteOpAction.Create || - write.action === WriteOpAction.Update - ) { - this.labeler.processRecord(write.uri, write.record) - } - }) } async rebaseRepo(did: string, swapCommit?: CID) { From 1b10634f11bf1b194fdd1b2ea33843da8ea23b60 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Mon, 12 Jun 2023 19:18:13 -0400 Subject: [PATCH 009/105] remove feedgens from pds, remove getPopular --- packages/pds/src/api/app/bsky/proxied.ts | 30 ----- packages/pds/src/context.ts | 6 - packages/pds/src/feed-gen/best-of-follows.ts | 84 -------------- packages/pds/src/feed-gen/bsky-team.ts | 55 --------- packages/pds/src/feed-gen/hot-classic.ts | 67 ----------- packages/pds/src/feed-gen/index.ts | 22 ---- packages/pds/src/feed-gen/mutuals.ts | 60 ---------- packages/pds/src/feed-gen/types.ts | 16 --- packages/pds/src/feed-gen/whats-hot.ts | 115 ------------------- packages/pds/src/feed-gen/with-friends.ts | 95 --------------- packages/pds/src/index.ts | 13 +-- 11 files changed, 1 insertion(+), 562 deletions(-) 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 diff --git a/packages/pds/src/api/app/bsky/proxied.ts b/packages/pds/src/api/app/bsky/proxied.ts index 356280b31fc..c52e863dbcf 100644 --- a/packages/pds/src/api/app/bsky/proxied.ts +++ b/packages/pds/src/api/app/bsky/proxied.ts @@ -424,34 +424,4 @@ export default function (server: Server, ctx: AppContext) { }) }, }) - - // @TODO maybe drop this? - server.app.bsky.unspecced.getPopular({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - const hotClassicUri = Object.keys(ctx.algos).find((uri) => - uri.endsWith('/hot-classic'), - ) - if (!hotClassicUri) { - return { - encoding: 'application/json', - body: { feed: [] }, - } - } - const { data: feed } = - await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( - { feed: hotClassicUri }, - await ctx.serviceAuthHeaders(requester), - ) - const res = await ctx.appviewAgent.api.app.bsky.feed.getFeed( - { ...params, feed: hotClassicUri }, - await ctx.serviceAuthHeaders(requester, feed.view.did), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) } diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index a01ebc254fb..e16fa2f124d 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -13,7 +13,6 @@ import { Services } from './services' import { Sequencer, SequencerLeader } from './sequencer' import { BackgroundQueue } from './background' import DidSqlCache from './did-cache' -import { MountedAlgos } from './feed-gen/types' import { Crawlers } from './crawlers' export class AppContext { @@ -35,7 +34,6 @@ export class AppContext { sequencerLeader: SequencerLeader backgroundQueue: BackgroundQueue crawlers: Crawlers - algos: MountedAlgos }, ) { this._appviewAgent = opts.cfg.bskyAppViewEndpoint @@ -129,10 +127,6 @@ export class AppContext { return this.opts.didCache } - get algos(): MountedAlgos { - return this.opts.algos - } - async serviceAuthHeaders(did: string, audience?: string) { const aud = audience ?? this.cfg.bskyAppViewDid if (!aud) { 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 9f930b85545..00000000000 --- a/packages/pds/src/feed-gen/best-of-follows.ts +++ /dev/null @@ -1,84 +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') - .leftJoin('post_embed_record', 'post_embed_record.postUri', '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 fa8224a4b7d..00000000000 --- a/packages/pds/src/feed-gen/bsky-team.ts +++ /dev/null @@ -1,55 +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:oky5czdrnfjpqslsw2a5iclo', // jay - 'did:plc:yk4dd2qkboz2yv6tpubpc6co', // daniel - 'did:plc:ragtjsm2j2vknwkz3zp4oxrd', // paul - 'did:plc:l3rouwludahu3ui3bt66mfvj', // devin - 'did:plc:tpg43qhh4lw4ksiffs4nbda3', // jake - 'did:plc:44ybard66vv44zksje25o7dz', // bryan - 'did:plc:qjeavhlw222ppsre4rscd3n2', // rose - 'did:plc:vjug55kidv6sye7ykr5faxxn', // emily - 'did:plc:fgsn4gf2dlgnybo4nbej5b2s', // ansh - 'did:plc:vpkhqolt662uhesyj6nxm7ys', // why - 'did:plc:z72i7hdynmk6r22z27h6tvur', // @bsky.app - 'did:plc:ewvi7nxzyoun6zhxrhs64oiz', // @atproto.com -] - -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 bdcba7de17e..00000000000 --- a/packages/pds/src/feed-gen/hot-classic.ts +++ /dev/null @@ -1,67 +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', - 'corpse', - 'self-harm', - 'porn', - 'sexual', - 'nudity', - 'underwear', -] - -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 0d17c6544ae..00000000000 --- a/packages/pds/src/feed-gen/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { AtUri } from '@atproto/uri' -import withFriends from './with-friends' -import bskyTeam from './bsky-team' -import whatsHot from './whats-hot' -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, 'whats-hot').toString()]: whatsHot, - [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 3b2d3498c56..00000000000 --- a/packages/pds/src/feed-gen/whats-hot.ts +++ /dev/null @@ -1,115 +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 { notSoftDeletedClause, 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') - .innerJoin('repo_root as author_repo', 'author_repo.did', 'post.creator') - .innerJoin('record', 'record.uri', 'post.uri') - .leftJoin('post_embed_record', 'post_embed_record.postUri', 'candidate.uri') - .where(notSoftDeletedClause(ref('author_repo'))) - .where(notSoftDeletedClause(ref('record'))) - .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 9929a200eea..00000000000 --- a/packages/pds/src/feed-gen/with-friends.ts +++ /dev/null @@ -1,95 +0,0 @@ -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 } from '../app-view/api/app/bsky/util/feed' - -const handler: AlgoHandler = async ( - ctx: AppContext, - params: SkeletonParams, - requester: string, -): Promise => { - 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 - - // @NOTE use of getFeedDateThreshold() not currently beneficial to this feed - const keyset = new FeedKeyset(ref('feed_item.sortAt'), ref('feed_item.cid')) - - let postsQb = feedService - .selectFeedItemQb() - .innerJoin('post_agg', 'post_agg.uri', 'feed_item.uri') - .where('feed_item.type', '=', 'post') - .where('post_agg.likeCount', '>=', 5) - .whereExists((qb) => - qb - .selectFrom('follow') - .where('follow.creator', '=', requester) - .whereRef('follow.subjectDid', '=', 'originatorDid'), - ) - .where((qb) => - accountService.whereNotMuted(qb, requester, [ref('post.creator')]), - ) - .whereNotExists(graphService.blockQb(requester, [ref('post.creator')])) - - postsQb = paginate(postsQb, { limit, cursor, keyset }) - - const feedItems = await postsQb.execute() - return { - feedItems, - cursor: keyset.packFromResult(feedItems), - } -} - -export default handler - -// Original algorithm, temporarily disabled because of performance issues -// -------------------------- - -// const postRate = sql`(10000 * ${ref('postsCount')} / extract(epoch from ${ref( -// 'user_account.createdAt', -// )}::timestamp))` -// const mostActiveMutuals = await ctx.db.db -// .selectFrom('follow') -// .select('subjectDid as did') -// .innerJoin('user_account', 'user_account.did', 'follow.subjectDid') -// .innerJoin('profile_agg', 'profile_agg.did', 'follow.subjectDid') -// .where('follow.creator', '=', requester) -// .whereExists((qb) => -// qb -// .selectFrom('follow as mutual') -// .where('mutual.subjectDid', '=', requester) -// .whereRef('mutual.creator', '=', 'follow.subjectDid'), -// ) -// .orderBy(postRate, 'desc') -// .limit(25) -// .execute() - -// if (!mostActiveMutuals.length) { -// return { feedItems: [] } -// } - -// // All posts that hit a certain threshold of likes and also have -// // at least one like by one of your most active mutuals. -// let postsQb = feedService -// .selectFeedItemQb() -// .innerJoin('post_agg', 'post_agg.uri', 'feed_item.uri') -// .where('feed_item.type', '=', 'post') -// .where('post_agg.likeCount', '>=', 5) -// .whereExists((qb) => { -// return qb -// .selectFrom('like') -// .whereRef('like.subject', '=', 'post.uri') -// .whereRef( -// 'like.creator', -// 'in', -// valuesList(mostActiveMutuals.map((follow) => follow.did)), -// ) -// }) -// .where((qb) => -// accountService.whereNotMuted(qb, requester, [ref('post.creator')]), -// ) -// .whereNotExists(graphService.blockQb(requester, [ref('post.creator')])) diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index c119f834e1f..1c973b01955 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -28,7 +28,6 @@ import { Sequencer, SequencerLeader } from './sequencer' import { BackgroundQueue } from './background' import DidSqlCache from './did-cache' import { IdResolver } from '@atproto/identity' -import { MountedAlgos } from './feed-gen/types' import { Crawlers } from './crawlers' export type { ServerConfigValues } from './config' @@ -37,7 +36,6 @@ export { Database } from './db' export { ViewMaintainer } from './db/views' export { DiskBlobStore, MemoryBlobStore } from './storage' export { AppContext } from './context' -export { makeAlgos } from './feed-gen' export class PDS { public ctx: AppContext @@ -60,17 +58,9 @@ export class PDS { blobstore: BlobStore repoSigningKey: crypto.Keypair plcRotationKey: crypto.Keypair - algos?: MountedAlgos config: ServerConfig }): PDS { - const { - db, - blobstore, - repoSigningKey, - plcRotationKey, - algos = {}, - config, - } = opts + const { db, blobstore, repoSigningKey, plcRotationKey, config } = opts const auth = new ServerAuth({ jwtSecret: config.jwtSecret, adminPass: config.adminPassword, @@ -129,7 +119,6 @@ export class PDS { mailer, backgroundQueue, crawlers, - algos, }) let server = createServer({ From d318f3e483863d4b73ced32a0fad5c6e88ec5cf2 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Mon, 12 Jun 2023 19:21:19 -0400 Subject: [PATCH 010/105] remove unused image helper from pds --- packages/pds/src/image/sharp.ts | 45 +-------------------------------- 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/packages/pds/src/image/sharp.ts b/packages/pds/src/image/sharp.ts index 1edc7a58835..dc39133365c 100644 --- a/packages/pds/src/image/sharp.ts +++ b/packages/pds/src/image/sharp.ts @@ -1,54 +1,11 @@ import { Readable } from 'stream' import { pipeline } from 'stream/promises' import sharp from 'sharp' -import { errHasMsg, forwardStreamErrors } from '@atproto/common' +import { errHasMsg } 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 { From 49010ed2f85a5e1875876cc5e541656cae928944 Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 12 Jun 2023 18:25:31 -0500 Subject: [PATCH 011/105] fixing compiler errors --- packages/pds/src/services/account/index.ts | 1 - packages/pds/src/services/moderation/views.ts | 17 +++------------- packages/pds/src/services/repo/index.ts | 8 +------- packages/pds/src/services/util/search.ts | 20 ------------------- 4 files changed, 4 insertions(+), 42 deletions(-) diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts index a2e874baa4f..101c6f9cbd9 100644 --- a/packages/pds/src/services/account/index.ts +++ b/packages/pds/src/services/account/index.ts @@ -256,7 +256,6 @@ export class AccountService { .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') diff --git a/packages/pds/src/services/moderation/views.ts b/packages/pds/src/services/moderation/views.ts index f51372d701b..2d3afd3da56 100644 --- a/packages/pds/src/services/moderation/views.ts +++ b/packages/pds/src/services/moderation/views.ts @@ -1,5 +1,5 @@ import { Selectable } from 'kysely' -import { ArrayEl, cborBytesToRecord } from '@atproto/common' +import { ArrayEl } from '@atproto/common' import { AtUri } from '@atproto/uri' import Database from '../../db' import { DidHandle } from '../../db/tables/did-handle' @@ -42,12 +42,6 @@ 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('ipld_block as profile_block', (join) => - join - .onRef('profile_block.cid', '=', 'profile.cid') - .onRef('profile_block.creator', '=', 'did_handle.did'), - ) .where( 'did_handle.did', 'in', @@ -57,7 +51,6 @@ export class ModerationViews { 'did_handle.did as did', 'user_account.email as email', 'user_account.invitesDisabled as invitesDisabled', - 'profile_block.content as profileBytes', ]) .execute(), this.db.db @@ -86,17 +79,13 @@ export class ModerationViews { ) const views = results.map((r) => { - const { email, invitesDisabled, profileBytes } = infoByDid[r.did] ?? {} + const { email, invitesDisabled } = infoByDid[r.did] ?? {} const action = actionByDid[r.did] - const relatedRecords: object[] = [] - if (profileBytes) { - relatedRecords.push(cborBytesToRecord(profileBytes)) - } return { did: r.did, handle: r.handle, email: email ?? undefined, - relatedRecords, + relatedRecords: [], indexedAt: r.indexedAt, moderation: { currentAction: action diff --git a/packages/pds/src/services/repo/index.ts b/packages/pds/src/services/repo/index.ts index b3a8ffb39e9..8e5067427b9 100644 --- a/packages/pds/src/services/repo/index.ts +++ b/packages/pds/src/services/repo/index.ts @@ -49,13 +49,7 @@ export class RepoService { crawlers: Crawlers, ) { return (db: Database) => - new RepoService( - db, - keypair, - blobstore, - backgroundQueue, - crawlers, - ) + new RepoService(db, keypair, blobstore, backgroundQueue, crawlers) } services = { diff --git a/packages/pds/src/services/util/search.ts b/packages/pds/src/services/util/search.ts index 7bb41ce790f..437ca585cdd 100644 --- a/packages/pds/src/services/util/search.ts +++ b/packages/pds/src/services/util/search.ts @@ -41,25 +41,6 @@ export const getUserSearchQueryPg = ( keyset: new SearchKeyset(distanceAccount, ref('handle')), }) - // Matching profiles based on display name - const distanceProfile = distance(term, ref('displayName')) - let profilesQb = db.db - .selectFrom('profile') - .innerJoin('did_handle', 'did_handle.did', 'profile.creator') - .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') - .if(!includeSoftDeleted, (qb) => - qb.where(notSoftDeletedClause(ref('repo_root'))), - ) - .where(similar(term, ref('displayName'))) // Coarse filter engaging trigram index - .where(distanceProfile, '<', threshold) // Refines results from trigram index - .select(['did_handle.did as did', distanceProfile.as('distance')]) - profilesQb = paginate(profilesQb, { - limit, - cursor, - direction: 'asc', - keyset: new SearchKeyset(distanceProfile, ref('handle')), - }) - // Combine user account and profile results, taking best matches from each const emptyQb = db.db .selectFrom('user_account') @@ -69,7 +50,6 @@ export const getUserSearchQueryPg = ( .selectFrom( emptyQb .unionAll(sql`${accountsQb}`) // The sql`` is adding parens - .unionAll(sql`${profilesQb}`) .as('accounts_and_profiles'), ) .selectAll() From b45878095bfb39c1197078c9f571de033d2c304b Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 12 Jun 2023 18:29:55 -0500 Subject: [PATCH 012/105] clean up sharp --- packages/pds/src/image/index.ts | 69 +++++++++++++++++++++++- packages/pds/src/image/logger.ts | 5 -- packages/pds/src/image/sharp.ts | 50 ----------------- packages/pds/src/image/util.ts | 32 ----------- packages/pds/src/services/util/search.ts | 6 +-- packages/pds/tests/image/server.test.ts | 2 +- packages/pds/tests/image/sharp.test.ts | 2 +- 7 files changed, 71 insertions(+), 95 deletions(-) delete mode 100644 packages/pds/src/image/logger.ts delete mode 100644 packages/pds/src/image/sharp.ts delete mode 100644 packages/pds/src/image/util.ts diff --git a/packages/pds/src/image/index.ts b/packages/pds/src/image/index.ts index 3197b5aeb5c..4f70321f0ed 100644 --- a/packages/pds/src/image/index.ts +++ b/packages/pds/src/image/index.ts @@ -1,2 +1,67 @@ -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 Dimensions = { height: number; width: number } + +export type ImageInfo = Dimensions & { + size: number + mime: `image/${string}` | 'unknown' +} + +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/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/sharp.ts b/packages/pds/src/image/sharp.ts deleted file mode 100644 index dc39133365c..00000000000 --- a/packages/pds/src/image/sharp.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Readable } from 'stream' -import { pipeline } from 'stream/promises' -import sharp from 'sharp' -import { errHasMsg } from '@atproto/common' -import { formatsToMimes, ImageInfo, Options } from './util' - -export type { Options } - -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/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/services/util/search.ts b/packages/pds/src/services/util/search.ts index 437ca585cdd..11a7d66809d 100644 --- a/packages/pds/src/services/util/search.ts +++ b/packages/pds/src/services/util/search.ts @@ -4,7 +4,7 @@ import Database from '../../db' import { notSoftDeletedClause, DbRef } from '../../db/util' import { GenericKeyset, paginate } from '../../db/pagination' -// @TODO utilized in both pds and app-view +// @TODO Come back and clean this up since removing profiles export const getUserSearchQueryPg = ( db: Database, opts: { @@ -98,9 +98,7 @@ export const getUserSearchQuerySqlite = ( // We'll ensure there's a space before each word in both textForMatch and in safeWords, // so that we can reliably match word prefixes using LIKE operator. - const textForMatch = sql`lower(' ' || ${ref( - 'did_handle.handle', - )} || ' ' || coalesce(${ref('profile.displayName')}, ''))` + const textForMatch = sql`lower(${ref('did_handle.handle')})` const keyset = new SearchKeyset(sql``, sql``) const unpackedCursor = keyset.unpackCursor(cursor) diff --git a/packages/pds/tests/image/server.test.ts b/packages/pds/tests/image/server.test.ts index 76dc7f57978..67deb935d08 100644 --- a/packages/pds/tests/image/server.test.ts +++ b/packages/pds/tests/image/server.test.ts @@ -4,7 +4,7 @@ import path from 'path' import fs from 'fs' import { AddressInfo } from 'net' import axios, { AxiosInstance } from 'axios' -import { getInfo } from '../../src/image/sharp' +import { getInfo } from '../../src/image' import { BlobDiskCache, ImageProcessingServer } from '../../src/image/server' import { DiskBlobStore } from '../../src' import { cidForCbor } from '@atproto/common' diff --git a/packages/pds/tests/image/sharp.test.ts b/packages/pds/tests/image/sharp.test.ts index d0a46b662b3..7279e0331b8 100644 --- a/packages/pds/tests/image/sharp.test.ts +++ b/packages/pds/tests/image/sharp.test.ts @@ -1,5 +1,5 @@ import { createReadStream } from 'fs' -import { Options, getInfo, resize } from '../../src/image/sharp' +import { Options, getInfo, resize } from '../../src/image' describe('sharp image processor', () => { it('scales up to cover.', async () => { From c9c561dac6926a51d2b8e3d0415ba44ce16b104c Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 12 Jun 2023 18:33:00 -0500 Subject: [PATCH 013/105] rm label service --- .../atproto/admin/reverseModerationAction.ts | 18 ---- .../com/atproto/admin/takeModerationAction.ts | 8 -- packages/pds/src/db/views.ts | 60 -------------- packages/pds/src/index.ts | 1 - packages/pds/src/services/index.ts | 3 - packages/pds/src/services/label/index.ts | 82 ------------------- 6 files changed, 172 deletions(-) delete mode 100644 packages/pds/src/db/views.ts delete mode 100644 packages/pds/src/services/label/index.ts diff --git a/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts b/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts index 18691d89e96..cf7a2b435cf 100644 --- a/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts +++ b/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts @@ -14,7 +14,6 @@ export default function (server: Server, ctx: AppContext) { const moderationAction = await db.transaction(async (dbTxn) => { const moderationTxn = services.moderation(dbTxn) - const labelTxn = services.label(dbTxn) const now = new Date() const existing = await moderationTxn.getAction(id) @@ -54,23 +53,6 @@ export default function (server: Server, ctx: AppContext) { }) } - // 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 5eb48f98e59..9e309851e59 100644 --- a/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts +++ b/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts @@ -38,7 +38,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.label(dbTxn) const result = await moderationTxn.logAction({ action: getAction(action), @@ -74,13 +73,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/db/views.ts b/packages/pds/src/db/views.ts deleted file mode 100644 index 8bf6c25b72b..00000000000 --- a/packages/pds/src/db/views.ts +++ /dev/null @@ -1,60 +0,0 @@ -import assert from 'assert' -import { wait } from '@atproto/common' -import { Leader } from './leader' -import { dbLogger } from '../logger' -import Database from '.' - -export const VIEW_MAINTAINER_ID = 1010 -const VIEWS = ['algo_whats_hot_view'] - -export class ViewMaintainer { - leader = new Leader(VIEW_MAINTAINER_ID, this.db) - destroyed = false - - // @NOTE the db must be authed as the owner of the materialized view, per postgres. - constructor(public db: Database, public intervalSec = 60) { - assert( - this.db.dialect === 'pg', - 'View maintainer can only be used with postgres', - ) - } - - async run() { - while (!this.destroyed) { - try { - const { ran } = await this.leader.run(async ({ signal }) => { - await this.db.maintainMaterializedViews({ - signal, - views: VIEWS, - intervalSec: this.intervalSec, - }) - }) - if (ran && !this.destroyed) { - throw new Error('View maintainer completed, but should be persistent') - } - } catch (err) { - dbLogger.error( - { - err, - views: VIEWS, - intervalSec: this.intervalSec, - lockId: VIEW_MAINTAINER_ID, - }, - 'view maintainer errored', - ) - } - if (!this.destroyed) { - await wait(10000 + jitter(2000)) - } - } - } - - destroy() { - this.destroyed = true - this.leader.destroy() - } -} - -function jitter(maxMs) { - return Math.round((Math.random() - 0.5) * maxMs * 2) -} diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index 1c973b01955..759f1dacd32 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -33,7 +33,6 @@ import { Crawlers } from './crawlers' export type { ServerConfigValues } from './config' export { ServerConfig } from './config' export { Database } from './db' -export { ViewMaintainer } from './db/views' export { DiskBlobStore, MemoryBlobStore } from './storage' export { AppContext } from './context' diff --git a/packages/pds/src/services/index.ts b/packages/pds/src/services/index.ts index b239b2eee5d..c10c130a5c9 100644 --- a/packages/pds/src/services/index.ts +++ b/packages/pds/src/services/index.ts @@ -6,7 +6,6 @@ import { AuthService } from './auth' import { RecordService } from './record' import { RepoService } from './repo' import { ModerationService } from './moderation' -import { LabelService } from './label' import { BackgroundQueue } from '../background' import { Crawlers } from '../crawlers' @@ -28,7 +27,6 @@ export function createServices(resources: { crawlers, ), moderation: ModerationService.creator(blobstore), - label: LabelService.creator(), } } @@ -38,7 +36,6 @@ export type Services = { record: FromDb repo: FromDb moderation: FromDb - label: FromDb } type FromDb = (db: Database) => T diff --git a/packages/pds/src/services/label/index.ts b/packages/pds/src/services/label/index.ts deleted file mode 100644 index 926f1c77869..00000000000 --- a/packages/pds/src/services/label/index.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { sql } from 'kysely' -import Database from '../../db' -import { Label } from '../../lexicon/types/com/atproto/label/defs' - -export type Labels = Record - -export class LabelService { - constructor(public db: Database) {} - - static creator() { - return (db: Database) => new LabelService(db) - } - - 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[], - includeNeg?: boolean, - ): Promise { - if (subjects.length < 1) return {} - const res = await this.db.db - .selectFrom('label') - .where('label.uri', 'in', subjects) - .if(!includeNeg, (qb) => qb.where('neg', '=', 0)) - .selectAll() - .execute() - 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) - } -} From 2774c1723a92742ac2d502e60000479984701b56 Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 12 Jun 2023 18:45:57 -0500 Subject: [PATCH 014/105] first pass on cleaning up tests --- packages/pds/src/services/repo/blobs.ts | 2 +- .../feed-generation.test.ts.snap | 948 --- .../tests/__snapshots__/indexing.test.ts.snap | 184 - packages/pds/tests/account-deletion.test.ts | 106 - .../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 | 0 .../admin/get-moderation-actions.test.ts | 0 .../admin/get-moderation-report.test.ts | 0 .../admin/get-moderation-reports.test.ts | 0 .../{views => }/admin/get-record.test.ts | 0 .../tests/{views => }/admin/get-repo.test.ts | 0 .../tests/{views => }/admin/invites.test.ts | 0 .../{views => }/admin/repo-search.test.ts | 0 packages/pds/tests/algos/hot-classic.test.ts | 93 - packages/pds/tests/algos/whats-hot.test.ts | 119 - packages/pds/tests/algos/with-friends.test.ts | 148 - .../__snapshots__/sync.test.ts.snap | 1208 ---- packages/pds/tests/event-stream/sync.test.ts | 150 - packages/pds/tests/feed-generation.test.ts | 408 -- 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 | 212 - .../labeler/fixtures/hiveai_resp_example.json | 401 -- packages/pds/tests/labeler/hive.test.ts | 16 - packages/pds/tests/labeler/labeler.test.ts | 170 - .../pds/tests/migrations/blob-creator.test.ts | 141 - .../migrations/indexed-at-on-record.test.ts | 77 - .../tests/migrations/post-hierarchy.test.ts | 70 - .../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 - .../{image/fixtures => sample-img}/at.png | Bin .../{image/fixtures => sample-img}/hd-key.jpg | Bin .../fixtures => sample-img}/key-alt.jpg | Bin .../key-landscape-large.jpg | Bin .../key-landscape-small.jpg | Bin .../key-portrait-large.jpg | Bin .../key-portrait-small.jpg | Bin .../__snapshots__/author-feed.test.ts.snap | 1806 ------ .../views/__snapshots__/blocks.test.ts.snap | 294 - .../views/__snapshots__/follows.test.ts.snap | 703 --- .../views/__snapshots__/likes.test.ts.snap | 103 - .../__snapshots__/mute-lists.test.ts.snap | 376 -- .../views/__snapshots__/mutes.test.ts.snap | 75 - .../__snapshots__/notifications.test.ts.snap | 1511 ----- .../views/__snapshots__/posts.test.ts.snap | 395 -- .../views/__snapshots__/profile.test.ts.snap | 218 - .../views/__snapshots__/reposts.test.ts.snap | 91 - .../views/__snapshots__/thread.test.ts.snap | 1458 ----- .../views/__snapshots__/timeline.test.ts.snap | 5133 ----------------- packages/pds/tests/views/actor-search.test.ts | 440 -- packages/pds/tests/views/author-feed.test.ts | 249 - packages/pds/tests/views/blocks.test.ts | 327 -- packages/pds/tests/views/follows.test.ts | 266 - packages/pds/tests/views/likes.test.ts | 97 - packages/pds/tests/views/mute-lists.test.ts | 336 -- 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 | 271 - packages/pds/tests/views/reposts.test.ts | 77 - packages/pds/tests/views/suggestions.test.ts | 75 - packages/pds/tests/views/thread.test.ts | 489 -- packages/pds/tests/views/timeline.test.ts | 283 - 72 files changed, 1 insertion(+), 21108 deletions(-) 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 (100%) rename packages/pds/tests/{views => }/admin/get-moderation-actions.test.ts (100%) rename packages/pds/tests/{views => }/admin/get-moderation-report.test.ts (100%) rename packages/pds/tests/{views => }/admin/get-moderation-reports.test.ts (100%) rename packages/pds/tests/{views => }/admin/get-record.test.ts (100%) rename packages/pds/tests/{views => }/admin/get-repo.test.ts (100%) rename packages/pds/tests/{views => }/admin/invites.test.ts (100%) rename packages/pds/tests/{views => }/admin/repo-search.test.ts (100%) 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/event-stream/__snapshots__/sync.test.ts.snap delete mode 100644 packages/pds/tests/event-stream/sync.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/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 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/post-hierarchy.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 rename packages/pds/tests/{image/fixtures => sample-img}/at.png (100%) rename packages/pds/tests/{image/fixtures => sample-img}/hd-key.jpg (100%) rename packages/pds/tests/{image/fixtures => sample-img}/key-alt.jpg (100%) rename packages/pds/tests/{image/fixtures => sample-img}/key-landscape-large.jpg (100%) rename packages/pds/tests/{image/fixtures => sample-img}/key-landscape-small.jpg (100%) rename packages/pds/tests/{image/fixtures => sample-img}/key-portrait-large.jpg (100%) rename packages/pds/tests/{image/fixtures => sample-img}/key-portrait-small.jpg (100%) 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-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/pds/src/services/repo/blobs.ts b/packages/pds/src/services/repo/blobs.ts index acb89a2b9b1..71d8c68b8dc 100644 --- a/packages/pds/src/services/repo/blobs.ts +++ b/packages/pds/src/services/repo/blobs.ts @@ -7,11 +7,11 @@ import { BlobStore, CidSet, WriteOpAction } from '@atproto/repo' import { AtUri } from '@atproto/uri' import { cloneStream, sha256RawToCid, streamSize } from '@atproto/common' import { InvalidRequestError } from '@atproto/xrpc-server' +import { BlobRef } from '@atproto/lexicon' import { PreparedBlobRef, PreparedWrite } from '../../repo/types' import Database from '../../db' 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 '../../background' 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 be210c9fbec..00000000000 --- a/packages/pds/tests/__snapshots__/feed-generation.test.ts.snap +++ /dev/null @@ -1,948 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -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 [], - "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(1)", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", - "muted": false, - }, - }, - "description": "Provides all feed candidates", - "did": "user(1)", - "displayName": "All", - "indexedAt": "1970-01-01T00:00:00.000Z", - "likeCount": 2, - "uri": "record(1)", - "viewer": Object { - "like": "record(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", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(1)", - "uri": "record(1)", - }, - }, - "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 [], - "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(1)", - "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 [], - "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(3)", - "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 [], - "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(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 [], - "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(5)", - "viewer": Object { - "like": "record(6)", - }, - }, -] -`; - -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 [], - "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", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", - "muted": false, - }, - }, - "cid": "cids(1)", - "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": "bob back at it again!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(1)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "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(1)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", - "muted": false, - }, - }, - "cid": "cids(1)", - "embeds": Array [], - "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", - "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(1)", - "uri": "record(1)", - }, - }, - }, - "text": "hi im carol", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(4)", - "viewer": Object { - "like": "record(7)", - }, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "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(6)", - "uri": "record(9)", - }, - "root": Object { - "cid": "cids(6)", - "uri": "record(9)", - }, - }, - "text": "of course", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(8)", - "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 [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(6)", - "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(9)", - "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 [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(6)", - "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(9)", - "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(11)", - "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(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(2)", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", - "muted": false, - }, - }, - "cid": "cids(1)", - "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", - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "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", - "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(1)", - "uri": "record(1)", - }, - }, - }, - "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(2)", - "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", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(10)", - "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(6)", - "following": "record(5)", - "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 [], - "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", - "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(3)", - "following": "record(2)", - "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(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(4)", - "embeds": Array [], - "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", - "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(4)", - }, - }, - }, - "text": "hi im carol", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(1)", - "viewer": Object { - "like": "record(7)", - }, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", - "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(6)", - "uri": "record(9)", - }, - "root": Object { - "cid": "cids(6)", - "uri": "record(9)", - }, - }, - "text": "of course", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(8)", - "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 [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(6)", - "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(9)", - "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 [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(6)", - "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(9)", - "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 [], - "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(3)", - }, - }, -} -`; - -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 [], - "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(3)", - }, - }, - Object { - "cid": "cids(1)", - "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 [], - "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(4)", - "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 5e070726fed..00000000000 --- a/packages/pds/tests/__snapshots__/indexing.test.ts.snap +++ /dev/null @@ -1,184 +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 [], - "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 { - "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 [], - "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": 11, - "byteStart": 0, - }, - }, - ], - "text": "@carol.test how are you?", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(0)", - "viewer": Object {}, - }, - "replies": Array [], - }, -} -`; - -exports[`indexing indexes posts. 3`] = ` -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 [], - "updateNotifications": Array [ - Object { - "author": "user(1)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "reason": "mention", - "reasonSubject": null, - "recordCid": "cids(1)", - "recordUri": "record(0)", - "userDid": "user(2)", - }, - ], -} -`; - -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/account-deletion.test.ts b/packages/pds/tests/account-deletion.test.ts index ca29783ac4c..dea79d262b4 100644 --- a/packages/pds/tests/account-deletion.test.ts +++ b/packages/pds/tests/account-deletion.test.ts @@ -11,26 +11,13 @@ 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 { RepoCommitHistory } from '../src/db/tables/repo-commit-history' import { RepoCommitBlock } from '../src/db/tables/repo-commit-block' 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 @@ -165,9 +152,6 @@ describe('account deletion', () => { expect(updatedDbContents.users).toEqual( initialDbContents.users.filter((row) => row.did !== carol.did), ) - expect(updatedDbContents.userState).toEqual( - initialDbContents.userState.filter((row) => row.did !== carol.did), - ) expect(updatedDbContents.blocks).toEqual( initialDbContents.blocks.filter((row) => row.creator !== carol.did), ) @@ -182,43 +166,9 @@ describe('account deletion', () => { (row) => row.creator !== carol.did, ), ) - }) - - 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 () => { @@ -291,22 +241,11 @@ describe('account deletion', () => { type DbContents = { roots: RepoRoot[] users: Selectable[] - userState: UserState[] blocks: IpldBlock[] seqs: Selectable[] commitHistories: RepoCommitHistory[] commitBlocks: RepoCommitBlock[] 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[] } @@ -315,28 +254,16 @@ const getDbContents = async (db: Database): Promise => { const [ roots, users, - userState, blocks, seqs, commitHistories, commitBlocks, 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') @@ -358,28 +285,6 @@ const getDbContents = async (db: Database): Promise => { .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') @@ -392,22 +297,11 @@ const getDbContents = async (db: Database): Promise => { return { roots, users, - userState, blocks, seqs, commitHistories, commitBlocks, records, - posts, - postImages, - postExternals, - postRecords, - likes, - reposts, - follows, - actorBlocks, - lists, - listItems, repoBlobs, blobs, } 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 100% rename from packages/pds/tests/views/admin/get-moderation-action.test.ts rename to packages/pds/tests/admin/get-moderation-action.test.ts 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 100% rename from packages/pds/tests/views/admin/get-moderation-actions.test.ts rename to packages/pds/tests/admin/get-moderation-actions.test.ts 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 100% rename from packages/pds/tests/views/admin/get-moderation-report.test.ts rename to packages/pds/tests/admin/get-moderation-report.test.ts 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 100% rename from packages/pds/tests/views/admin/get-moderation-reports.test.ts rename to packages/pds/tests/admin/get-moderation-reports.test.ts diff --git a/packages/pds/tests/views/admin/get-record.test.ts b/packages/pds/tests/admin/get-record.test.ts similarity index 100% rename from packages/pds/tests/views/admin/get-record.test.ts rename to packages/pds/tests/admin/get-record.test.ts diff --git a/packages/pds/tests/views/admin/get-repo.test.ts b/packages/pds/tests/admin/get-repo.test.ts similarity index 100% rename from packages/pds/tests/views/admin/get-repo.test.ts rename to packages/pds/tests/admin/get-repo.test.ts diff --git a/packages/pds/tests/views/admin/invites.test.ts b/packages/pds/tests/admin/invites.test.ts similarity index 100% rename from packages/pds/tests/views/admin/invites.test.ts rename to packages/pds/tests/admin/invites.test.ts diff --git a/packages/pds/tests/views/admin/repo-search.test.ts b/packages/pds/tests/admin/repo-search.test.ts similarity index 100% rename from packages/pds/tests/views/admin/repo-search.test.ts rename to packages/pds/tests/admin/repo-search.test.ts 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 180c4c90b92..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('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.ctx.backgroundQueue.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.ctx.backgroundQueue.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 d26c73dcf62..00000000000 --- a/packages/pds/tests/algos/whats-hot.test.ts +++ /dev/null @@ -1,119 +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('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.ctx.backgroundQueue.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(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.ctx.backgroundQueue.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 () => { - 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 510274242cd..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.ctx.backgroundQueue.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/event-stream/__snapshots__/sync.test.ts.snap b/packages/pds/tests/event-stream/__snapshots__/sync.test.ts.snap deleted file mode 100644 index cbabf0cb734..00000000000 --- a/packages/pds/tests/event-stream/__snapshots__/sync.test.ts.snap +++ /dev/null @@ -1,1208 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`sync rebuilds timeline indexes from repo 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 [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - "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(1)", - "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(1)", - "muted": false, - }, - }, - "cid": "cids(1)", - "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(5)", - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(2)", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(5)", - "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", - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "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.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(6)", - }, - }, - }, - "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(2)", - "uri": "record(3)", - }, - }, - "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(2)", - "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(5)", - "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 [], - "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(10)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(9)", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(7)", - "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(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(10)", - "val": "test-label", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(10)", - "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(3)", - }, - "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(10)", - "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 [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(8)", - "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(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(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - "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 [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - }, - }, - 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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(7)", - "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(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(10)", - "val": "test-label", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(10)", - "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(3)", - }, - "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(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 [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - "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 [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - }, - }, - 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 [], - "viewer": Object { - "blockedBy": false, - "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(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, - }, - }, - "cid": "cids(1)", - "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(5)", - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(2)", - "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.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(6)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - ], - "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.record", - "record": Object { - "cid": "cids(2)", - "uri": "record(3)", - }, - }, - "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(9)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(12)", - "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(1)", - "uri": "record(2)", - }, - }, - "text": "yoohoo label_me", - }, - "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(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "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.000+00:00", - "text": "bobby boy here", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(13)", - "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 [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - }, - 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(1)", - "muted": false, - }, - }, - "cid": "cids(1)", - "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(5)", - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(2)", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(5)", - "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", - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "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.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(6)", - }, - }, - }, - "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(2)", - "uri": "record(3)", - }, - }, - "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(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(1)", - "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 { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "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(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(5)", - "embeds": Array [], - "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", - "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(6)", - }, - }, - }, - "text": "hi im carol", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(3)", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "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", - "text": "bob back at it again!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(6)", - "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 [], - "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.000Z", - "text": "hey there", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(16)", - "viewer": Object {}, - }, - }, -] -`; diff --git a/packages/pds/tests/event-stream/sync.test.ts b/packages/pds/tests/event-stream/sync.test.ts deleted file mode 100644 index cf42f86223c..00000000000 --- a/packages/pds/tests/event-stream/sync.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { sql } from 'kysely' -import AtpAgent from '@atproto/api' -import { getWriteLog, RecordWriteOp, WriteOpAction } from '@atproto/repo' -import SqlRepoStorage from '../../src/sql-repo-storage' -import basicSeed from '../seeds/basic' -import { SeedClient } from '../seeds/client' -import { forSnapshot, runTestServer, TestServerInfo } from '../_util' -import { - prepareCreate, - prepareDelete, - prepareUpdate, - PreparedWrite, -} from '../../src/repo' -import { AppContext } from '../../src' - -describe('sync', () => { - let server: TestServerInfo - let ctx: AppContext - let agent: AtpAgent - let sc: SeedClient - - beforeAll(async () => { - server = await runTestServer({ - dbPostgresSchema: 'event_stream_sync', - }) - ctx = server.ctx - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await basicSeed(sc) - }) - - afterAll(async () => { - await server.close() - }) - - it('rebuilds timeline indexes from repo state.', async () => { - const { db, services } = ctx - const { ref } = db.db.dynamic - // Destroy indexes - await Promise.all( - indexedTables.map((t) => sql`delete from ${ref(t)}`.execute(db.db)), - ) - // Confirm timeline empty - const emptiedTL = await agent.api.app.bsky.feed.getTimeline( - {}, - { headers: sc.getHeaders(sc.dids.alice) }, - ) - expect(emptiedTL.data.feed).toEqual([]) - // Compute oplog from state of all repos - const repoOpLogs = await Promise.all( - Object.entries(sc.dids) - .sort(([nameA], [nameB]) => nameA.localeCompare(nameB)) // Order for test determinism - .map(([, did]) => did) - .map(async (did) => ({ - did, - opLog: await getOpLog(did), - })), - ) - const indexOps = repoOpLogs.flatMap(({ did, opLog }) => - opLog.map((ops) => ({ did, ops })), - ) - // Run oplog through indexers - let ts = Date.now() // Increment for test determinism - for (const op of indexOps) { - const now = new Date(ts++).toISOString() - const writes = await prepareWrites(op.did, op.ops) - await db.transaction((dbTxn) => - services.repo(dbTxn).indexWrites(writes, now), - ) - } - // Check indexed timeline - const aliceTL = await agent.api.app.bsky.feed.getTimeline( - {}, - { headers: sc.getHeaders(sc.dids.alice) }, - ) - expect(forSnapshot(aliceTL.data.feed)).toMatchSnapshot() - }) - - async function getOpLog(did: string) { - const { db } = ctx - const storage = new SqlRepoStorage(db, did) - const root = await storage.getHead() - if (!root) throw new Error('Missing repo root') - return await getWriteLog(storage, root, null) - } - - function prepareWrites( - did: string, - ops: RecordWriteOp[], - ): Promise { - return Promise.all( - ops.map((op) => { - const { action } = op - if (action === WriteOpAction.Create) { - return prepareCreate({ - did, - collection: op.collection, - rkey: op.rkey, - record: op.record, - }) - } else if (action === WriteOpAction.Update) { - return prepareUpdate({ - did, - collection: op.collection, - rkey: op.rkey, - record: op.record, - }) - } else if (action === WriteOpAction.Delete) { - return prepareDelete({ - did, - collection: op.collection, - rkey: op.rkey, - }) - } else { - const exhaustiveCheck: never = action - throw new Error(`Unhandled case: ${exhaustiveCheck}`) - } - }), - ) - } - - const indexedTables = [ - 'record', - 'duplicate_record', - 'user_notification', - 'profile', - 'follow', - 'post', - 'post_hierarchy', - 'post_embed_image', - 'post_embed_external', - 'post_embed_record', - 'repost', - 'feed_item', - 'like', - /* Not these: - * 'ipld_block', - * 'blob', - * 'repo_blob', - * 'user', - * 'did_handle', - * 'refresh_token', - * 'repo_root', - * 'invite_code', - * 'invite_code_use', - * 'message_queue', - * 'message_queue_cursor', - */ - ] -}) diff --git a/packages/pds/tests/feed-generation.test.ts b/packages/pds/tests/feed-generation.test.ts deleted file mode 100644 index 02cde8876f0..00000000000 --- a/packages/pds/tests/feed-generation.test.ts +++ /dev/null @@ -1,408 +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 { 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' - -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 - - beforeAll(async () => { - network = await TestNetworkNoAppView.create({ - dbPostgresSchema: 'feed_generation', - }) - agent = network.pds.getClient() - sc = new SeedClient(agent) - await basicSeed(sc) - await network.pds.ctx.backgroundQueue.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') - gen = await network.createFeedGen({ - [allUri.toString()]: feedGenHandler('all'), - [feedUriBadPagination.toString()]: feedGenHandler('bad-pagination'), - [evenUri.toString()]: feedGenHandler('even'), - }) - }) - - 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), - ) - feedUriAll = all.uri - feedUriAllRef = new RecordRef(all.uri, all.cid) - feedUriEven = even.uri - feedUriOdd = odd.uri - feedUriBadPagination = badPagination.uri - }) - - 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(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() - }) - - 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) - }) - - // @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] }, - { headers: sc.getHeaders(sc.dids.bob) }, - ) - expect(forSnapshot(resEven.data)).toMatchSnapshot() - }) - }) - - describe('getPopularFeedGenerators', () => { - it('gets popular feed generators', async () => { - const resEven = - await agent.api.app.bsky.unspecced.getPopularFeedGenerators( - {}, - { headers: sc.getHeaders(sc.dids.bob) }, - ) - expect(resEven.data.feeds.map((f) => f.likeCount)).toEqual([2, 0, 0, 0]) - }) - }) - - 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('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' | '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) => - feedName === 'even' ? i % 2 === 0 : 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/image/server.test.ts b/packages/pds/tests/image/server.test.ts deleted file mode 100644 index 67deb935d08..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' -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 7279e0331b8..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' - -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 7402d7e6aa9..00000000000 --- a/packages/pds/tests/indexing.test.ts +++ /dev/null @@ -1,212 +0,0 @@ -import AtpAgent, { AppBskyActorProfile, AppBskyFeedPost } from '@atproto/api' -import { AtUri } from '@atproto/uri' -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 updateRecord = await prepareUpdate({ - did: sc.dids.alice, - collection: ids.AppBskyFeedPost, - rkey: uri.rkey, - record: { - $type: ids.AppBskyFeedPost, - text: '@carol.test how are you?', - facets: [ - { - index: { byteStart: 0, byteEnd: 11 }, - features: [ - { - $type: `${ids.AppBskyRichtextFacet}#mention`, - did: sc.dids.carol, - }, - ], - }, - ], - createdAt, - } as AppBskyFeedPost.Record, - }) - 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.ctx.backgroundQueue.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) - - // Update - await services - .repo(db) - .processWrites({ did: sc.dids.alice, writes: [updateRecord] }, 1) - await server.ctx.backgroundQueue.processAll() - - const getAfterUpdate = await agent.api.app.bsky.feed.getPostThread( - { uri: uri.toString() }, - { 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.alice, writes: [deleteRecord] }, 1) - await server.ctx.backgroundQueue.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, - updateNotifications, - 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.ctx.backgroundQueue.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.ctx.backgroundQueue.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.ctx.backgroundQueue.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() - }) - - 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/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 30a41d1a44b..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 exmapleResp = JSON.parse(exampleRespBytes.toString()) - const classes = hive.respToClasses(exmapleResp) - 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 ea943dcf46d..00000000000 --- a/packages/pds/tests/labeler/labeler.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { AtUri, BlobRef } from '@atproto/api' -import stream from 'stream' -import { runTestServer, CloseFn } 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, streamToBytes, TID } from '@atproto/common' -import * as ui8 from 'uint8arrays' -import { LabelService } from '../../src/app-view/services/label' -import { BackgroundQueue } from '../../src/event-stream/background-queue' - -describe('labeler', () => { - 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 () => { - const server = await runTestServer({ - dbPostgresSchema: 'views_author_feed', - }) - 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) - }) - - 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() - 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() - 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() - - 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(img: stream.Readable): Promise { - const buf = await streamToBytes(img) - if (ui8.equals(buf, new Uint8Array([1, 2, 3, 4]))) { - return ['img-label'] - } - - if (ui8.equals(buf, new Uint8Array([5, 6, 7, 8]))) { - return ['other-img-label'] - } - return [] - } -} 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 8d6843a1136..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/uri' - -describe('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 d2a3f8f4f8f..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/uri' -import { Kysely } from 'kysely' - -describe('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/post-hierarchy.test.ts b/packages/pds/tests/migrations/post-hierarchy.test.ts deleted file mode 100644 index 145be7bb16c..00000000000 --- a/packages/pds/tests/migrations/post-hierarchy.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { AtpAgent } from '@atproto/api' -import { Database } from '../../src' -import { RecordRef, SeedClient } from '../seeds/client' -import usersSeed from '../seeds/users' -import threadSeed, { walk, item, Item } from '../seeds/thread' -import { CloseFn, runTestServer } from '../_util' - -describe('post hierarchy migration', () => { - let db: Database - let close: CloseFn - let sc: SeedClient - const threads: Item[] = [ - item(1, [item(2, [item(3), item(4)])]), - item(5, [item(6), item(7, [item(9, [item(11)]), item(10)]), item(8)]), - item(12), - ] - - beforeAll(async () => { - const server = await runTestServer({ - dbPostgresSchema: 'migration_post_hierarchy', - }) - db = server.ctx.db - close = server.close - const agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await usersSeed(sc) - await threadSeed(sc, sc.dids.alice, threads) - await db.migrateToOrThrow('_20230208T081544325Z') // Down to before index exists - await db.migrateToLatestOrThrow() // Build index from migration - }) - - afterAll(async () => { - await close() - }) - - it('indexes full post thread hierarchy.', async () => { - let closureSize = 0 - const itemByUri: Record = {} - - const postsAndReplies = ([] as { text: string; ref: RecordRef }[]) - .concat(Object.values(sc.posts[sc.dids.alice])) - .concat(Object.values(sc.replies[sc.dids.alice])) - - await walk(threads, async (item, depth) => { - const post = postsAndReplies.find((p) => p.text === String(item.id)) - if (!post) throw new Error('Post not found') - itemByUri[post.ref.uriStr] = item - closureSize += depth + 1 - }) - - const hierarchy = await db.db - .selectFrom('post_hierarchy') - .selectAll() - .execute() - - expect(hierarchy.length).toEqual(closureSize) - - for (const relation of hierarchy) { - const item = itemByUri[relation.uri] - const ancestor = itemByUri[relation.ancestorUri] - let depth = -1 - await walk([ancestor], async (candidate, candidateDepth) => { - if (candidate === item) { - depth = candidateDepth - } - }) - expect(depth).toEqual(relation.depth) - } - }) -}) 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 ad046836ed6..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('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 0b514c2e07f..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 { EcdsaKeypair, Keypair, randomStr } from '@atproto/crypto' -import { TID } from '@atproto/common' -import { Kysely } from 'kysely' -import { CID } from 'multiformats/cid' - -describe('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 EcdsaKeypair.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 048ccd9dacf..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('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 bda821a1539..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('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/image/fixtures/at.png b/packages/pds/tests/sample-img/at.png similarity index 100% rename from packages/pds/tests/image/fixtures/at.png rename to packages/pds/tests/sample-img/at.png diff --git a/packages/pds/tests/image/fixtures/hd-key.jpg b/packages/pds/tests/sample-img/hd-key.jpg similarity index 100% rename from packages/pds/tests/image/fixtures/hd-key.jpg rename to packages/pds/tests/sample-img/hd-key.jpg diff --git a/packages/pds/tests/image/fixtures/key-alt.jpg b/packages/pds/tests/sample-img/key-alt.jpg similarity index 100% rename from packages/pds/tests/image/fixtures/key-alt.jpg rename to packages/pds/tests/sample-img/key-alt.jpg diff --git a/packages/pds/tests/image/fixtures/key-landscape-large.jpg b/packages/pds/tests/sample-img/key-landscape-large.jpg similarity index 100% rename from packages/pds/tests/image/fixtures/key-landscape-large.jpg rename to packages/pds/tests/sample-img/key-landscape-large.jpg diff --git a/packages/pds/tests/image/fixtures/key-landscape-small.jpg b/packages/pds/tests/sample-img/key-landscape-small.jpg similarity index 100% rename from packages/pds/tests/image/fixtures/key-landscape-small.jpg rename to packages/pds/tests/sample-img/key-landscape-small.jpg diff --git a/packages/pds/tests/image/fixtures/key-portrait-large.jpg b/packages/pds/tests/sample-img/key-portrait-large.jpg similarity index 100% rename from packages/pds/tests/image/fixtures/key-portrait-large.jpg rename to packages/pds/tests/sample-img/key-portrait-large.jpg diff --git a/packages/pds/tests/image/fixtures/key-portrait-small.jpg b/packages/pds/tests/sample-img/key-portrait-small.jpg similarity index 100% rename from packages/pds/tests/image/fixtures/key-portrait-small.jpg rename to packages/pds/tests/sample-img/key-portrait-small.jpg 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 66d6982aedd..00000000000 --- a/packages/pds/tests/views/__snapshots__/author-feed.test.ts.snap +++ /dev/null @@ -1,1806 +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 [], - "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(2)", - "uri": "record(2)", - }, - "root": Object { - "cid": "cids(1)", - "uri": "record(1)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "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": "bobby", - "handle": "bob.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", - "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(2)", - "val": "test-label", - }, - Object { - "cid": "cids(2)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(2)", - "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(3)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(1)", - "uri": "record(1)", - }, - "root": Object { - "cid": "cids(1)", - "uri": "record(1)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "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 [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(1)", - "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(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(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [], - "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(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(7)", - "muted": false, - }, - }, - "cid": "cids(5)", - "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(10)", - "following": "record(9)", - "muted": false, - }, - }, - "cid": "cids(6)", - "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.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(7)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(8)", - "uri": "record(11)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - ], - "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(8)", - }, - }, - "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 [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(1)", - "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(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(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [], - "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", - "text": "hey there", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(12)", - "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 [], - "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(1)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(2)", - "uri": "record(1)", - }, - "root": Object { - "cid": "cids(2)", - "uri": "record(1)", - }, - }, - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", - "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(1)", - "viewer": Object { - "like": "record(4)", - }, - }, - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", - "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(1)", - "viewer": Object { - "like": "record(4)", - }, - }, - }, - }, - 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 [], - "viewer": Object { - "blockedBy": false, - "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.000+00:00", - "text": "bobby boy here", - }, - "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": "bobby", - "handle": "bob.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "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", - "text": "bob back at it again!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(6)", - "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 [], - "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", - "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(4)", - }, - }, - "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(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(6)", - "uri": "record(6)", - }, - "root": Object { - "cid": "cids(6)", - "uri": "record(6)", - }, - }, - "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(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [], - "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": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(6)", - "viewer": Object { - "like": "record(9)", - }, - }, - "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 [], - "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": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "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, - "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 [], - "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", - "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 [], - "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(3)", - "repost": "record(2)", - }, - }, - "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(1)", - "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(2)", - "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 [], - "viewer": Object { - "blockedBy": false, - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(5)", - "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", - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "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", - "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(6)", - }, - }, - }, - "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(2)", - "uri": "record(5)", - }, - }, - "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(4)", - "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(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(8)", - "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 [], - "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", - "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(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", - "text": "dan here!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(5)", - "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 [], - "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(1)", - "uri": "record(3)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "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": "bobby", - "handle": "bob.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "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(4)", - "val": "test-label", - }, - Object { - "cid": "cids(2)", - "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(3)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(1)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(1)", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(1)", - "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(5)", - }, - }, - }, - }, - 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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "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(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(5)", - "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(6)", - "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(3)", - }, - "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(8)", - "uri": "record(10)", - }, - }, - }, - "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(6)", - "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(4)", - "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(5)", - "uri": "record(8)", - }, - }, - "text": "yoohoo label_me", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(7)", - "viewer": Object { - "like": "record(11)", - }, - }, - }, - 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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(1)", - "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(5)", - }, - }, - }, - 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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "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": "hey there", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(12)", - "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 8c8e3bf3b7e..00000000000 --- a/packages/pds/tests/views/__snapshots__/blocks.test.ts.snap +++ /dev/null @@ -1,294 +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 [], - "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(1)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewBlocked", - "uri": "record(3)", - }, - }, - ], - "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.record", - "record": Object { - "cid": "cids(2)", - "uri": "record(3)", - }, - }, - "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(1)", - "uri": "record(2)", - }, - }, - "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", - "blocked": true, - "uri": "record(3)", - }, - "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 [], - "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(1)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(1)", - "uri": "record(3)", - }, - }, - "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 [], - "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(3)", - "repost": "record(2)", - }, - }, - "replies": Array [ - Object { - "$type": "app.bsky.feed.defs#blockedPost", - "blocked": true, - "uri": "record(4)", - }, - 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 [], - "viewer": Object { - "blockedBy": false, - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(1)", - "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(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(5)", - "val": "test-label", - }, - Object { - "cid": "cids(1)", - "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(2)", - }, - "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 {}, - }, - }, - ], - }, -} -`; 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 25a4518111e..00000000000 --- a/packages/pds/tests/views/__snapshots__/likes.test.ts.snap +++ /dev/null @@ -1,103 +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 [], - "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 624239d7a20..00000000000 --- a/packages/pds/tests/views/__snapshots__/mute-lists.test.ts.snap +++ /dev/null @@ -1,376 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -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 [], - "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(3)", - "repost": "record(2)", - }, - }, - "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(7)", - "following": "record(6)", - "muted": true, - "mutedByList": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "indexedAt": "1970-01-01T00:00:00.000Z", - "name": "alice mutes", - "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(5)", - "viewer": Object { - "muted": true, - }, - }, - }, - }, - "cid": "cids(1)", - "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(4)", - "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 [], - "viewer": Object { - "blockedBy": false, - "following": "record(9)", - "muted": true, - "mutedByList": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "indexedAt": "1970-01-01T00:00:00.000Z", - "name": "alice mutes", - "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(5)", - "viewer": Object { - "muted": true, - }, - }, - }, - }, - "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(8)", - "val": "test-label", - }, - Object { - "cid": "cids(2)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(8)", - "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(3)", - }, - "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(8)", - "viewer": Object {}, - }, - }, - ], -} -`; - -exports[`pds views with mutes from mute lists returns a users own list mutes 1`] = ` -Object { - "cursor": "0000000000000::bafycid", - "lists": Array [ - Object { - "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 [], - "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", - "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 [], - "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(2)", - "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 { - "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 [], - "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", - "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 [], - "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(2)", - "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", - "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 [], - "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", - "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", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "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 287b6ba633a..00000000000 --- a/packages/pds/tests/views/__snapshots__/notifications.test.ts.snap +++ /dev/null @@ -1,1511 +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(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(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(2)", - "displayName": "bobby", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", - "muted": false, - }, - }, - "cid": "cids(4)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [ - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(7)", - "val": "test-label", - }, - Object { - "cid": "cids(4)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(7)", - "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(5)", - }, - "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(7)", - }, - 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(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(1)", - "uri": "record(4)", - }, - }, - "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(7)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(12)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(8)", - "uri": "record(12)", - }, - }, - "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(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 { - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", - "muted": false, - }, - }, - "cid": "cids(10)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(12)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(8)", - "uri": "record(12)", - }, - }, - "uri": "record(14)", - }, - 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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", - "muted": false, - }, - }, - "cid": "cids(12)", - "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(9)", - }, - 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(13)", - "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 [], - "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(3)", - "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(1)", - }, - "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", - }, - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(3)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(5)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(4)", - "uri": "record(5)", - }, - }, - "uri": "record(4)", - }, - 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 [], - "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": "like", - "reasonSubject": "record(3)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(2)", - "uri": "record(3)", - }, - }, - "uri": "record(6)", - }, - 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 [], - "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": "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(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(5)", - }, - 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(4)", - "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(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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(10)", - "following": "record(9)", - "muted": false, - }, - }, - "cid": "cids(5)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": true, - "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(6)", - }, - "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(7)", - "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(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(8)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": true, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(13)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(9)", - "uri": "record(13)", - }, - }, - "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(10)", - "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(14)", - }, - 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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(10)", - "following": "record(9)", - "muted": false, - }, - }, - "cid": "cids(11)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": true, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(13)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(9)", - "uri": "record(13)", - }, - }, - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(10)", - "following": "record(9)", - "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 { - "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(13)", - "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(14)", - "uri": "record(18)", - }, - }, - "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(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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(10)", - "following": "record(9)", - "muted": false, - }, - }, - "cid": "cids(15)", - "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(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(16)", - "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(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(5)", - }, - 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(4)", - "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(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 [], - "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(6)", - }, - "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(7)", - "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(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(8)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(13)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(9)", - "uri": "record(13)", - }, - }, - "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(10)", - "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(14)", - }, - 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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(10)", - "following": "record(9)", - "muted": false, - }, - }, - "cid": "cids(11)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, - "labels": Array [], - "reason": "like", - "reasonSubject": "record(13)", - "record": Object { - "$type": "app.bsky.feed.like", - "createdAt": "1970-01-01T00:00:00.000Z", - "subject": Object { - "cid": "cids(9)", - "uri": "record(13)", - }, - }, - "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 [], - "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(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 { - "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(13)", - "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(14)", - "uri": "record(18)", - }, - }, - "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(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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(10)", - "following": "record(9)", - "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(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(16)", - "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 [], - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "muted": false, - }, - }, - "cid": "cids(3)", - "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 ab333105a2c..00000000000 --- a/packages/pds/tests/views/__snapshots__/posts.test.ts.snap +++ /dev/null @@ -1,395 +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 [], - "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", - "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 [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(1)", - "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(1)", - "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 [], - "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", - "text": "bob back at it again!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(2)", - "viewer": Object {}, - }, - 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(3)", - "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 [], - "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", - "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(4)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(2)", - "uri": "record(2)", - }, - }, - }, - "text": "hi im carol", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(5)", - "viewer": Object { - "like": "record(8)", - }, - }, - 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(10)", - "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": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(3)", - "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 [], - "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", - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "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", - "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(4)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(5)", - }, - "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(3)", - "uri": "record(5)", - }, - }, - "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 { - "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 [], - "viewer": Object { - "blockedBy": false, - "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(12)", - }, - "root": Object { - "cid": "cids(1)", - "uri": "record(1)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(11)", - "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 eba5c1c17eb..00000000000 --- a/packages/pds/tests/views/__snapshots__/profile.test.ts.snap +++ /dev/null @@ -1,218 +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 [], - "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 [], - "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(2)", - "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(3)", - "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 [], - "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 [], - "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 fc0beba5d9d..00000000000 --- a/packages/pds/tests/views/__snapshots__/reposts.test.ts.snap +++ /dev/null @@ -1,91 +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 [], - "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 b247e0b12f8..00000000000 --- a/packages/pds/tests/views/__snapshots__/thread.test.ts.snap +++ /dev/null @@ -1,1458 +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(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 [], - "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(1)", - "uri": "record(3)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object { - "repost": "record(5)", - }, - }, - "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(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 [], - "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(1)", - "uri": "record(3)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object { - "repost": "record(5)", - }, - }, - "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 [], - "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(3)", - }, - }, - "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 [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(1)", - "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(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(4)", - "val": "test-label", - }, - Object { - "cid": "cids(1)", - "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(2)", - }, - "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(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 [], - "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": 0, - "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(0)", - "uri": "record(0)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(5)", - "viewer": Object { - "repost": "record(6)", - }, - }, - "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 [], - "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(3)", - }, - }, - "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 [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(1)", - "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(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(4)", - "val": "test-label", - }, - Object { - "cid": "cids(1)", - "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(2)", - }, - "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(4)", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(1)", - "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(6)", - }, - }, - "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 [], - "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(4)", - "val": "test-label", - }, - Object { - "cid": "cids(2)", - "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(3)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(1)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(1)", - "uri": "record(3)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(4)", - "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 [], - "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(1)", - "uri": "record(3)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object { - "repost": "record(5)", - }, - }, - "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 [], - "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(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, - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(1)", - "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(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(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [], - "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(6)", - "val": "test-label", - }, - Object { - "cid": "cids(2)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(6)", - "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(3)", - }, - "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(6)", - "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 [], - "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(6)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(7)", - "viewer": Object { - "repost": "record(8)", - }, - }, - "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 [], - "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(4)", - "val": "test-label", - }, - Object { - "cid": "cids(2)", - "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(3)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(1)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(1)", - "uri": "record(3)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(4)", - "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 [], - "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(1)", - "uri": "record(3)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object { - "repost": "record(5)", - }, - }, - "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 [], - "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(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, - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(1)", - "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(4)", - "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 [], - "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(6)", - "val": "test-label", - }, - Object { - "cid": "cids(2)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(6)", - "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(3)", - }, - "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(6)", - "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 [], - "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 [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(1)", - "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(3)", - "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 [], - "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": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(1)", - "uri": "record(3)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "Reply reply", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(4)", - "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 [], - "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(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 [], - "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(1)", - "uri": "record(3)", - }, - }, - "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 [], - "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(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, - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(1)", - "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(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(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [], - "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(6)", - "val": "test-label", - }, - Object { - "cid": "cids(2)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(6)", - "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(3)", - }, - "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(6)", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", - "muted": true, - }, - }, - "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(6)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(7)", - "viewer": Object { - "repost": "record(8)", - }, - }, - "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 6b053a0a342..00000000000 --- a/packages/pds/tests/views/__snapshots__/timeline.test.ts.snap +++ /dev/null @@ -1,5133 +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 [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - "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(1)", - "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 [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(1)", - "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(3)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 0, - "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 [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(3)", - "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(1)", - "muted": false, - }, - }, - "cid": "cids(4)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewNotFound", - "uri": "record(6)", - }, - }, - ], - "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", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(5)", - "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", - }, - }, - }, - "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", - }, - ], - "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(4)", - "uri": "record(5)", - }, - }, - "text": "yoohoo label_me", - }, - "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(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - }, - 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(1)", - "muted": false, - }, - }, - "cid": "cids(4)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewNotFound", - "uri": "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(5)", - "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(5)", - "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(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", - "text": "dan 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": "ali", - "handle": "alice.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "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": "hey there", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(8)", - "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 [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - "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(1)", - "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 [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(1)", - "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(3)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "thanks bob", - }, - "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(6)", - "following": "record(5)", - "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", - "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(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 [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - "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 [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - }, - }, - 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 [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(4)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewNotFound", - "uri": "record(8)", - }, - }, - "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(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(5)", - "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(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "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(9)", - "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 [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - }, - 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(1)", - "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(12)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(8)", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "muted": false, - }, - }, - "cid": "cids(11)", - "embeds": Array [], - "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": "kind", - }, - ], - "uri": "record(14)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "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(13)", - "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(9)", - }, - "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(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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", - "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": "did:example:labeler", - "uri": "record(14)", - "val": "kind", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "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 [], - "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.000Z", - "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 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 [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - "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(1)", - "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(1)", - "muted": false, - }, - }, - "cid": "cids(1)", - "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(5)", - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(2)", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(5)", - "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(6)", - "val": "kind", - }, - ], - "uri": "record(6)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "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(3)", - "val": "kind", - }, - ], - "uri": "record(3)", - "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(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(6)", - }, - }, - }, - "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(2)", - "uri": "record(3)", - }, - }, - "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(2)", - "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(5)", - "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 [], - "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(10)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(9)", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(7)", - "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(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(10)", - "val": "test-label", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(10)", - "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(3)", - }, - "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(10)", - "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 [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(8)", - "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(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(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - "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 [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - }, - }, - 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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(7)", - "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(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(10)", - "val": "test-label", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(10)", - "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(3)", - }, - "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(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 [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - "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 [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - }, - }, - 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 [], - "viewer": Object { - "blockedBy": false, - "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(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, - }, - }, - "cid": "cids(1)", - "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(5)", - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(2)", - "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(3)", - "val": "kind", - }, - ], - "uri": "record(3)", - "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(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(6)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - ], - "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.record", - "record": Object { - "cid": "cids(2)", - "uri": "record(3)", - }, - }, - "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(9)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(12)", - "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(1)", - "uri": "record(2)", - }, - }, - "text": "yoohoo label_me", - }, - "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(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "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.000+00:00", - "text": "bobby boy here", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(13)", - "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 [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - }, - 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(1)", - "muted": false, - }, - }, - "cid": "cids(1)", - "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(5)", - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(2)", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(5)", - "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(6)", - "val": "kind", - }, - ], - "uri": "record(6)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "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(3)", - "val": "kind", - }, - ], - "uri": "record(3)", - "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(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(6)", - }, - }, - }, - "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(2)", - "uri": "record(3)", - }, - }, - "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(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(1)", - "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 { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "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(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(5)", - "embeds": Array [], - "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(6)", - "val": "kind", - }, - ], - "uri": "record(6)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "text": "bob back at it again!", - }, - }, - }, - }, - "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(3)", - "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(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(6)", - }, - }, - }, - "text": "hi im carol", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(3)", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(5)", - "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(6)", - "val": "kind", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "text": "bob back at it again!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(6)", - "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 [], - "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.000Z", - "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 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 [], - "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", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", - "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(7)", - "uri": "record(9)", - }, - "root": Object { - "cid": "cids(6)", - "uri": "record(8)", - }, - }, - "text": "thanks bob", - }, - "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(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(7)", - "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(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(9)", - "val": "test-label", - }, - Object { - "cid": "cids(7)", - "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(2)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(6)", - "uri": "record(8)", - }, - "root": Object { - "cid": "cids(6)", - "uri": "record(8)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(9)", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(6)", - "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(8)", - "viewer": Object { - "like": "record(10)", - }, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "following": "record(3)", - "muted": false, - }, - }, - "cid": "cids(8)", - "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(6)", - "uri": "record(8)", - }, - "root": Object { - "cid": "cids(6)", - "uri": "record(8)", - }, - }, - "text": "of course", - }, - "replyCount": 0, - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(6)", - "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(8)", - "viewer": Object { - "like": "record(10)", - }, - }, - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(6)", - "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(8)", - "viewer": Object { - "like": "record(10)", - }, - }, - }, - }, - 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 [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(7)", - "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(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(9)", - "val": "test-label", - }, - Object { - "cid": "cids(7)", - "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(2)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(6)", - "uri": "record(8)", - }, - "root": Object { - "cid": "cids(6)", - "uri": "record(8)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(9)", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(6)", - "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(8)", - "viewer": Object { - "like": "record(10)", - }, - }, - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(6)", - "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(8)", - "viewer": Object { - "like": "record(10)", - }, - }, - }, - }, - 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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", - "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(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(9)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(12)", - "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(12)", - "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(3)", - "displayName": "bobby", - "handle": "bob.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.000+00:00", - "text": "bobby boy 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(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(6)", - "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(8)", - "viewer": Object { - "like": "record(10)", - }, - }, - }, - 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 [], - "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", - "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(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 [], - "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", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", - "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": "hey there", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(16)", - "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 [], - "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", - "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(4)", - }, - }, - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", - "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(7)", - "uri": "record(9)", - }, - "root": Object { - "cid": "cids(6)", - "uri": "record(8)", - }, - }, - "text": "thanks bob", - }, - "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(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "muted": false, - }, - }, - "cid": "cids(7)", - "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(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(9)", - "val": "test-label", - }, - Object { - "cid": "cids(7)", - "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(2)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(6)", - "uri": "record(8)", - }, - "root": Object { - "cid": "cids(6)", - "uri": "record(8)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(9)", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(6)", - "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(8)", - "viewer": Object { - "like": "record(10)", - }, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(8)", - "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(6)", - "uri": "record(8)", - }, - "root": Object { - "cid": "cids(6)", - "uri": "record(8)", - }, - }, - "text": "of course", - }, - "replyCount": 0, - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(6)", - "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(8)", - "viewer": Object { - "like": "record(10)", - }, - }, - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(6)", - "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(8)", - "viewer": Object { - "like": "record(10)", - }, - }, - }, - }, - 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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", - "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(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(9)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(12)", - "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(12)", - "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(1)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", - "muted": false, - }, - }, - "cid": "cids(6)", - "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(8)", - "viewer": Object { - "like": "record(10)", - }, - }, - }, - 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 [], - "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", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", - "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": "hey there", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(14)", - "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 [], - "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(3)", - "repost": "record(2)", - }, - }, - "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 [], - "viewer": Object { - "blockedBy": false, - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(1)", - "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(1)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(4)", - "val": "test-label", - }, - Object { - "cid": "cids(1)", - "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(2)", - }, - "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(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 [], - "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(3)", - "repost": "record(2)", - }, - }, - "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 [], - "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(3)", - "repost": "record(2)", - }, - }, - }, - }, - 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 [], - "viewer": Object { - "blockedBy": false, - "following": "record(5)", - "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.000+00:00", - "text": "bobby boy here", - }, - "replyCount": 0, - "repostCount": 0, - "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, - "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(3)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "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(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "following": "record(5)", - "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", - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "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": "kind", - }, - ], - "uri": "record(8)", - "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(6)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(7)", - "uri": "record(9)", - }, - }, - }, - "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(8)", - }, - }, - "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(7)", - "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(8)", - "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(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(2)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "following": "record(5)", - "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", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "text": "bob back at it again!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(9)", - "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 [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - "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(1)", - "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 [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(1)", - "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(3)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(2)", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": true, - }, - }, - "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(3)", - "val": "test-label", - }, - Object { - "cid": "cids(2)", - "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(3)", - }, - "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(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 [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - }, - }, - 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 [], - "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(1)", - "muted": false, - }, - }, - "cid": "cids(5)", - "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(10)", - "following": "record(9)", - "muted": true, - }, - }, - "cid": "cids(6)", - "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": "kind", - }, - ], - "uri": "record(8)", - "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(3)", - }, - "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(8)", - "uri": "record(11)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - ], - "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.record", - "record": Object { - "cid": "cids(6)", - "uri": "record(8)", - }, - }, - "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(6)", - "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(7)", - }, - }, - "text": "yoohoo label_me", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(6)", - "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 [], - "viewer": Object { - "blockedBy": false, - "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 {}, - }, - }, - 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(1)", - "muted": false, - }, - }, - "cid": "cids(5)", - "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(10)", - "following": "record(9)", - "muted": true, - }, - }, - "cid": "cids(6)", - "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 [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "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(11)", - "val": "kind", - }, - ], - "uri": "record(11)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "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": "kind", - }, - ], - "uri": "record(8)", - "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(3)", - }, - "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(8)", - "uri": "record(11)", - }, - }, - }, - "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(6)", - "uri": "record(8)", - }, - }, - "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(7)", - "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(1)", - "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(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 [], - "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": "hey there", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(13)", - "viewer": Object {}, - }, - }, -] -`; 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 47753a8de6b..00000000000 --- a/packages/pds/tests/views/actor-search.test.ts +++ /dev/null @@ -1,440 +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.ctx.backgroundQueue.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)) - - if (db.dialect === 'pg') { - expect(forSnapshot(result.data.actors)).toEqual(snapTypeaheadPg) - } else { - expect(forSnapshot(result.data.actors)).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 }, - ) - - expect(limited.data.actors).toEqual(full.data.actors.slice(0, 5)) - }) - - 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)) - - if (db.dialect === 'pg') { - expect(forSnapshot(result.data.actors)).toEqual(snapSearchPg) - } else { - expect(forSnapshot(result.data.actors)).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) - expect(results(paginatedAll)).toEqual(results([full.data])) - }) - - 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)', - handle: 'cara-wiegand69.test', - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(1)', - displayName: 'Carol Littel', - handle: 'eudora-dietrich4.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(2)', - displayName: 'Sadie Carter', - handle: 'shane-torphy52.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(3)', - displayName: 'Carlton Abernathy IV', - handle: 'aliya-hodkiewicz.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(4)', - handle: 'carlos6.test', - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(5)', - displayName: 'Latoya Windler', - handle: 'carolina-mcdermott77.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(6)', - displayName: 'Rachel Kshlerin', - handle: 'cayla-marquardt39.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)', - handle: 'cara-wiegand69.test', - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(1)', - displayName: 'Carol Littel', - indexedAt: '1970-01-01T00:00:00.000Z', - handle: 'eudora-dietrich4.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(2)', - displayName: 'Sadie Carter', - indexedAt: '1970-01-01T00:00:00.000Z', - handle: 'shane-torphy52.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(3)', - displayName: 'Carlton Abernathy IV', - indexedAt: '1970-01-01T00:00:00.000Z', - handle: 'aliya-hodkiewicz.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(4)', - handle: 'carlos6.test', - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(5)', - displayName: 'Latoya Windler', - indexedAt: '1970-01-01T00:00:00.000Z', - handle: 'carolina-mcdermott77.test', - avatar, - viewer: { blockedBy: false, muted: false }, - labels: [], - }, - { - did: 'user(6)', - displayName: 'Rachel Kshlerin', - indexedAt: '1970-01-01T00:00:00.000Z', - handle: 'cayla-marquardt39.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 68cdd0bc48b..00000000000 --- a/packages/pds/tests/views/author-feed.test.ts +++ /dev/null @@ -1,249 +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 basicSeed from '../seeds/basic' - -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 - - 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.ctx.backgroundQueue.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 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 { data: postBlock } = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: alice }, - { headers: sc.getHeaders(carol) }, - ) - - expect(postBlock.feed.length).toEqual(0) - - // Cleanup - await agent.api.com.atproto.admin.reverseModerationAction( - { - id: action.id, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - }) - - 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 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() }, - }, - ) - - 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 agent.api.com.atproto.admin.reverseModerationAction( - { - id: action.id, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - }) -}) diff --git a/packages/pds/tests/views/blocks.test.ts b/packages/pds/tests/views/blocks.test.ts deleted file mode 100644 index 4d72e639b1d..00000000000 --- a/packages/pds/tests/views/blocks.test.ts +++ /dev/null @@ -1,327 +0,0 @@ -import AtpAgent from '@atproto/api' -import { runTestServer, CloseFn, TestServerInfo, forSnapshot } from '../_util' -import { SeedClient } from '../seeds/client' -import basicSeed from '../seeds/basic' -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' - -describe('pds views with blocking', () => { - let server: TestServerInfo - let agent: AtpAgent - let close: CloseFn - let sc: SeedClient - let aliceReplyToDan: { ref: RecordRef } - - let alice: string - let carol: string - let dan: 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) - // dan blocks carol - await agent.api.app.bsky.graph.block.create( - { repo: dan }, - { createdAt: new Date().toISOString(), subject: carol }, - sc.getHeaders(dan), - ) - aliceReplyToDan = await sc.reply( - alice, - sc.posts[dan][0].ref, - sc.posts[dan][0].ref, - 'alice replies to dan', - ) - await server.ctx.backgroundQueue.processAll() - }) - - afterAll(async () => { - await close() - }) - - it('blocks thread post', async () => { - const { carol, dan } = sc.dids - 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, - }, - }) - 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, - }, - }) - }) - - 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 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('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 982aa2c0d29..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.ctx.backgroundQueue.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 10bef798aba..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.ctx.backgroundQueue.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 4ce32569c1b..00000000000 --- a/packages/pds/tests/views/mute-lists.test.ts +++ /dev/null @@ -1,336 +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' - -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.ctx.backgroundQueue.processAll() - }) - - afterAll(async () => { - await close() - }) - - let listUri: 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 - 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) - }) -}) diff --git a/packages/pds/tests/views/mutes.test.ts b/packages/pds/tests/views/mutes.test.ts deleted file mode 100644 index a3a17eff595..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.ctx.backgroundQueue.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 67b00331e3c..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.ctx.backgroundQueue.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(11) - - 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.ctx.backgroundQueue.processAll() - - 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(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(12) - - 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(10) - expect(forSnapshot(notifs)).toMatchSnapshot() - expect(notifCount.data.count).toBe(10) - - // 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(12) - 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(12) - - 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 1456f4cbc33..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.ctx.backgroundQueue.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.ctx.backgroundQueue.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 55b2312cf12..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.ctx.labeler.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 recivedUris = 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(recivedUris).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 1f68946811d..00000000000 --- a/packages/pds/tests/views/profile.test.ts +++ /dev/null @@ -1,271 +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.ctx.backgroundQueue.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("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 5e4eb6e0b37..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.ctx.backgroundQueue.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 1c075fe17a8..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.ctx.backgroundQueue.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 0f4fa68b35e..00000000000 --- a/packages/pds/tests/views/thread.test.ts +++ /dev/null @@ -1,489 +0,0 @@ -import AtpAgent, { AppBskyFeedGetPostThread } from '@atproto/api' -import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' -import { Database } from '../../src' -import { - runTestServer, - forSnapshot, - CloseFn, - adminAuth, - TestServerInfo, -} from '../_util' -import { RecordRef, SeedClient } from '../seeds/client' -import basicSeed from '../seeds/basic' -import threadSeed, { walk, item, Item } from '../seeds/thread' - -describe('pds thread views', () => { - let server: TestServerInfo - let agent: AtpAgent - let db: Database - 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', - }) - db = server.ctx.db - 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 - }) - - 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.ctx.backgroundQueue.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.ctx.backgroundQueue.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.ctx.backgroundQueue.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('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() }, - }, - ), - ), - ) - }) - - it('builds post hierarchy index.', async () => { - const threads: Item[] = [ - item(1, [item(2, [item(3), item(4)])]), - item(5, [item(6), item(7, [item(9, [item(11)]), item(10)]), item(8)]), - item(12), - ] - await threadSeed(sc, sc.dids.alice, threads) - let closureSize = 0 - const itemByUri: Record = {} - - const postsAndReplies = ([] as { text: string; ref: RecordRef }[]) - .concat(Object.values(sc.posts[sc.dids.alice])) - .concat(Object.values(sc.replies[sc.dids.alice])) - .filter((p) => { - const id = parseInt(p.text, 10) - return 0 < id && id <= 12 - }) - - await walk(threads, async (item, depth) => { - const post = postsAndReplies.find((p) => p.text === String(item.id)) - if (!post) throw new Error('Post not found') - itemByUri[post.ref.uriStr] = item - closureSize += depth + 1 - }) - - const hierarchy = await db.db - .selectFrom('post_hierarchy') - .where( - 'uri', - 'in', - postsAndReplies.map((p) => p.ref.uriStr), - ) - .orWhere( - 'ancestorUri', - 'in', - postsAndReplies.map((p) => p.ref.uriStr), - ) - .selectAll() - .execute() - - expect(hierarchy.length).toEqual(closureSize) - - for (const relation of hierarchy) { - const item = itemByUri[relation.uri] - const ancestor = itemByUri[relation.ancestorUri] - let depth = -1 - await walk([ancestor], async (candidate, candidateDepth) => { - if (candidate === item) { - depth = candidateDepth - } - }) - expect(depth).toEqual(relation.depth) - } - }) -}) diff --git a/packages/pds/tests/views/timeline.test.ts b/packages/pds/tests/views/timeline.test.ts deleted file mode 100644 index dc0d10a79cb..00000000000 --- a/packages/pds/tests/views/timeline.test.ts +++ /dev/null @@ -1,283 +0,0 @@ -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.ctx.backgroundQueue.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('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() }, - }, - ), - ), - ) - }) -}) From 69f3727d462649f1848113e75c023de7a81575ce Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 12 Jun 2023 19:45:06 -0500 Subject: [PATCH 015/105] fix up a bunch of tests --- .../bsky/src/db/tables/duplicate-record.ts | 12 - packages/dev-env/src/pds.ts | 6 - packages/pds/src/api/app/bsky/proxied.ts | 4 +- .../pds/src/api/com/atproto/sync/getBlob.ts | 11 +- packages/pds/tests/account-deletion.test.ts | 23 -- packages/pds/tests/blob-deletes.test.ts | 16 +- packages/pds/tests/crud.test.ts | 4 +- packages/pds/tests/duplicate-records.test.ts | 179 ------------- packages/pds/tests/file-uploads.test.ts | 34 +-- packages/pds/tests/handles.test.ts | 67 ++--- packages/pds/tests/moderation.test.ts | 240 +----------------- .../__snapshots__/feedgen.test.ts.snap | 20 +- .../proxied/__snapshots__/views.test.ts.snap | 48 ++-- packages/pds/tests/seeds/basic.ts | 6 +- packages/pds/tests/seeds/client.ts | 4 +- packages/pds/tests/sync/sync.test.ts | 4 +- 16 files changed, 96 insertions(+), 582 deletions(-) delete mode 100644 packages/bsky/src/db/tables/duplicate-record.ts delete mode 100644 packages/pds/tests/duplicate-records.test.ts diff --git a/packages/bsky/src/db/tables/duplicate-record.ts b/packages/bsky/src/db/tables/duplicate-record.ts deleted file mode 100644 index 3cad0cd148b..00000000000 --- a/packages/bsky/src/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/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index a0405a909f5..510d98e61c9 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 } 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' @@ -74,11 +73,6 @@ export class TestPds { : pds.Database.memory() await db.migrateToLatestOrThrow() - if (config.bskyAppViewEndpoint) { - // Disable communication to app view within pds - MessageDispatcher.prototype.send = async () => {} - } - const server = pds.PDS.create({ db, blobstore, diff --git a/packages/pds/src/api/app/bsky/proxied.ts b/packages/pds/src/api/app/bsky/proxied.ts index c52e863dbcf..b2bf7994481 100644 --- a/packages/pds/src/api/app/bsky/proxied.ts +++ b/packages/pds/src/api/app/bsky/proxied.ts @@ -188,11 +188,11 @@ export default function (server: Server, ctx: AppContext) { }, }) - server.app.bsky.feed.getPostThread({ + server.app.bsky.feed.getLikes({ auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.feed.getPostThread( + const res = await ctx.appviewAgent.api.app.bsky.feed.getLikes( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/com/atproto/sync/getBlob.ts b/packages/pds/src/api/com/atproto/sync/getBlob.ts index 1fda2236927..9ab0c89562d 100644 --- a/packages/pds/src/api/com/atproto/sync/getBlob.ts +++ b/packages/pds/src/api/com/atproto/sync/getBlob.ts @@ -2,6 +2,7 @@ import { CID } from 'multiformats/cid' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { InvalidRequestError } from '@atproto/xrpc-server' +import { BlobNotFoundError } from '@atproto/repo' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getBlob(async ({ params, res }) => { @@ -15,7 +16,15 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError(`blob not found: ${params.cid}`) } const cid = CID.parse(params.cid) - const blobStream = await ctx.blobstore.getStream(cid) + let blobStream + try { + blobStream = await ctx.blobstore.getStream(cid) + } catch (err) { + if (err instanceof BlobNotFoundError) { + throw new InvalidRequestError('Blob not found') + } + throw err + } res.setHeader('content-length', found.size) res.setHeader('x-content-type-options', 'nosniff') res.setHeader('content-security-policy', `default-src 'none'; sandbox`) diff --git a/packages/pds/tests/account-deletion.test.ts b/packages/pds/tests/account-deletion.test.ts index dea79d262b4..7eb85aedf98 100644 --- a/packages/pds/tests/account-deletion.test.ts +++ b/packages/pds/tests/account-deletion.test.ts @@ -190,29 +190,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/blob-deletes.test.ts b/packages/pds/tests/blob-deletes.test.ts index bf902b57169..a2effc21ea8 100644 --- a/packages/pds/tests/blob-deletes.test.ts +++ b/packages/pds/tests/blob-deletes.test.ts @@ -53,7 +53,7 @@ describe('blob deletes', () => { it('deletes blob when record is deleted', async () => { const img = await sc.uploadFile( alice, - 'tests/image/fixtures/key-portrait-small.jpg', + 'tests/sample-img/key-portrait-small.jpg', 'image/jpeg', ) const post = await sc.post(alice, 'test', undefined, [img]) @@ -70,12 +70,12 @@ describe('blob deletes', () => { it('deletes blob when blob-ref in record is updated', async () => { const img = await sc.uploadFile( alice, - 'tests/image/fixtures/key-portrait-small.jpg', + 'tests/sample-img/key-portrait-small.jpg', 'image/jpeg', ) const img2 = await sc.uploadFile( alice, - 'tests/image/fixtures/key-landscape-small.jpg', + 'tests/sample-img/key-landscape-small.jpg', 'image/jpeg', ) await updateProfile(sc, alice, img.image, img.image) @@ -99,12 +99,12 @@ describe('blob deletes', () => { it('does not delete blob when blob-ref in record is not updated', async () => { const img = await sc.uploadFile( alice, - 'tests/image/fixtures/key-portrait-small.jpg', + 'tests/sample-img/key-portrait-small.jpg', 'image/jpeg', ) const img2 = await sc.uploadFile( alice, - 'tests/image/fixtures/key-landscape-small.jpg', + 'tests/sample-img/key-landscape-small.jpg', 'image/jpeg', ) await updateProfile(sc, alice, img.image, img.image) @@ -125,7 +125,7 @@ describe('blob deletes', () => { it('does not delete blob when blob is reused by another record in same commit', async () => { const img = await sc.uploadFile( alice, - 'tests/image/fixtures/key-portrait-small.jpg', + 'tests/sample-img/key-portrait-small.jpg', 'image/jpeg', ) const post = await sc.post(alice, 'post', undefined, [img]) @@ -172,12 +172,12 @@ describe('blob deletes', () => { it('does not delete blob from blob store if another user is using it', async () => { const imgAlice = await sc.uploadFile( alice, - 'tests/image/fixtures/key-landscape-small.jpg', + 'tests/sample-img/key-landscape-small.jpg', 'image/jpeg', ) const imgBob = await sc.uploadFile( bob, - 'tests/image/fixtures/key-landscape-small.jpg', + 'tests/sample-img/key-landscape-small.jpg', 'image/jpeg', ) const postAlice = await sc.post(alice, 'post', undefined, [imgAlice]) diff --git a/packages/pds/tests/crud.test.ts b/packages/pds/tests/crud.test.ts index fee0007ea3f..af4aedb81c0 100644 --- a/packages/pds/tests/crud.test.ts +++ b/packages/pds/tests/crud.test.ts @@ -174,9 +174,7 @@ describe('crud operations', () => { }) it('attaches images to a post', async () => { - const file = await fs.readFile( - 'tests/image/fixtures/key-landscape-small.jpg', - ) + const file = await fs.readFile('tests/sample-img/key-landscape-small.jpg') const uploadedRes = await aliceAgent.api.com.atproto.repo.uploadBlob(file, { encoding: 'image/jpeg', }) diff --git a/packages/pds/tests/duplicate-records.test.ts b/packages/pds/tests/duplicate-records.test.ts deleted file mode 100644 index 2815c0bcb36..00000000000 --- a/packages/pds/tests/duplicate-records.test.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/uri' -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/file-uploads.test.ts b/packages/pds/tests/file-uploads.test.ts index fa36a4fb473..097b797103e 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' @@ -94,7 +92,7 @@ describe('file uploads', () => { }) it('uploads files', async () => { - smallFile = await fs.readFile('tests/image/fixtures/key-portrait-small.jpg') + smallFile = await fs.readFile('tests/sample-img/key-portrait-small.jpg') const res = await aliceAgent.api.com.atproto.repo.uploadBlob(smallFile, { encoding: 'image/jpeg', }) @@ -147,29 +145,11 @@ 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 it('does not allow referencing a file that is outside blob constraints', async () => { - largeFile = await fs.readFile('tests/image/fixtures/hd-key.jpg') + largeFile = await fs.readFile('tests/sample-img/hd-key.jpg') const res = await aliceAgent.api.com.atproto.repo.uploadBlob(largeFile, { encoding: 'image/jpeg', }) @@ -195,9 +175,7 @@ describe('file uploads', () => { }) it('permits duplicate uploads of the same file', async () => { - const file = await fs.readFile( - 'tests/image/fixtures/key-landscape-small.jpg', - ) + const file = await fs.readFile('tests/sample-img/key-landscape-small.jpg') const { data: uploadA } = await aliceAgent.api.com.atproto.repo.uploadBlob( file, { @@ -257,9 +235,7 @@ describe('file uploads', () => { }) it('corrects a bad mimetype', async () => { - const file = await fs.readFile( - 'tests/image/fixtures/key-landscape-large.jpg', - ) + const file = await fs.readFile('tests/sample-img/key-landscape-large.jpg') const res = await aliceAgent.api.com.atproto.repo.uploadBlob(file, { encoding: 'video/mp4', } as any) @@ -276,7 +252,7 @@ describe('file uploads', () => { }) it('handles pngs', async () => { - const file = await fs.readFile('tests/image/fixtures/at.png') + const file = await fs.readFile('tests/sample-img/at.png') const res = await aliceAgent.api.com.atproto.repo.uploadBlob(file, { encoding: 'image/png', }) diff --git a/packages/pds/tests/handles.test.ts b/packages/pds/tests/handles.test.ts index 50acddb0656..7fdde0ce2fa 100644 --- a/packages/pds/tests/handles.test.ts +++ b/packages/pds/tests/handles.test.ts @@ -51,6 +51,15 @@ describe('handles', () => { await close() }) + const getDbHandle = async (did: string): Promise => { + const res = await ctx.db.db + .selectFrom('did_handle') + .selectAll() + .where('did', '=', did) + .executeTakeFirst() + return res?.handle ?? null + } + it('resolves handles', async () => { const res = await agent.api.com.atproto.identity.resolveHandle({ handle: 'alice.test', @@ -94,35 +103,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' }, @@ -188,11 +168,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 handle = await getDbHandle(alice) + expect(handle).toBe('alice.external') const data = await idResolver.did.resolveAtprotoData(alice) expect(data.handle).toBe('alice.external') @@ -219,11 +196,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 handle = await getDbHandle(alice) + expect(handle).toBe('alice.external') }) it('allows admin overrules of service domains', async () => { @@ -237,12 +211,8 @@ describe('handles', () => { encoding: 'application/json', }, ) - - const profile = await agent.api.app.bsky.actor.getProfile( - { actor: bob }, - { headers: sc.getHeaders(bob) }, - ) - expect(profile.data.handle).toBe('bob-alt.test') + const handle = await getDbHandle(bob) + expect(handle).toBe('bob-alt.test') }) it('allows admin override of reserved domains', async () => { @@ -257,11 +227,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 handle = await getDbHandle(bob) + expect(handle).toBe('dril.test') }) it('disallows setting handle to an off-service domain', async () => { diff --git a/packages/pds/tests/moderation.test.ts b/packages/pds/tests/moderation.test.ts index caf645b3c9d..235f302117a 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/uri' import { adminAuth, @@ -824,164 +824,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 non-admin 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: moderatorAuth() }, - }, - ) - await expect(attemptLabel).rejects.toThrow( - 'Must be an admin to takedown or label content', - ) - }) - it('does not allow non-admin moderators to takedown.', async () => { const attemptTakedown = agent.api.com.atproto.admin.takeModerationAction( { @@ -1002,75 +844,15 @@ describe('moderation', () => { 'Must be an admin to takedown or label content', ) }) - - 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( - { - id: actionId, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - } - - 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', () => { 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, @@ -1099,7 +881,7 @@ describe('moderation', () => { it('prevents blob from being referenced again.', async () => { const uploaded = await sc.uploadFile( sc.dids.alice, - 'tests/image/fixtures/key-alt.jpg', + 'tests/sample-img/key-alt.jpg', 'image/jpeg', ) expect(uploaded.image.ref.equals(blob.image.ref)).toBeTruthy() @@ -1108,9 +890,11 @@ describe('moderation', () => { }) 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' }) + const attempt = agent.api.com.atproto.sync.getBlob({ + did: sc.dids.carol, + cid: blob.image.ref.toString(), + }) + await expect(attempt).rejects.toThrow('Blob not found') }) it('restores blob when action is reversed.', async () => { @@ -1131,10 +915,12 @@ describe('moderation', () => { 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) + const res = await agent.api.com.atproto.sync.getBlob({ + did: sc.dids.carol, + cid: blob.image.ref.toString(), + }) + + expect(res.data.byteLength).toBeGreaterThan(9000) }) }) }) diff --git a/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap index 7495feae3dc..67a06917585 100644 --- a/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap @@ -62,7 +62,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(3)/cids(4)@jpeg", "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(3)/cids(4)@jpeg", }, @@ -95,7 +95,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -267,7 +267,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(3)/cids(4)@jpeg", "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(3)/cids(4)@jpeg", }, @@ -300,7 +300,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -444,7 +444,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -455,7 +455,7 @@ Object { }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -618,12 +618,12 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(4)@jpeg", "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(4)@jpeg", }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(9)@jpeg", "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(9)@jpeg", }, @@ -670,7 +670,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -681,7 +681,7 @@ Object { }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", diff --git a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap index a546785680e..709b884a53f 100644 --- a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap @@ -222,7 +222,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(1)/cids(2)@jpeg", "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(1)/cids(2)@jpeg", }, @@ -255,7 +255,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -554,12 +554,12 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(3)/cids(3)@jpeg", "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(3)/cids(3)@jpeg", }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(3)/cids(4)@jpeg", "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(3)/cids(4)@jpeg", }, @@ -606,7 +606,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -617,7 +617,7 @@ Object { }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -751,12 +751,12 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(4)@jpeg", "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(4)@jpeg", }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(5)@jpeg", "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", }, @@ -803,7 +803,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -814,7 +814,7 @@ Object { }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -945,7 +945,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(4)@jpeg", "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(4)@jpeg", }, @@ -978,7 +978,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1150,7 +1150,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(4)@jpeg", "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(4)@jpeg", }, @@ -1183,7 +1183,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1327,7 +1327,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1338,7 +1338,7 @@ Object { }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1517,12 +1517,12 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(4)@jpeg", "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(4)@jpeg", }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(5)@jpeg", "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", }, @@ -1569,7 +1569,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1580,7 +1580,7 @@ Object { }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1685,12 +1685,12 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(4)@jpeg", "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(4)@jpeg", }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(5)@jpeg", "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", }, @@ -1737,7 +1737,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1748,7 +1748,7 @@ Object { }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", diff --git a/packages/pds/tests/seeds/basic.ts b/packages/pds/tests/seeds/basic.ts index dffcf677cc4..983dcaddcb9 100644 --- a/packages/pds/tests/seeds/basic.ts +++ b/packages/pds/tests/seeds/basic.ts @@ -29,12 +29,12 @@ export default async (sc: SeedClient) => { await sc.post(bob, posts.bob[0]) const img1 = await sc.uploadFile( carol, - 'tests/image/fixtures/key-landscape-small.jpg', + 'tests/sample-img/key-landscape-small.jpg', 'image/jpeg', ) const img2 = await sc.uploadFile( carol, - 'tests/image/fixtures/key-alt.jpg', + 'tests/sample-img/key-alt.jpg', 'image/jpeg', ) await sc.post( @@ -95,7 +95,7 @@ export default async (sc: SeedClient) => { const replyImg = await sc.uploadFile( bob, - 'tests/image/fixtures/key-landscape-small.jpg', + 'tests/sample-img/key-landscape-small.jpg', 'image/jpeg', ) await sc.reply( diff --git a/packages/pds/tests/seeds/client.ts b/packages/pds/tests/seeds/client.ts index c5cbcd0a5b5..639db353a45 100644 --- a/packages/pds/tests/seeds/client.ts +++ b/packages/pds/tests/seeds/client.ts @@ -123,9 +123,7 @@ export class SeedClient { description: string, fromUser?: string, ) { - AVATAR_IMG ??= await fs.readFile( - 'tests/image/fixtures/key-portrait-small.jpg', - ) + AVATAR_IMG ??= await fs.readFile('tests/sample-img/key-portrait-small.jpg') let avatarBlob { diff --git a/packages/pds/tests/sync/sync.test.ts b/packages/pds/tests/sync/sync.test.ts index 6cc07e7f66b..543f5290551 100644 --- a/packages/pds/tests/sync/sync.test.ts +++ b/packages/pds/tests/sync/sync.test.ts @@ -267,12 +267,12 @@ describe('repo sync', () => { it('syncs images', async () => { const img1 = await sc.uploadFile( did, - 'tests/image/fixtures/key-landscape-small.jpg', + 'tests/sample-img/key-landscape-small.jpg', 'image/jpeg', ) const img2 = await sc.uploadFile( did, - 'tests/image/fixtures/key-portrait-small.jpg', + 'tests/sample-img/key-portrait-small.jpg', 'image/jpeg', ) await sc.post(did, 'blah', undefined, [img1]) From 9c4667d5364ed4aeb012a234ea2a957e54e36eee Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 12 Jun 2023 19:51:53 -0500 Subject: [PATCH 016/105] moderation view tests --- .../get-moderation-action.test.ts.snap | 34 +--- .../get-moderation-report.test.ts.snap | 32 +--- .../__snapshots__/get-record.test.ts.snap | 158 +----------------- .../admin/__snapshots__/get-repo.test.ts.snap | 122 +------------- .../tests/admin/get-moderation-action.test.ts | 8 +- .../admin/get-moderation-actions.test.ts | 8 +- .../tests/admin/get-moderation-report.test.ts | 8 +- .../admin/get-moderation-reports.test.ts | 8 +- packages/pds/tests/admin/get-record.test.ts | 33 +--- packages/pds/tests/admin/get-repo.test.ts | 30 +--- packages/pds/tests/admin/invites.test.ts | 4 +- 11 files changed, 34 insertions(+), 411 deletions(-) diff --git a/packages/pds/tests/admin/__snapshots__/get-moderation-action.test.ts.snap b/packages/pds/tests/admin/__snapshots__/get-moderation-action.test.ts.snap index 2c36fee7ee9..16a4eb16000 100644 --- a/packages/pds/tests/admin/__snapshots__/get-moderation-action.test.ts.snap +++ b/packages/pds/tests/admin/__snapshots__/get-moderation-action.test.ts.snap @@ -43,21 +43,7 @@ Object { "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", - }, - ], + "relatedRecords": Array [], }, "uri": "record(0)", "value": Object { @@ -90,7 +76,7 @@ Object { ], "subject": Object { "$type": "com.atproto.repo.strongRef", - "cid": "cids(1)", + "cid": "cids(0)", "uri": "record(0)", }, }, @@ -121,21 +107,7 @@ Object { "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": "its me!", - "displayName": "ali", - }, - ], + "relatedRecords": Array [], }, "subjectBlobs": Array [], } diff --git a/packages/pds/tests/admin/__snapshots__/get-moderation-report.test.ts.snap b/packages/pds/tests/admin/__snapshots__/get-moderation-report.test.ts.snap index ef546a504d1..4ef1f8e812b 100644 --- a/packages/pds/tests/admin/__snapshots__/get-moderation-report.test.ts.snap +++ b/packages/pds/tests/admin/__snapshots__/get-moderation-report.test.ts.snap @@ -64,21 +64,7 @@ Object { "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", - }, - ], + "relatedRecords": Array [], }, "uri": "record(0)", "value": Object { @@ -127,21 +113,7 @@ Object { "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": "its me!", - "displayName": "ali", - }, - ], + "relatedRecords": Array [], }, } `; 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 256c66a4203..d6b7edd6468 100644 --- a/packages/pds/tests/admin/__snapshots__/get-record.test.ts.snap +++ b/packages/pds/tests/admin/__snapshots__/get-record.test.ts.snap @@ -84,21 +84,7 @@ Object { "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", - }, - ], + "relatedRecords": Array [], }, "uri": "record(0)", "value": Object { @@ -193,147 +179,7 @@ Object { "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", - }, - ], - }, - "uri": "record(0)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "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", - }, - ], - "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", - }, - ], + "relatedRecords": Array [], }, "uri": "record(0)", "value": 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 4e93c13e2c2..1c8e4cdd217 100644 --- a/packages/pds/tests/admin/__snapshots__/get-repo.test.ts.snap +++ b/packages/pds/tests/admin/__snapshots__/get-repo.test.ts.snap @@ -73,126 +73,6 @@ Object { }, ], }, - "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", - }, - ], -} -`; - -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 { - "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", - }, - ], + "relatedRecords": Array [], } `; diff --git a/packages/pds/tests/admin/get-moderation-action.test.ts b/packages/pds/tests/admin/get-moderation-action.test.ts index 66562a73ec2..54b86984416 100644 --- a/packages/pds/tests/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/admin/get-moderation-actions.test.ts b/packages/pds/tests/admin/get-moderation-actions.test.ts index 31ee1686c3c..1ad5e066c60 100644 --- a/packages/pds/tests/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/admin/get-moderation-report.test.ts b/packages/pds/tests/admin/get-moderation-report.test.ts index c1363a0c49f..7d433539b37 100644 --- a/packages/pds/tests/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/admin/get-moderation-reports.test.ts b/packages/pds/tests/admin/get-moderation-reports.test.ts index 144a75a9f83..b6b083d0b17 100644 --- a/packages/pds/tests/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/admin/get-record.test.ts b/packages/pds/tests/admin/get-record.test.ts index 34933973caf..b40391eb28e 100644 --- a/packages/pds/tests/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 @@ -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 61ac6ed329c..5ffd9a3be32 100644 --- a/packages/pds/tests/admin/get-repo.test.ts +++ b/packages/pds/tests/admin/get-repo.test.ts @@ -6,16 +6,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 repo view', () => { let server: TestServerInfo @@ -87,26 +87,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/invites.test.ts b/packages/pds/tests/admin/invites.test.ts index 90f63cbd1f3..fdd76c24d8e 100644 --- a/packages/pds/tests/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 From 6940a76a9bf3257fa5e096dd8133069e2aa4a371 Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 12 Jun 2023 19:58:50 -0500 Subject: [PATCH 017/105] last admin tests --- .../__snapshots__/moderation.test.ts.snap | 0 .../pds/tests/{ => admin}/moderation.test.ts | 10 +- packages/pds/tests/admin/repo-search.test.ts | 118 ++---------------- 3 files changed, 14 insertions(+), 114 deletions(-) rename packages/pds/tests/{ => admin}/__snapshots__/moderation.test.ts.snap (100%) rename packages/pds/tests/{ => admin}/moderation.test.ts (99%) diff --git a/packages/pds/tests/__snapshots__/moderation.test.ts.snap b/packages/pds/tests/admin/__snapshots__/moderation.test.ts.snap similarity index 100% rename from packages/pds/tests/__snapshots__/moderation.test.ts.snap rename to packages/pds/tests/admin/__snapshots__/moderation.test.ts.snap diff --git a/packages/pds/tests/moderation.test.ts b/packages/pds/tests/admin/moderation.test.ts similarity index 99% rename from packages/pds/tests/moderation.test.ts rename to packages/pds/tests/admin/moderation.test.ts index 235f302117a..92bd30c199f 100644 --- a/packages/pds/tests/moderation.test.ts +++ b/packages/pds/tests/admin/moderation.test.ts @@ -7,18 +7,18 @@ import { moderatorAuth, runTestServer, TestServerInfo, -} from './_util' -import { ImageRef, RecordRef, SeedClient } from './seeds/client' -import basicSeed from './seeds/basic' +} from '../_util' +import { ImageRef, RecordRef, SeedClient } from '../seeds/client' +import basicSeed from '../seeds/basic' import { ACKNOWLEDGE, FLAG, TAKEDOWN, -} from '../src/lexicon/types/com/atproto/admin/defs' +} from '../../src/lexicon/types/com/atproto/admin/defs' import { REASONOTHER, REASONSPAM, -} from '../src/lexicon/types/com/atproto/moderation/defs' +} from '../../src/lexicon/types/com/atproto/moderation/defs' import { BlobNotFoundError } from '@atproto/repo' describe('moderation', () => { diff --git a/packages/pds/tests/admin/repo-search.test.ts b/packages/pds/tests/admin/repo-search.test.ts index 2da1f328247..e8eb262793c 100644 --- a/packages/pds/tests/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 @@ -54,9 +54,6 @@ 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', ] @@ -119,7 +116,6 @@ describe('pds admin repo search view', () => { { headers }, ) - expect(full.data.repos.length).toBeGreaterThan(5) expect(results(paginatedAll)).toEqual(results([full.data])) }) @@ -170,75 +166,6 @@ Array [ }, Object { "did": "user(1)", - "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(2)", - "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(3)", - "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(4)", "email": "carlos6.test@bsky.app", "handle": "carlos6.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -247,53 +174,26 @@ Array [ "relatedRecords": Array [], }, Object { - "did": "user(5)", + "did": "user(2)", "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", - }, - ], + "relatedRecords": Array [], }, Object { - "did": "user(6)", + "did": "user(3)", "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", - }, - ], + "relatedRecords": Array [], }, ] ` + const snapSqlite = ` Array [ Object { From 810273e3c05755f2f80d41ac7671bb0534fe7aae Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 12 Jun 2023 20:00:56 -0500 Subject: [PATCH 018/105] got a lil overzealous in deletes --- packages/bsky/src/db/tables/duplicate-record.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 packages/bsky/src/db/tables/duplicate-record.ts diff --git a/packages/bsky/src/db/tables/duplicate-record.ts b/packages/bsky/src/db/tables/duplicate-record.ts new file mode 100644 index 00000000000..3cad0cd148b --- /dev/null +++ b/packages/bsky/src/db/tables/duplicate-record.ts @@ -0,0 +1,12 @@ +export interface DuplicateRecord { + uri: string + cid: string + duplicateOf: string + indexedAt: string +} + +export const tableName = 'duplicate_record' + +export type PartialDB = { + [tableName]: DuplicateRecord +} From b22809009287b7da08921ece1ccbd46cc0759b2a Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 12 Jun 2023 20:20:16 -0500 Subject: [PATCH 019/105] clean up unused cfg --- packages/dev-env/src/pds.ts | 6 ---- packages/pds/src/config.ts | 65 ------------------------------------- packages/pds/tests/_util.ts | 9 ----- 3 files changed, 80 deletions(-) diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index 510d98e61c9..825b5d3be66 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -51,16 +51,10 @@ 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 sequencerLeaderLockId: uniqueLockId(), - labelerDid: 'did:example:labeler', - labelerKeywords: { label_me: 'test-label', label_me_2: 'test-label-2' }, - feedGenDid: 'did:example:feedGen', ...cfg, }) diff --git a/packages/pds/src/config.ts b/packages/pds/src/config.ts index 4b24c797d4f..6619e53be2b 100644 --- a/packages/pds/src/config.ts +++ b/packages/pds/src/config.ts @@ -35,21 +35,10 @@ export interface ServerConfigValues { availableUserDomains: string[] - imgUriSalt: string - imgUriKey: string - imgUriEndpoint?: string - blobCacheLocation?: string - appUrlPasswordReset: string emailSmtpUrl?: string emailNoReplyAddress: string - hiveApiKey?: string - labelerDid: string - labelerKeywords: Record - - feedGenDid?: string - maxSubscriptionBuffer: number repoBackfillLimitMs: number sequencerLeaderLockId?: number @@ -126,14 +115,6 @@ export class ServerConfig { ? process.env.AVAILABLE_USER_DOMAINS.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 appUrlPasswordReset = process.env.APP_URL_PASSWORD_RESET || 'app://password-reset' @@ -142,12 +123,6 @@ export class ServerConfig { const emailNoReplyAddress = process.env.EMAIL_NO_REPLY_ADDRESS || 'noreply@blueskyweb.xyz' - 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 @@ -200,17 +175,9 @@ export class ServerConfig { termsOfServiceUrl, databaseLocation, availableUserDomains, - imgUriSalt, - imgUriKey, - imgUriEndpoint, - blobCacheLocation, appUrlPasswordReset, emailSmtpUrl, emailNoReplyAddress, - hiveApiKey, - labelerDid, - labelerKeywords, - feedGenDid, maxSubscriptionBuffer, repoBackfillLimitMs, sequencerLeaderLockId, @@ -347,22 +314,6 @@ export class ServerConfig { return this.cfg.availableUserDomains } - 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 appUrlPasswordReset() { return this.cfg.appUrlPasswordReset } @@ -375,22 +326,6 @@ export class ServerConfig { return this.cfg.emailNoReplyAddress } - 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/tests/_util.ts b/packages/pds/tests/_util.ts index 43a7a75380e..0871c37b4aa 100644 --- a/packages/pds/tests/_util.ts +++ b/packages/pds/tests/_util.ts @@ -14,7 +14,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' @@ -28,7 +27,6 @@ export type TestServerInfo = { export type TestServerOpts = { migration?: string - algos?: MountedAlgos } export const runTestServer = async ( @@ -91,15 +89,9 @@ 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`, - 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(), @@ -143,7 +135,6 @@ export const runTestServer = async ( repoSigningKey, plcRotationKey, config: cfg, - algos: opts.algos, }) const pdsServer = await pds.start() const pdsPort = (pdsServer.address() as AddressInfo).port From 5ad2e095dcff392d1b658a6c874ab8be787a4966 Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 12 Jun 2023 20:27:32 -0500 Subject: [PATCH 020/105] clean up label table --- packages/pds/src/db/database-schema.ts | 4 ---- packages/pds/src/db/tables/label.ts | 12 ---------- packages/pds/src/services/moderation/views.ts | 24 ++----------------- .../__snapshots__/get-record.test.ts.snap | 2 -- .../admin/__snapshots__/get-repo.test.ts.snap | 1 - 5 files changed, 2 insertions(+), 41 deletions(-) delete mode 100644 packages/pds/src/db/tables/label.ts diff --git a/packages/pds/src/db/database-schema.ts b/packages/pds/src/db/database-schema.ts index 60f556a9fd0..96ff092c13f 100644 --- a/packages/pds/src/db/database-schema.ts +++ b/packages/pds/src/db/database-schema.ts @@ -16,7 +16,6 @@ import * as blob from './tables/blob' import * as repoBlob from './tables/repo-blob' import * as deleteAccountToken from './tables/delete-account-token' import * as moderation from './tables/moderation' -import * as label from './tables/label' import * as repoSeq from './tables/repo-seq' import * as appMigration from './tables/app-migration' @@ -33,14 +32,11 @@ export type DatabaseSchemaType = appMigration.PartialDB & repoCommitBlock.PartialDB & repoCommitHistory.PartialDB & ipldBlock.PartialDB & - repoCommitBlock.PartialDB & - repoCommitHistory.PartialDB & inviteCode.PartialDB & blob.PartialDB & repoBlob.PartialDB & deleteAccountToken.PartialDB & moderation.PartialDB & - label.PartialDB & repoSeq.PartialDB export type DatabaseSchema = Kysely 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/services/moderation/views.ts b/packages/pds/src/services/moderation/views.ts index 2d3afd3da56..d263b9b77a5 100644 --- a/packages/pds/src/services/moderation/views.ts +++ b/packages/pds/src/services/moderation/views.ts @@ -16,7 +16,6 @@ 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' @@ -119,10 +118,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, @@ -132,7 +130,6 @@ export class ModerationViews { actions, }, invites: inviteCodes, - labels, } } @@ -238,11 +235,10 @@ 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), ]) return { ...record, @@ -252,7 +248,6 @@ export class ModerationViews { reports, actions, }, - labels, } } @@ -557,21 +552,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/tests/admin/__snapshots__/get-record.test.ts.snap b/packages/pds/tests/admin/__snapshots__/get-record.test.ts.snap index d6b7edd6468..31a9de9a436 100644 --- a/packages/pds/tests/admin/__snapshots__/get-record.test.ts.snap +++ b/packages/pds/tests/admin/__snapshots__/get-record.test.ts.snap @@ -6,7 +6,6 @@ Object { "blobs": Array [], "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], "moderation": Object { "actions": Array [ Object { @@ -101,7 +100,6 @@ Object { "blobs": Array [], "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], "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 1c8e4cdd217..bce55507d24 100644 --- a/packages/pds/tests/admin/__snapshots__/get-repo.test.ts.snap +++ b/packages/pds/tests/admin/__snapshots__/get-repo.test.ts.snap @@ -8,7 +8,6 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "invites": Array [], "invitesDisabled": false, - "labels": Array [], "moderation": Object { "actions": Array [ Object { From 1ad57bac33492b5dc73a7ae4274827647bbd46cb Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Tue, 13 Jun 2023 01:18:38 -0400 Subject: [PATCH 021/105] simplify admin repo search query/logic --- .../src/api/com/atproto/admin/searchRepos.ts | 19 +- packages/pds/src/services/account/index.ts | 63 +++--- packages/pds/src/services/util/search.ts | 161 -------------- packages/pds/tests/admin/repo-search.test.ts | 209 ++---------------- 4 files changed, 56 insertions(+), 396 deletions(-) delete mode 100644 packages/pds/src/services/util/search.ts diff --git a/packages/pds/src/api/com/atproto/admin/searchRepos.ts b/packages/pds/src/api/com/atproto/admin/searchRepos.ts index 28506c5af4f..ecf2cd73113 100644 --- a/packages/pds/src/api/com/atproto/admin/searchRepos.ts +++ b/packages/pds/src/api/com/atproto/admin/searchRepos.ts @@ -1,7 +1,6 @@ +import { sql } from 'kysely' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { SearchKeyset } from '../../../../services/util/search' -import { sql } from 'kysely' import { ListKeyset } from '../../../../services/account' export default function (server: Server, ctx: AppContext) { @@ -10,14 +9,15 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params }) => { const { db, services } = ctx const moderationService = services.moderation(db) - const { term = '', limit = 50, cursor, invitedBy } = params + const { limit, cursor, invitedBy } = params + const term = params.term?.trim() ?? '' + + const keyset = new ListKeyset(sql``, sql``) if (!term) { const results = await services .account(db) .list({ limit, cursor, includeSoftDeleted: true, invitedBy }) - const keyset = new ListKeyset(sql``, sql``) - return { encoding: 'application/json', body: { @@ -27,19 +27,14 @@ export default function (server: Server, ctx: AppContext) { } } - const searchField = term.startsWith('did:') ? 'did' : 'handle' - const results = await services .account(db) - .search({ searchField, term, limit, cursor, includeSoftDeleted: true }) - const keyset = new SearchKeyset(sql``, sql``) + .search({ term, limit, cursor, includeSoftDeleted: true }) return { encoding: 'application/json', body: { - // For did search, we can only find 1 or no match, cursors can be ignored entirely - cursor: - searchField === 'did' ? undefined : keyset.packFromResult(results), + cursor: keyset.packFromResult(results), repos: await moderationService.views.repo(results), }, } diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts index 101c6f9cbd9..13c4b92eba0 100644 --- a/packages/pds/src/services/account/index.ts +++ b/packages/pds/src/services/account/index.ts @@ -1,4 +1,5 @@ -import { sql } from 'kysely' +import { randomStr } from '@atproto/crypto' +import { InvalidRequestError } from '@atproto/xrpc-server' import { dbLogger as log } from '../../logger' import Database from '../../db' import * as scrypt from '../../db/scrypt' @@ -6,12 +7,9 @@ import { UserAccountEntry } from '../../db/tables/user-account' import { DidHandle } from '../../db/tables/did-handle' import { RepoRoot } from '../../db/tables/repo-root' import { countAll, notSoftDeletedClause, nullToZero } 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' export class AccountService { constructor(public db: Database) {} @@ -231,37 +229,42 @@ export class AccountService { } async search(opts: { - searchField?: 'did' | 'handle' term: string limit: number cursor?: string includeSoftDeleted?: boolean - }): Promise<(RepoRoot & DidHandle & { distance: number })[]> { - if (opts.searchField === 'did') { - const didSearchBuilder = this.db.db - .selectFrom('did_handle') - .where('did_handle.did', '=', opts.term) - .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') - .selectAll(['did_handle', 'repo_root']) - .select(sql`0`.as('distance')) - - return await didSearchBuilder.execute() - } + }): Promise<(RepoRoot & DidHandle)[]> { + const { term, limit, cursor, includeSoftDeleted } = opts + const { ref } = this.db.db.dynamic - 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) - .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') - .selectAll('did_handle') - .selectAll('repo_root') - .select(sql`0`.as('distance')) - - return await builder.execute() + const builder = this.db.db + .selectFrom('did_handle') + .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') + .innerJoin('user_account', 'user_account.did', 'did_handle.did') + .if(!includeSoftDeleted, (qb) => + qb.where(notSoftDeletedClause(ref('repo_root'))), + ) + .where((qb) => { + if (term.includes('@')) { + return qb.where('user_account.email', 'ilike', `%${term}%`) + } + if (term.startsWith('did:')) { + return qb.where('did_handle.did', '=', term) + } + return qb.where('did_handle.handle', 'ilike', `${term}%`) + }) + .selectAll(['did_handle', 'repo_root']) + + const keyset = new ListKeyset( + ref('repo_root.indexedAt'), + ref('did_handle.handle'), + ) + + return await paginate(builder, { + limit, + cursor, + keyset, + }).execute() } async list(opts: { diff --git a/packages/pds/src/services/util/search.ts b/packages/pds/src/services/util/search.ts deleted file mode 100644 index 11a7d66809d..00000000000 --- a/packages/pds/src/services/util/search.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { sql } from 'kysely' -import { InvalidRequestError } from '@atproto/xrpc-server' -import Database from '../../db' -import { notSoftDeletedClause, DbRef } from '../../db/util' -import { GenericKeyset, paginate } from '../../db/pagination' - -// @TODO Come back and clean this up since removing profiles -export const getUserSearchQueryPg = ( - db: Database, - opts: { - term: string - limit: number - cursor?: string - includeSoftDeleted?: boolean - invitedBy?: string - }, -) => { - const { ref } = db.db.dynamic - const { term, limit, cursor, includeSoftDeleted } = opts - - // Performing matching by word using "strict word similarity" operator. - // The more characters the user gives us, the more we can ratchet down - // the distance threshold for matching. - const threshold = term.length < 3 ? 0.9 : 0.8 - - // Matching user accounts based on handle - const distanceAccount = distance(term, ref('handle')) - let accountsQb = db.db - .selectFrom('did_handle') - .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') - .if(!includeSoftDeleted, (qb) => - qb.where(notSoftDeletedClause(ref('repo_root'))), - ) - .where(similar(term, ref('handle'))) // Coarse filter engaging trigram index - .where(distanceAccount, '<', threshold) // Refines results from trigram index - .select(['did_handle.did as did', distanceAccount.as('distance')]) - accountsQb = paginate(accountsQb, { - limit, - cursor, - direction: 'asc', - keyset: new SearchKeyset(distanceAccount, ref('handle')), - }) - - // 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 - .as('accounts_and_profiles'), - ) - .selectAll() - .distinctOn('did') // Per did, take whichever of account and profile distance is best - .orderBy('did') - .orderBy('distance') - - // Sort and paginate all user results - const allQb = db.db - .selectFrom(resultsQb.as('results')) - .innerJoin('did_handle', 'did_handle.did', 'results.did') - return paginate(allQb, { - limit, - cursor, - direction: 'asc', - keyset: new SearchKeyset(ref('distance'), ref('handle')), - }) -} - -export const getUserSearchQuerySqlite = ( - db: Database, - opts: { - term: string - limit: number - cursor?: string - includeSoftDeleted?: boolean - }, -) => { - const { ref } = db.db.dynamic - const { term, limit, cursor, includeSoftDeleted } = opts - - // Take the first three words in the search term. We're going to build a dynamic query - // based on the number of words, so to keep things predictable just ignore words 4 and - // beyond. We also remove the special wildcard characters supported by the LIKE operator, - // since that's where these values are heading. - const safeWords = term - .replace(/[%_]/g, '') - .split(/\s+/) - .filter(Boolean) - .slice(0, 3) - - if (!safeWords.length) { - // Return no results. This could happen with weird input like ' % _ '. - return db.db.selectFrom('did_handle').where(sql`1 = 0`) - } - - // We'll ensure there's a space before each word in both textForMatch and in safeWords, - // so that we can reliably match word prefixes using LIKE operator. - const textForMatch = sql`lower(${ref('did_handle.handle')})` - - const keyset = new SearchKeyset(sql``, sql``) - const unpackedCursor = keyset.unpackCursor(cursor) - - return db.db - .selectFrom('did_handle') - .innerJoin('repo_root as _repo_root', '_repo_root.did', 'did_handle.did') - .if(!includeSoftDeleted, (qb) => - qb.where(notSoftDeletedClause(ref('_repo_root'))), - ) - .where((q) => { - safeWords.forEach((word) => { - // Match word prefixes against contents of handle and displayName - q = q.where(textForMatch, 'like', `% ${word.toLowerCase()}%`) - }) - return q - }) - .if(!!unpackedCursor, (qb) => - unpackedCursor ? qb.where('handle', '>', unpackedCursor.secondary) : qb, - ) - .orderBy('handle') - .limit(limit) -} - -// Remove leading @ in case a handle is input that way -export const cleanTerm = (term: string) => term.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})` - -// Can utilize trigram index to match on strict word similarity -const similar = (term: string, ref: DbRef) => sql`(${term} <<% ${ref})` - -type Result = { distance: number; handle: string } -type LabeledResult = { primary: number; secondary: string } -export class SearchKeyset extends GenericKeyset { - labelResult(result: Result) { - return { - primary: result.distance, - secondary: result.handle, - } - } - labeledResultToCursor(labeled: LabeledResult) { - return { - primary: labeled.primary.toString().replace('0.', '.'), - secondary: labeled.secondary, - } - } - cursorToLabeledResult(cursor: { primary: string; secondary: string }) { - const distance = parseFloat(cursor.primary) - if (isNaN(distance)) { - throw new InvalidRequestError('Malformed cursor') - } - return { - primary: distance, - secondary: cursor.secondary, - } - } -} diff --git a/packages/pds/tests/admin/repo-search.test.ts b/packages/pds/tests/admin/repo-search.test.ts index e8eb262793c..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) @@ -60,12 +51,6 @@ describe('pds admin repo search view', () => { 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', @@ -77,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 }, ) @@ -96,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) => { @@ -116,6 +108,7 @@ describe('pds admin repo search view', () => { { headers }, ) + expect(full.data.repos.length).toBeGreaterThan(3) expect(results(paginatedAll)).toEqual(results([full.data])) }) @@ -143,173 +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": "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(1)", - "email": "carlos6.test@bsky.app", - "handle": "carlos6.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invitesDisabled": false, - "moderation": Object {}, - "relatedRecords": Array [], - }, - Object { - "did": "user(2)", - "email": "carolina-mcdermott77.test@bsky.app", - "handle": "carolina-mcdermott77.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invitesDisabled": false, - "moderation": Object {}, - "relatedRecords": Array [], - }, - Object { - "did": "user(3)", - "email": "cayla-marquardt39.test@bsky.app", - "handle": "cayla-marquardt39.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invitesDisabled": false, - "moderation": Object {}, - "relatedRecords": Array [], - }, -] -` - -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", - }, - ], - }, -] -` From 12cfdfdd6d88051103b0d6e4a8506118a3d499d2 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Tue, 13 Jun 2023 01:24:33 -0400 Subject: [PATCH 022/105] tidy pds entrypoint --- packages/pds/service/index.js | 30 +++--------------------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/packages/pds/service/index.js b/packages/pds/service/index.js index 77f20b5d84e..b9a3ca12047 100644 --- a/packages/pds/service/index.js +++ b/packages/pds/service/index.js @@ -11,18 +11,8 @@ require('dd-trace/init') // Only works with commonjs // Tracer code above must come before anything else const path = require('path') -const { - KmsKeypair, - S3BlobStore, - CloudfrontInvalidator, -} = require('@atproto/aws') -const { - Database, - ServerConfig, - PDS, - ViewMaintainer, - makeAlgos, -} = require('@atproto/pds') +const { KmsKeypair, S3BlobStore } = require('@atproto/aws') +const { Database, ServerConfig, PDS } = require('@atproto/pds') const { Secp256k1Keypair } = require('@atproto/crypto') const main = async () => { @@ -31,11 +21,9 @@ const main = async () => { const migrateDb = Database.postgres({ url: pgUrl(env.dbMigrateCreds), schema: env.dbSchema, - // We need one connection for the - // view-maintainer lock then one for anything else. - poolSize: 2, }) await migrateDb.migrateToLatestOrThrow() + await migrateDb.close() // Use lower-credentialed user to run the app const db = Database.postgres({ url: pgUrl(env.dbCreds), @@ -67,29 +55,17 @@ 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() await pds.start() // Graceful shutdown (see also https://aws.amazon.com/blogs/containers/graceful-shutdowns-with-ecs/) process.on('SIGTERM', async () => { await pds.destroy() - viewMaintainer.destroy() - await viewMaintainerRunning - await migrateDb.close() }) } From 5172554772dfb81a707fa75d7126fe8601914a44 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Tue, 13 Jun 2023 11:12:35 -0400 Subject: [PATCH 023/105] in-progress pds config changes --- packages/pds/service/index.js | 6 +- packages/pds/src/config.ts | 169 ++++++++------------- packages/pds/src/index.ts | 4 +- packages/pds/src/storage/disk-blobstore.ts | 1 + packages/pds/tests/_util.ts | 2 - 5 files changed, 69 insertions(+), 113 deletions(-) diff --git a/packages/pds/service/index.js b/packages/pds/service/index.js index b9a3ca12047..74fdde1521d 100644 --- a/packages/pds/service/index.js +++ b/packages/pds/service/index.js @@ -16,7 +16,7 @@ const { Database, ServerConfig, PDS } = require('@atproto/pds') const { Secp256k1Keypair } = require('@atproto/crypto') const main = async () => { - const env = getEnv() + const env = ServerConfig.getEnv() // Migrate using credentialed user const migrateDb = Database.postgres({ url: pgUrl(env.dbMigrateCreds), @@ -105,11 +105,7 @@ const getEnv = () => ({ dbPoolMaxUses: maybeParseInt(process.env.DB_POOL_MAX_USES), dbPoolIdleTimeoutMs: maybeParseInt(process.env.DB_POOL_IDLE_TIMEOUT_MS), smtpHost: process.env.SMTP_HOST, - 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) => { diff --git a/packages/pds/src/config.ts b/packages/pds/src/config.ts index 6619e53be2b..27bbff76224 100644 --- a/packages/pds/src/config.ts +++ b/packages/pds/src/config.ts @@ -1,52 +1,63 @@ import { parseIntWithFallback, DAY, HOUR } from '@atproto/common' -export interface ServerConfigValues { - debugMode?: boolean - version: string +// off-config but still from env: +// repo signing key (two flavors?), recovery key +// logging: LOG_LEVEL, LOG_SYSTEMS, LOG_ENABLED, LOG_DESTINATION - publicUrl?: string - scheme: string +export interface ServerEnvironment { + // infra port?: number hostname: string + version?: string + privacyPolicyUrl?: string + termsOfServiceUrl?: string + serverDid?: string + // db: one required + dbSqliteLocation?: string dbPostgresUrl?: string + dbPostgresMigrationUrl?: string dbPostgresSchema?: string + dbPostgresPoolSize?: number + dbPostgresPoolMaxUses?: number + dbPostgresPoolIdleTimeoutMs?: number - blobstoreLocation?: string - blobstoreTmp?: string + // blobstore: one required + blobstoreS3Bucket?: string + blobstoreDiskLocation?: string + blobstoreDiskTmpLocation?: string + // secrets jwtSecret: string - - didPlcUrl: string - didCacheStaleTTL: number - didCacheMaxTTL: number - - serverDid: string - recoveryKey: string adminPassword: string moderatorPassword?: string - inviteRequired: boolean - userInviteInterval: number | null - privacyPolicyUrl?: string - termsOfServiceUrl?: string - - databaseLocation?: string + // plc + didPlcUrl?: string + didCacheStaleTTL?: number + didCacheMaxTTL?: number - availableUserDomains: string[] + // accounts + recoveryDidKey: string + inviteRequired?: boolean + inviteInterval?: number | null + handleDomains?: string[] // public hostname by default - appUrlPasswordReset: string + // email emailSmtpUrl?: string - emailNoReplyAddress: string + emailFromAddress?: string - maxSubscriptionBuffer: number - repoBackfillLimitMs: number + // subscription + maxSubscriptionBuffer?: number + repoBackfillLimitMs?: number sequencerLeaderLockId?: number + // appview bskyAppViewEndpoint?: string bskyAppViewDid?: string - crawlersToNotify?: string[] + // crawler + crawlers?: string[] } export class ServerConfig { @@ -60,20 +71,12 @@ export class ServerConfig { } static readEnv(overrides?: Partial) { - const debugMode = process.env.DEBUG_MODE === '1' - const version = process.env.PDS_VERSION || '0.0.0' - - const publicUrl = process.env.PUBLIC_URL || undefined - const hostname = process.env.HOSTNAME || 'localhost' - let scheme - if ('TLS' in process.env) { - scheme = process.env.TLS === '1' ? 'https' : 'http' - } else { - scheme = hostname === 'localhost' ? 'http' : 'https' - } + const version = nonemptyString(process.env.PDS_VERSION) + + const publicUrl = nonemptyString(process.env.PUBLIC_URL) const port = parseIntWithFallback(process.env.PORT, 2583) - const jwtSecret = process.env.JWT_SECRET || 'jwt_secret' + const jwtSecret = nonemptyString(process.env.JWT_SECRET) const didPlcUrl = process.env.DID_PLC_URL || 'http://localhost:2582' const didCacheStaleTTL = parseIntWithFallback( @@ -85,11 +88,6 @@ export class ServerConfig { DAY, ) - const serverDid = overrides?.serverDid || process.env.SERVER_DID - if (typeof serverDid !== 'string') { - throw new Error('No value provided for process.env.SERVER_DID') - } - const recoveryKey = overrides?.recoveryKey || process.env.RECOVERY_KEY if (typeof recoveryKey !== 'string') { throw new Error('No value provided for process.env.RECOVERY_KEY') @@ -98,33 +96,26 @@ export class ServerConfig { const adminPassword = process.env.ADMIN_PASSWORD || 'admin' const moderatorPassword = process.env.MODERATOR_PASSWORD || undefined - const inviteRequired = process.env.INVITE_REQUIRED === 'true' ? true : false + const inviteRequired = bool(process.env.INVITE_REQUIRED) const userInviteInterval = parseIntWithFallback( process.env.USER_INVITE_INTERVAL, null, ) - const privacyPolicyUrl = process.env.PRIVACY_POLICY_URL - const termsOfServiceUrl = process.env.TERMS_OF_SERVICE_URL - - const databaseLocation = process.env.DATABASE_LOC - - const blobstoreLocation = process.env.BLOBSTORE_LOC - const blobstoreTmp = process.env.BLOBSTORE_TMP - - const availableUserDomains = process.env.AVAILABLE_USER_DOMAINS - ? process.env.AVAILABLE_USER_DOMAINS.split(',') - : [] + const privacyPolicyUrl = nonemptyString(process.env.PRIVACY_POLICY_URL) + const termsOfServiceUrl = nonemptyString(process.env.TERMS_OF_SERVICE_URL) - const appUrlPasswordReset = - process.env.APP_URL_PASSWORD_RESET || 'app://password-reset' - - const emailSmtpUrl = process.env.EMAIL_SMTP_URL || undefined + const blobstoreS3Bucket = nonemptyString(process.env.BLOBSTORE_S3_BUCKET) + const blobstoreDiskLocation = nonemptyString( + process.env.BLOBSTORE_DISK_LOCATION, + ) - const emailNoReplyAddress = - process.env.EMAIL_NO_REPLY_ADDRESS || 'noreply@blueskyweb.xyz' + const availableUserDomains = commaList(process.env.AVAILABLE_USER_DOMAINS) - const dbPostgresUrl = process.env.DB_POSTGRES_URL - const dbPostgresSchema = process.env.DB_POSTGRES_SCHEMA + const emailSmtpUrl = nonemptyString(process.env.EMAIL_SMTP_URL) + const emailFromAddress = nonemptyString(process.env.EMAIL_FROM_ADDRESS) + const dbSqliteLocation = nonemptyString(process.env.DB_SQLITE_LOCATION) + const dbPostgresUrl = nonemptyString(process.env.DB_POSTGRES_URL) + const dbPostgresSchema = nonemptyString(process.env.DB_POSTGRES_SCHEMA) const maxSubscriptionBuffer = parseIntWithFallback( process.env.MAX_SUBSCRIPTION_BUFFER, @@ -146,16 +137,11 @@ export class ServerConfig { ) const bskyAppViewDid = nonemptyString(process.env.BSKY_APP_VIEW_DID) - const crawlersEnv = process.env.CRAWLERS_TO_NOTIFY - const crawlersToNotify = - crawlersEnv && crawlersEnv.length > 0 ? crawlersEnv.split(',') : [] + const crawlersToNotify = commaList(process.env.CRAWLERS_TO_NOTIFY) return new ServerConfig({ - debugMode, version, publicUrl, - scheme, - hostname, port, dbPostgresUrl, dbPostgresSchema, @@ -166,7 +152,6 @@ export class ServerConfig { didPlcUrl, didCacheStaleTTL, didCacheMaxTTL, - serverDid, adminPassword, moderatorPassword, inviteRequired, @@ -188,37 +173,16 @@ export class ServerConfig { }) } - get debugMode() { - return !!this.cfg.debugMode - } - get version() { return this.cfg.version } - get scheme() { - return this.cfg.scheme - } - get port() { return this.cfg.port } - get hostname() { - return this.cfg.hostname - } - - get internalUrl() { - return `${this.scheme}://${this.hostname}:${this.port}` - } - - get origin() { - const u = new URL(this.internalUrl) - return u.origin - } - get publicUrl() { - return this.cfg.publicUrl || this.internalUrl + return this.cfg.publicUrl } get publicHostname() { @@ -302,22 +266,10 @@ export class ServerConfig { return this.cfg.termsOfServiceUrl } - get databaseLocation() { - return this.cfg.databaseLocation - } - - get useMemoryDatabase() { - return !this.databaseLocation - } - get availableUserDomains() { return this.cfg.availableUserDomains } - get appUrlPasswordReset() { - return this.cfg.appUrlPasswordReset - } - get emailSmtpUrl() { return this.cfg.emailSmtpUrl } @@ -355,3 +307,12 @@ const nonemptyString = (str: string | undefined): string | undefined => { if (str === undefined || str.length === 0) return undefined return str } + +const bool = (str: string | undefined): boolean => { + return str === 'true' || str === '1' +} + +const commaList = (str: string | undefined): string[] => { + if (str === undefined || str.length === 0) return [] + return str.split(',') +} diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index 759f1dacd32..bff4868c17c 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -92,7 +92,7 @@ export class PDS { const backgroundQueue = new BackgroundQueue(db) const crawlers = new Crawlers( - config.hostname, + config.publicHostname, config.crawlersToNotify ?? [], ) @@ -121,7 +121,7 @@ export class PDS { }) let server = createServer({ - validateResponse: config.debugMode, + validateResponse: false, payload: { jsonLimit: 100 * 1024, // 100kb textLimit: 100 * 1024, // 100kb diff --git a/packages/pds/src/storage/disk-blobstore.ts b/packages/pds/src/storage/disk-blobstore.ts index 57826ba9d50..fe826bf273e 100644 --- a/packages/pds/src/storage/disk-blobstore.ts +++ b/packages/pds/src/storage/disk-blobstore.ts @@ -24,6 +24,7 @@ export class DiskBlobStore implements BlobStore { this.quarantineLocation = quarantineLocation } + // @TODO move quarantine out of temp by default static async create( location: string, tmpLocation?: string, diff --git a/packages/pds/tests/_util.ts b/packages/pds/tests/_util.ts index 0871c37b4aa..0895b62b876 100644 --- a/packages/pds/tests/_util.ts +++ b/packages/pds/tests/_util.ts @@ -71,9 +71,7 @@ export const runTestServer = async ( const blobstoreLoc = path.join(os.tmpdir(), randomStr(5, 'base32')) const cfg = new ServerConfig({ - debugMode: true, version: '0.0.0', - scheme: 'http', hostname: 'localhost', serverDid, recoveryKey, From b7b3d60fa9253a12f009b0061f5544788795f240 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 13 Jun 2023 11:23:28 -0500 Subject: [PATCH 024/105] cfg fiddling --- packages/pds/service/index.js | 110 ++++++--------- packages/pds/src/config.ts | 232 ++++++++++++++++++++++++++++++- packages/pds/src/index.ts | 2 +- packages/pds/src/mailer/index.ts | 6 +- 4 files changed, 271 insertions(+), 79 deletions(-) diff --git a/packages/pds/service/index.js b/packages/pds/service/index.js index 74fdde1521d..acae70d3dd0 100644 --- a/packages/pds/service/index.js +++ b/packages/pds/service/index.js @@ -12,56 +12,32 @@ require('dd-trace/init') // Only works with commonjs // Tracer code above must come before anything else const path = require('path') const { KmsKeypair, S3BlobStore } = require('@atproto/aws') -const { Database, ServerConfig, PDS } = require('@atproto/pds') +const { Database, ServerConfig, PDS, DiskBlobStore } = require('@atproto/pds') const { Secp256k1Keypair } = require('@atproto/crypto') const main = async () => { const env = ServerConfig.getEnv() + const config = new ServerConfig(env) + // Migrate using credentialed user - const migrateDb = Database.postgres({ - url: pgUrl(env.dbMigrateCreds), - schema: env.dbSchema, - }) + const migrateDb = getMigrationDb(config) await migrateDb.migrateToLatestOrThrow() await migrateDb.close() - // Use lower-credentialed user to run the app - const db = Database.postgres({ - url: pgUrl(env.dbCreds), - schema: env.dbSchema, - poolSize: env.dbPoolSize, - poolMaxUses: env.dbPoolMaxUses, - poolIdleTimeoutMs: env.dbPoolIdleTimeoutMs, - }) - const s3Blobstore = new S3BlobStore({ bucket: env.s3Bucket }) + + // @TODO config for keys const repoSigningKey = await Secp256k1Keypair.import(env.repoSigningKey) const plcRotationKey = await KmsKeypair.load({ keyId: env.plcRotationKeyId, }) - let recoveryKey - if (env.recoveryKeyId.startsWith('did:')) { - recoveryKey = env.recoveryKeyId - } else { - const recoveryKeypair = await KmsKeypair.load({ - keyId: env.recoveryKeyId, - }) - recoveryKey = recoveryKeypair.did() - } - const cfg = ServerConfig.readEnv({ - port: env.port, - recoveryKey, - emailSmtpUrl: smtpUrl({ - host: env.smtpHost, - username: env.smtpUsername, - password: env.smtpPassword, - }), - }) + const pds = PDS.create({ - db, - blobstore: s3Blobstore, + config, + db: getDb(config), + blobstore: getBlobstore(config), repoSigningKey, plcRotationKey, - config: cfg, }) + await pds.start() // Graceful shutdown (see also https://aws.amazon.com/blogs/containers/graceful-shutdowns-with-ecs/) process.on('SIGTERM', async () => { @@ -69,45 +45,43 @@ const main = async () => { }) } -const pgUrl = ({ - username = 'postgres', - password = 'postgres', - host = '0.0.0.0', - port = '5432', - database = 'postgres', - sslmode, -}) => { - const enc = encodeURIComponent - return `postgresql://${username}:${enc( - password, - )}@${host}:${port}/${database}${sslmode ? `?sslmode=${enc(sslmode)}` : ''}` +const getDb = (config) => { + if (config.db.dialect === 'pg') { + return Database.postgres({ + url: config.db.url, + schema: config.db.schema, + poolSize: config.db.poolSize, + poolMaxUses: config.db.poolMaxUses, + poolIdleTimeoutMs: config.db.poolIdleTimeoutMs, + }) + } else { + return Database.sqlite(config.db.location) + } } -const smtpUrl = ({ username, password, host }) => { - const enc = encodeURIComponent - return `smtps://${username}:${enc(password)}@${host}` +const getMigrationDb = (config) => { + if (config.db.dialect === 'pg') { + return Database.postgres({ + url: config.db.migrationUrl, + schema: config.db.schema, + }) + } else { + return Database.sqlite(config.db.location) + } } -const maybeParseInt = (str) => { - const parsed = parseInt(str) - return isNaN(parsed) ? undefined : parsed +const getBlobstore = (config) => { + if (config.blobstore.provider === 's3') { + return new S3BlobStore({ bucket: config.blobstore.bucket }) + } else { + return new DiskBlobStore( + config.blobstore.location, + config.blobstore.tempLocation, + config.blobstore.quarantineLocation + ) + } } -const getEnv = () => ({ - port: parseInt(process.env.PORT), - plcRotationKeyId: process.env.PLC_ROTATION_KEY_ID, - repoSigningKey: process.env.REPO_SIGNING_KEY, - recoveryKeyId: process.env.RECOVERY_KEY_ID, - dbCreds: JSON.parse(process.env.DB_CREDS_JSON), - dbMigrateCreds: JSON.parse(process.env.DB_MIGRATE_CREDS_JSON), - dbSchema: process.env.DB_SCHEMA || undefined, - dbPoolSize: maybeParseInt(process.env.DB_POOL_SIZE), - dbPoolMaxUses: maybeParseInt(process.env.DB_POOL_MAX_USES), - dbPoolIdleTimeoutMs: maybeParseInt(process.env.DB_POOL_IDLE_TIMEOUT_MS), - smtpHost: process.env.SMTP_HOST, - s3Bucket: process.env.S3_BUCKET_NAME, -}) - const maintainXrpcResource = (span, req) => { // Show actual xrpc method as resource rather than the route pattern if (span && req.originalUrl?.startsWith('/xrpc/')) { diff --git a/packages/pds/src/config.ts b/packages/pds/src/config.ts index 27bbff76224..774a07f3bdc 100644 --- a/packages/pds/src/config.ts +++ b/packages/pds/src/config.ts @@ -1,4 +1,6 @@ -import { parseIntWithFallback, DAY, HOUR } from '@atproto/common' +import os from 'node:os' +import path from 'node:path' +import { parseIntWithFallback, DAY, HOUR, SECOND } from '@atproto/common' // off-config but still from env: // repo signing key (two flavors?), recovery key @@ -8,10 +10,10 @@ export interface ServerEnvironment { // infra port?: number hostname: string + serviceDid?: string version?: string privacyPolicyUrl?: string termsOfServiceUrl?: string - serverDid?: string // db: one required dbSqliteLocation?: string @@ -32,16 +34,17 @@ export interface ServerEnvironment { adminPassword: string moderatorPassword?: string - // plc + // identity didPlcUrl?: string didCacheStaleTTL?: number didCacheMaxTTL?: number + resolverTimeout?: number + recoveryDidKey?: string + handleDomains?: string[] // public hostname by default // accounts - recoveryDidKey: string inviteRequired?: boolean - inviteInterval?: number | null - handleDomains?: string[] // public hostname by default + inviteInterval?: number // email emailSmtpUrl?: string @@ -316,3 +319,220 @@ const commaList = (str: string | undefined): string[] => { if (str === undefined || str.length === 0) return [] return str.split(',') } + +const envToCfg = (env: ServerEnvironment): ConfigTakeTwo => { + const port = env.port ?? 2583 + const hostname = env.hostname + const did = env.serviceDid ?? `did:web:${hostname}` + const serviceCfg = { + port, + hostname, + did, + version: env.version, // default? + privacyPolicyUrl: env.privacyPolicyUrl, + termsOfServiceUrl: env.termsOfServiceUrl, + } + + let dbCfg: ConfigTakeTwo['db'] + if (env.dbSqliteLocation && env.dbPostgresUrl) { + throw new Error('Cannot set both sqlite & postgres db env vars') + } + if (env.dbSqliteLocation) { + dbCfg = { + dialect: 'sqlite', + location: env.dbSqliteLocation, + } + } else if (env.dbPostgresUrl) { + dbCfg = { + dialect: 'pg', + url: env.dbPostgresUrl, + migrationUrl: env.dbPostgresMigrationUrl || env.dbPostgresUrl, + schema: env.dbPostgresSchema, + pool: { + idleTimeoutMs: env.dbPostgresPoolIdleTimeoutMs ?? 10000, + maxUses: env.dbPostgresPoolMaxUses || Infinity, + size: env.dbPostgresPoolSize ?? 10, + }, + } + } else { + throw new Error('Must configure either sqlite or postgres db') + } + + let blobstoreCfg: ConfigTakeTwo['blobstore'] + if (env.blobstoreS3Bucket && env.blobstoreDiskLocation) { + throw new Error('Cannot set both S3 and disk blobstore env vars') + } + if (env.blobstoreS3Bucket) { + blobstoreCfg = { provider: 's3', bucket: env.blobstoreS3Bucket } + } else if (env.blobstoreDiskLocation) { + blobstoreCfg = { + provider: 'disk', + location: env.blobstoreDiskLocation, + tempLocation: + env.blobstoreDiskTmpLocation || path.join(os.tmpdir(), 'pds/blobs'), + quarantineLocation: path.join(env.blobstoreDiskLocation, 'quarantine'), + } + } else { + throw new Error('Must configure either S3 or disk blobstore') + } + + const secretsCfg: ConfigTakeTwo['secrets'] = { + jwtSecret: env.jwtSecret, + adminPassword: env.adminPassword, + moderatorPassword: env.moderatorPassword || env.adminPassword, + } + + const handleDomains = + env.handleDomains && env.handleDomains.length > 0 + ? env.handleDomains + : [env.hostname] + const identityCfg: ConfigTakeTwo['identity'] = { + plcUrl: env.didPlcUrl || 'https://plc.bsky-sandbox.dev', + cacheMaxTTL: env.didCacheMaxTTL || DAY, + cacheStaleTTL: env.didCacheStaleTTL || HOUR, + resolverTimeout: env.resolverTimeout || 3 * SECOND, + recoveryDidKey: env.recoveryDidKey ?? null, + handleDomains, + } + + const invitesCfg: ConfigTakeTwo['invites'] = env.inviteRequired + ? { + required: true, + interval: env.inviteInterval ?? null, + } + : { + required: false, + } + + let emailCfg: ConfigTakeTwo['email'] + if (!env.emailFromAddress && !env.emailSmtpUrl) { + emailCfg = null + } else { + if (!env.emailFromAddress || !env.emailSmtpUrl) { + throw new Error('Partial email config') + } + emailCfg = { + smtpUrl: env.emailSmtpUrl, + fromAddress: env.emailFromAddress, + } + } + + const subscriptionCfg: ConfigTakeTwo['subscription'] = { + maxBuffer: env.maxSubscriptionBuffer ?? 500, + repoBackfillLimitMs: env.repoBackfillLimitMs ?? DAY, + sequencerLeaderLockId: env.sequencerLeaderLockId ?? 1100, + } + + const bskyAppViewCfg: ConfigTakeTwo['bskyAppView'] = { + endpoint: env.bskyAppViewEndpoint ?? 'https://api.bsky-sandbox.dev', + did: env.bskyAppViewDid ?? 'did:plc:abc', // get real did + } + + const crawlersCfg: ConfigTakeTwo['crawlers'] = env.crawlers ?? [] + + return { + service: serviceCfg, + db: dbCfg, + blobstore: blobstoreCfg, + secrets: secretsCfg, + identity: identityCfg, + invites: invitesCfg, + email: emailCfg, + subscription: subscriptionCfg, + bskyAppView: bskyAppViewCfg, + crawlers: crawlersCfg, + } +} + +export type ConfigTakeTwo = { + service: ServiceConfig + db: SqliteConfig | PostgresConfig + blobstore: S3BlobstoreConfig | DiskBlobstoreConfig + secrets: SecretsConfig + identity: IdentityConfig + invites: InvitesConfig + email: EmailConfig | null + subscription: SubscriptionConfig + bskyAppView: BksyAppViewConfig + crawlers: string[] +} + +export type ServiceConfig = { + port: number + hostname: string + did: string + version?: string + privacyPolicyUrl?: string + termsOfServiceUrl?: string +} + +export type SqliteConfig = { + dialect: 'sqlite' + location: string +} + +export type PostgresPoolConfig = { + size: number + maxUses: number + idleTimeoutMs: number +} + +export type PostgresConfig = { + dialect: 'pg' + url: string + migrationUrl: string + pool: PostgresPoolConfig + schema?: string +} + +export type S3BlobstoreConfig = { + provider: 's3' + bucket: string +} + +export type DiskBlobstoreConfig = { + provider: 'disk' + location: string + tempLocation: string + quarantineLocation: string +} + +export type SecretsConfig = { + jwtSecret: string + adminPassword: string + moderatorPassword: string +} + +export type IdentityConfig = { + plcUrl: string + resolverTimeout: number + cacheStaleTTL: number + cacheMaxTTL: number + recoveryDidKey: string | null + handleDomains: string[] +} + +export type InvitesConfig = + | { + required: true + interval: number | null + } + | { + required: false + } + +export type EmailConfig = { + smtpUrl: string + fromAddress: string +} + +export type SubscriptionConfig = { + maxBuffer: number + repoBackfillLimitMs: number + sequencerLeaderLockId: number +} + +export type BksyAppViewConfig = { + endpoint: string + did: string +} diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index bff4868c17c..38cfba0dae0 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -30,7 +30,7 @@ import DidSqlCache from './did-cache' import { IdResolver } from '@atproto/identity' import { Crawlers } from './crawlers' -export type { ServerConfigValues } from './config' +// export type { ServerConfigValues } from './config' export { ServerConfig } from './config' export { Database } from './db' export { DiskBlobStore, MemoryBlobStore } from './storage' diff --git a/packages/pds/src/mailer/index.ts b/packages/pds/src/mailer/index.ts index 99059f6f02e..32408870675 100644 --- a/packages/pds/src/mailer/index.ts +++ b/packages/pds/src/mailer/index.ts @@ -28,10 +28,8 @@ export class ServerMailer { } // The returned config can be used inside email templates. - static getEmailConfig(config: ServerConfig) { - return { - appUrlPasswordReset: config.appUrlPasswordReset, - } + static getEmailConfig(_config: ServerConfig) { + return {} } async sendResetPassword( From dcdeeaa71e99ebaa67aa4842284fefdfb4ef35fd Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 13 Jun 2023 12:36:54 -0500 Subject: [PATCH 025/105] finish cleaning up cfg/ctx --- packages/pds/package.json | 1 + packages/pds/src/config.ts | 538 ----------------------------- packages/pds/src/config/config.ts | 221 ++++++++++++ packages/pds/src/config/env.ts | 114 ++++++ packages/pds/src/config/index.ts | 3 + packages/pds/src/config/secrets.ts | 71 ++++ packages/pds/src/config/util.ts | 19 + packages/pds/src/context.ts | 258 ++++++++------ packages/pds/src/index.ts | 98 +----- packages/pds/src/mailer/index.ts | 4 +- packages/pds/tsconfig.json | 1 + 11 files changed, 605 insertions(+), 723 deletions(-) delete mode 100644 packages/pds/src/config.ts create mode 100644 packages/pds/src/config/config.ts create mode 100644 packages/pds/src/config/env.ts create mode 100644 packages/pds/src/config/index.ts create mode 100644 packages/pds/src/config/secrets.ts create mode 100644 packages/pds/src/config/util.ts diff --git a/packages/pds/package.json b/packages/pds/package.json index df69f8e9a16..eed3e5345cd 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -32,6 +32,7 @@ }, "dependencies": { "@atproto/api": "*", + "@atproto/aws": "*", "@atproto/common": "*", "@atproto/crypto": "*", "@atproto/identifier": "*", diff --git a/packages/pds/src/config.ts b/packages/pds/src/config.ts deleted file mode 100644 index 774a07f3bdc..00000000000 --- a/packages/pds/src/config.ts +++ /dev/null @@ -1,538 +0,0 @@ -import os from 'node:os' -import path from 'node:path' -import { parseIntWithFallback, DAY, HOUR, SECOND } from '@atproto/common' - -// off-config but still from env: -// repo signing key (two flavors?), recovery key -// logging: LOG_LEVEL, LOG_SYSTEMS, LOG_ENABLED, LOG_DESTINATION - -export interface ServerEnvironment { - // infra - port?: number - hostname: string - serviceDid?: string - version?: string - privacyPolicyUrl?: string - termsOfServiceUrl?: string - - // db: one required - dbSqliteLocation?: string - dbPostgresUrl?: string - dbPostgresMigrationUrl?: string - dbPostgresSchema?: string - dbPostgresPoolSize?: number - dbPostgresPoolMaxUses?: number - dbPostgresPoolIdleTimeoutMs?: number - - // blobstore: one required - blobstoreS3Bucket?: string - blobstoreDiskLocation?: string - blobstoreDiskTmpLocation?: string - - // secrets - jwtSecret: string - adminPassword: string - moderatorPassword?: string - - // identity - didPlcUrl?: string - didCacheStaleTTL?: number - didCacheMaxTTL?: number - resolverTimeout?: number - recoveryDidKey?: string - handleDomains?: string[] // public hostname by default - - // accounts - inviteRequired?: boolean - inviteInterval?: number - - // email - emailSmtpUrl?: string - emailFromAddress?: string - - // subscription - maxSubscriptionBuffer?: number - repoBackfillLimitMs?: number - sequencerLeaderLockId?: number - - // appview - bskyAppViewEndpoint?: string - bskyAppViewDid?: string - - // crawler - crawlers?: string[] -} - -export class ServerConfig { - constructor(private cfg: ServerConfigValues) { - const invalidDomain = cfg.availableUserDomains.find( - (domain) => domain.length < 1 || !domain.startsWith('.'), - ) - if (invalidDomain) { - throw new Error(`Invalid domain: ${invalidDomain}`) - } - } - - static readEnv(overrides?: Partial) { - const version = nonemptyString(process.env.PDS_VERSION) - - const publicUrl = nonemptyString(process.env.PUBLIC_URL) - const port = parseIntWithFallback(process.env.PORT, 2583) - - const jwtSecret = nonemptyString(process.env.JWT_SECRET) - - const didPlcUrl = process.env.DID_PLC_URL || 'http://localhost:2582' - const didCacheStaleTTL = parseIntWithFallback( - process.env.DID_CACHE_STALE_TTL, - HOUR, - ) - const didCacheMaxTTL = parseIntWithFallback( - process.env.DID_CACHE_MAX_TTL, - DAY, - ) - - const recoveryKey = overrides?.recoveryKey || process.env.RECOVERY_KEY - if (typeof recoveryKey !== 'string') { - throw new Error('No value provided for process.env.RECOVERY_KEY') - } - - const adminPassword = process.env.ADMIN_PASSWORD || 'admin' - const moderatorPassword = process.env.MODERATOR_PASSWORD || undefined - - const inviteRequired = bool(process.env.INVITE_REQUIRED) - const userInviteInterval = parseIntWithFallback( - process.env.USER_INVITE_INTERVAL, - null, - ) - const privacyPolicyUrl = nonemptyString(process.env.PRIVACY_POLICY_URL) - const termsOfServiceUrl = nonemptyString(process.env.TERMS_OF_SERVICE_URL) - - const blobstoreS3Bucket = nonemptyString(process.env.BLOBSTORE_S3_BUCKET) - const blobstoreDiskLocation = nonemptyString( - process.env.BLOBSTORE_DISK_LOCATION, - ) - - const availableUserDomains = commaList(process.env.AVAILABLE_USER_DOMAINS) - - const emailSmtpUrl = nonemptyString(process.env.EMAIL_SMTP_URL) - const emailFromAddress = nonemptyString(process.env.EMAIL_FROM_ADDRESS) - const dbSqliteLocation = nonemptyString(process.env.DB_SQLITE_LOCATION) - const dbPostgresUrl = nonemptyString(process.env.DB_POSTGRES_URL) - const dbPostgresSchema = nonemptyString(process.env.DB_POSTGRES_SCHEMA) - - const maxSubscriptionBuffer = parseIntWithFallback( - process.env.MAX_SUBSCRIPTION_BUFFER, - 500, - ) - - const repoBackfillLimitMs = parseIntWithFallback( - process.env.REPO_BACKFILL_LIMIT_MS, - DAY, - ) - - const sequencerLeaderLockId = parseIntWithFallback( - process.env.SEQUENCER_LEADER_LOCK_ID, - undefined, - ) - - const bskyAppViewEndpoint = nonemptyString( - process.env.BSKY_APP_VIEW_ENDPOINT, - ) - const bskyAppViewDid = nonemptyString(process.env.BSKY_APP_VIEW_DID) - - const crawlersToNotify = commaList(process.env.CRAWLERS_TO_NOTIFY) - - return new ServerConfig({ - version, - publicUrl, - port, - dbPostgresUrl, - dbPostgresSchema, - blobstoreLocation, - blobstoreTmp, - jwtSecret, - recoveryKey, - didPlcUrl, - didCacheStaleTTL, - didCacheMaxTTL, - adminPassword, - moderatorPassword, - inviteRequired, - userInviteInterval, - privacyPolicyUrl, - termsOfServiceUrl, - databaseLocation, - availableUserDomains, - appUrlPasswordReset, - emailSmtpUrl, - emailNoReplyAddress, - maxSubscriptionBuffer, - repoBackfillLimitMs, - sequencerLeaderLockId, - bskyAppViewEndpoint, - bskyAppViewDid, - crawlersToNotify, - ...overrides, - }) - } - - get version() { - return this.cfg.version - } - - get port() { - return this.cfg.port - } - - get publicUrl() { - return this.cfg.publicUrl - } - - get publicHostname() { - const u = new URL(this.publicUrl) - return u.hostname - } - - get dbPostgresUrl() { - return this.cfg.dbPostgresUrl - } - - get dbPostgresSchema() { - return this.cfg.dbPostgresSchema - } - - get blobstoreLocation() { - return this.cfg.blobstoreLocation - } - - get blobstoreTmp() { - return this.cfg.blobstoreTmp - } - - get jwtSecret() { - return this.cfg.jwtSecret - } - - get didPlcUrl() { - return this.cfg.didPlcUrl - } - - get didCacheStaleTTL() { - return this.cfg.didCacheStaleTTL - } - - get didCacheMaxTTL() { - return this.cfg.didCacheMaxTTL - } - - get serverDid() { - return this.cfg.serverDid - } - - get recoveryKey() { - return this.cfg.recoveryKey - } - - get adminPassword() { - return this.cfg.adminPassword - } - - get moderatorPassword() { - return this.cfg.moderatorPassword - } - - get inviteRequired() { - return this.cfg.inviteRequired - } - - get userInviteInterval() { - return this.cfg.userInviteInterval - } - - get privacyPolicyUrl() { - if ( - this.cfg.privacyPolicyUrl && - this.cfg.privacyPolicyUrl.startsWith('/') - ) { - return this.publicUrl + this.cfg.privacyPolicyUrl - } - return this.cfg.privacyPolicyUrl - } - - get termsOfServiceUrl() { - if ( - this.cfg.termsOfServiceUrl && - this.cfg.termsOfServiceUrl.startsWith('/') - ) { - return this.publicUrl + this.cfg.termsOfServiceUrl - } - return this.cfg.termsOfServiceUrl - } - - get availableUserDomains() { - return this.cfg.availableUserDomains - } - - get emailSmtpUrl() { - return this.cfg.emailSmtpUrl - } - - get emailNoReplyAddress() { - return this.cfg.emailNoReplyAddress - } - - get maxSubscriptionBuffer() { - return this.cfg.maxSubscriptionBuffer - } - - get repoBackfillLimitMs() { - return this.cfg.repoBackfillLimitMs - } - - get sequencerLeaderLockId() { - return this.cfg.sequencerLeaderLockId - } - - get bskyAppViewEndpoint() { - return this.cfg.bskyAppViewEndpoint - } - - get bskyAppViewDid() { - return this.cfg.bskyAppViewDid - } - - get crawlersToNotify() { - return this.cfg.crawlersToNotify - } -} - -const nonemptyString = (str: string | undefined): string | undefined => { - if (str === undefined || str.length === 0) return undefined - return str -} - -const bool = (str: string | undefined): boolean => { - return str === 'true' || str === '1' -} - -const commaList = (str: string | undefined): string[] => { - if (str === undefined || str.length === 0) return [] - return str.split(',') -} - -const envToCfg = (env: ServerEnvironment): ConfigTakeTwo => { - const port = env.port ?? 2583 - const hostname = env.hostname - const did = env.serviceDid ?? `did:web:${hostname}` - const serviceCfg = { - port, - hostname, - did, - version: env.version, // default? - privacyPolicyUrl: env.privacyPolicyUrl, - termsOfServiceUrl: env.termsOfServiceUrl, - } - - let dbCfg: ConfigTakeTwo['db'] - if (env.dbSqliteLocation && env.dbPostgresUrl) { - throw new Error('Cannot set both sqlite & postgres db env vars') - } - if (env.dbSqliteLocation) { - dbCfg = { - dialect: 'sqlite', - location: env.dbSqliteLocation, - } - } else if (env.dbPostgresUrl) { - dbCfg = { - dialect: 'pg', - url: env.dbPostgresUrl, - migrationUrl: env.dbPostgresMigrationUrl || env.dbPostgresUrl, - schema: env.dbPostgresSchema, - pool: { - idleTimeoutMs: env.dbPostgresPoolIdleTimeoutMs ?? 10000, - maxUses: env.dbPostgresPoolMaxUses || Infinity, - size: env.dbPostgresPoolSize ?? 10, - }, - } - } else { - throw new Error('Must configure either sqlite or postgres db') - } - - let blobstoreCfg: ConfigTakeTwo['blobstore'] - if (env.blobstoreS3Bucket && env.blobstoreDiskLocation) { - throw new Error('Cannot set both S3 and disk blobstore env vars') - } - if (env.blobstoreS3Bucket) { - blobstoreCfg = { provider: 's3', bucket: env.blobstoreS3Bucket } - } else if (env.blobstoreDiskLocation) { - blobstoreCfg = { - provider: 'disk', - location: env.blobstoreDiskLocation, - tempLocation: - env.blobstoreDiskTmpLocation || path.join(os.tmpdir(), 'pds/blobs'), - quarantineLocation: path.join(env.blobstoreDiskLocation, 'quarantine'), - } - } else { - throw new Error('Must configure either S3 or disk blobstore') - } - - const secretsCfg: ConfigTakeTwo['secrets'] = { - jwtSecret: env.jwtSecret, - adminPassword: env.adminPassword, - moderatorPassword: env.moderatorPassword || env.adminPassword, - } - - const handleDomains = - env.handleDomains && env.handleDomains.length > 0 - ? env.handleDomains - : [env.hostname] - const identityCfg: ConfigTakeTwo['identity'] = { - plcUrl: env.didPlcUrl || 'https://plc.bsky-sandbox.dev', - cacheMaxTTL: env.didCacheMaxTTL || DAY, - cacheStaleTTL: env.didCacheStaleTTL || HOUR, - resolverTimeout: env.resolverTimeout || 3 * SECOND, - recoveryDidKey: env.recoveryDidKey ?? null, - handleDomains, - } - - const invitesCfg: ConfigTakeTwo['invites'] = env.inviteRequired - ? { - required: true, - interval: env.inviteInterval ?? null, - } - : { - required: false, - } - - let emailCfg: ConfigTakeTwo['email'] - if (!env.emailFromAddress && !env.emailSmtpUrl) { - emailCfg = null - } else { - if (!env.emailFromAddress || !env.emailSmtpUrl) { - throw new Error('Partial email config') - } - emailCfg = { - smtpUrl: env.emailSmtpUrl, - fromAddress: env.emailFromAddress, - } - } - - const subscriptionCfg: ConfigTakeTwo['subscription'] = { - maxBuffer: env.maxSubscriptionBuffer ?? 500, - repoBackfillLimitMs: env.repoBackfillLimitMs ?? DAY, - sequencerLeaderLockId: env.sequencerLeaderLockId ?? 1100, - } - - const bskyAppViewCfg: ConfigTakeTwo['bskyAppView'] = { - endpoint: env.bskyAppViewEndpoint ?? 'https://api.bsky-sandbox.dev', - did: env.bskyAppViewDid ?? 'did:plc:abc', // get real did - } - - const crawlersCfg: ConfigTakeTwo['crawlers'] = env.crawlers ?? [] - - return { - service: serviceCfg, - db: dbCfg, - blobstore: blobstoreCfg, - secrets: secretsCfg, - identity: identityCfg, - invites: invitesCfg, - email: emailCfg, - subscription: subscriptionCfg, - bskyAppView: bskyAppViewCfg, - crawlers: crawlersCfg, - } -} - -export type ConfigTakeTwo = { - service: ServiceConfig - db: SqliteConfig | PostgresConfig - blobstore: S3BlobstoreConfig | DiskBlobstoreConfig - secrets: SecretsConfig - identity: IdentityConfig - invites: InvitesConfig - email: EmailConfig | null - subscription: SubscriptionConfig - bskyAppView: BksyAppViewConfig - crawlers: string[] -} - -export type ServiceConfig = { - port: number - hostname: string - did: string - version?: string - privacyPolicyUrl?: string - termsOfServiceUrl?: string -} - -export type SqliteConfig = { - dialect: 'sqlite' - location: string -} - -export type PostgresPoolConfig = { - size: number - maxUses: number - idleTimeoutMs: number -} - -export type PostgresConfig = { - dialect: 'pg' - url: string - migrationUrl: string - pool: PostgresPoolConfig - schema?: string -} - -export type S3BlobstoreConfig = { - provider: 's3' - bucket: string -} - -export type DiskBlobstoreConfig = { - provider: 'disk' - location: string - tempLocation: string - quarantineLocation: string -} - -export type SecretsConfig = { - jwtSecret: string - adminPassword: string - moderatorPassword: string -} - -export type IdentityConfig = { - plcUrl: string - resolverTimeout: number - cacheStaleTTL: number - cacheMaxTTL: number - recoveryDidKey: string | null - handleDomains: string[] -} - -export type InvitesConfig = - | { - required: true - interval: number | null - } - | { - required: false - } - -export type EmailConfig = { - smtpUrl: string - fromAddress: string -} - -export type SubscriptionConfig = { - maxBuffer: number - repoBackfillLimitMs: number - sequencerLeaderLockId: number -} - -export type BksyAppViewConfig = { - endpoint: string - did: string -} diff --git a/packages/pds/src/config/config.ts b/packages/pds/src/config/config.ts new file mode 100644 index 00000000000..e5684ad8961 --- /dev/null +++ b/packages/pds/src/config/config.ts @@ -0,0 +1,221 @@ +import os from 'node:os' +import path from 'node:path' +import { DAY, HOUR, SECOND } from '@atproto/common' +import { ServerEnvironment } from './env' + +// off-config but still from env: +// logging: LOG_LEVEL, LOG_SYSTEMS, LOG_ENABLED, LOG_DESTINATION + +export const envToCfg = (env: ServerEnvironment): ServerConfig => { + const port = env.port ?? 2583 + const hostname = env.hostname ?? 'localhost' + const did = env.serviceDid ?? `did:web:${hostname}` + const serviceCfg: ServerConfig['service'] = { + port, + hostname, + did, + version: env.version, // default? + privacyPolicyUrl: env.privacyPolicyUrl, + termsOfServiceUrl: env.termsOfServiceUrl, + } + + let dbCfg: ServerConfig['db'] + if (env.dbSqliteLocation && env.dbPostgresUrl) { + throw new Error('Cannot set both sqlite & postgres db env vars') + } + if (env.dbSqliteLocation) { + dbCfg = { + dialect: 'sqlite', + location: env.dbSqliteLocation, + } + } else if (env.dbPostgresUrl) { + dbCfg = { + dialect: 'pg', + url: env.dbPostgresUrl, + migrationUrl: env.dbPostgresMigrationUrl || env.dbPostgresUrl, + schema: env.dbPostgresSchema, + pool: { + idleTimeoutMs: env.dbPostgresPoolIdleTimeoutMs ?? 10000, + maxUses: env.dbPostgresPoolMaxUses || Infinity, + size: env.dbPostgresPoolSize ?? 10, + }, + } + } else { + throw new Error('Must configure either sqlite or postgres db') + } + + let blobstoreCfg: ServerConfig['blobstore'] + if (env.blobstoreS3Bucket && env.blobstoreDiskLocation) { + throw new Error('Cannot set both S3 and disk blobstore env vars') + } + if (env.blobstoreS3Bucket) { + blobstoreCfg = { provider: 's3', bucket: env.blobstoreS3Bucket } + } else if (env.blobstoreDiskLocation) { + blobstoreCfg = { + provider: 'disk', + location: env.blobstoreDiskLocation, + tempLocation: + env.blobstoreDiskTmpLocation || path.join(os.tmpdir(), 'pds/blobs'), + } + } else { + throw new Error('Must configure either S3 or disk blobstore') + } + + let handleDomains: string[] + if (env.handleDomains && env.handleDomains.length > 0) { + handleDomains = env.handleDomains + } else { + if (hostname === 'localhost') { + handleDomains = ['.test'] + } else { + handleDomains = [`.${hostname}`] + } + } + const invalidDomain = handleDomains.find( + (domain) => domain.length < 1 || !domain.startsWith('.'), + ) + if (invalidDomain) { + throw new Error(`Invalid handle domain: ${invalidDomain}`) + } + + const identityCfg: ServerConfig['identity'] = { + plcUrl: env.didPlcUrl || 'https://plc.bsky-sandbox.dev', + cacheMaxTTL: env.didCacheMaxTTL || DAY, + cacheStaleTTL: env.didCacheStaleTTL || HOUR, + resolverTimeout: env.resolverTimeout || 3 * SECOND, + recoveryDidKey: env.recoveryDidKey ?? null, + handleDomains, + } + + const invitesCfg: ServerConfig['invites'] = env.inviteRequired + ? { + required: true, + interval: env.inviteInterval ?? null, + } + : { + required: false, + } + + let emailCfg: ServerConfig['email'] + if (!env.emailFromAddress && !env.emailSmtpUrl) { + emailCfg = null + } else { + if (!env.emailFromAddress || !env.emailSmtpUrl) { + throw new Error('Partial email config') + } + emailCfg = { + smtpUrl: env.emailSmtpUrl, + fromAddress: env.emailFromAddress, + } + } + + const subscriptionCfg: ServerConfig['subscription'] = { + maxBuffer: env.maxSubscriptionBuffer ?? 500, + repoBackfillLimitMs: env.repoBackfillLimitMs ?? DAY, + sequencerLeaderLockId: env.sequencerLeaderLockId ?? 1100, + } + + const bskyAppViewCfg: ServerConfig['bskyAppView'] = { + endpoint: env.bskyAppViewEndpoint ?? 'https://api.bsky-sandbox.dev', + did: env.bskyAppViewDid ?? 'did:plc:abc', // get real did + } + + const crawlersCfg: ServerConfig['crawlers'] = env.crawlers ?? [] + + return { + service: serviceCfg, + db: dbCfg, + blobstore: blobstoreCfg, + identity: identityCfg, + invites: invitesCfg, + email: emailCfg, + subscription: subscriptionCfg, + bskyAppView: bskyAppViewCfg, + crawlers: crawlersCfg, + } +} + +export type ServerConfig = { + service: ServiceConfig + db: SqliteConfig | PostgresConfig + blobstore: S3BlobstoreConfig | DiskBlobstoreConfig + identity: IdentityConfig + invites: InvitesConfig + email: EmailConfig | null + subscription: SubscriptionConfig + bskyAppView: BksyAppViewConfig + crawlers: string[] +} + +export type ServiceConfig = { + port: number + hostname: string + did: string + version?: string + privacyPolicyUrl?: string + termsOfServiceUrl?: string +} + +export type SqliteConfig = { + dialect: 'sqlite' + location: string +} + +export type PostgresPoolConfig = { + size: number + maxUses: number + idleTimeoutMs: number +} + +export type PostgresConfig = { + dialect: 'pg' + url: string + migrationUrl: string + pool: PostgresPoolConfig + schema?: string +} + +export type S3BlobstoreConfig = { + provider: 's3' + bucket: string +} + +export type DiskBlobstoreConfig = { + provider: 'disk' + location: string + tempLocation: string +} + +export type IdentityConfig = { + plcUrl: string + resolverTimeout: number + cacheStaleTTL: number + cacheMaxTTL: number + recoveryDidKey: string | null + handleDomains: string[] +} + +export type InvitesConfig = + | { + required: true + interval: number | null + } + | { + required: false + } + +export type EmailConfig = { + smtpUrl: string + fromAddress: string +} + +export type SubscriptionConfig = { + maxBuffer: number + repoBackfillLimitMs: number + sequencerLeaderLockId: number +} + +export type BksyAppViewConfig = { + endpoint: string + did: string +} diff --git a/packages/pds/src/config/env.ts b/packages/pds/src/config/env.ts new file mode 100644 index 00000000000..f47b39a4d3d --- /dev/null +++ b/packages/pds/src/config/env.ts @@ -0,0 +1,114 @@ +import { envInt, envStr, envBool, envList } from './util' + +export const readEnv = (): ServerEnvironment => { + return { + port: envInt(process.env.PORT), + hostname: envStr(process.env.HOSTNAME), + serviceDid: envStr(process.env.SERVICE_DID), + version: envStr(process.env.PDS_VERSION), + privacyPolicyUrl: envStr(process.env.PRIVACY_POLICY_URL), + termsOfServiceUrl: envStr(process.env.TERMS_OF_SERVICE_URL), + dbSqliteLocation: envStr(process.env.DB_SQLITE_LOCATION), + dbPostgresUrl: envStr(process.env.DB_POSTGRES_URL), + dbPostgresMigrationUrl: envStr(process.env.DB_POSTGRES_MIGRATION_URL), + dbPostgresSchema: envStr(process.env.DB_POSTGRES_SCHEMA), + dbPostgresPoolSize: envInt(process.env.DB_POSTGRES_POOL_SIZE), + dbPostgresPoolMaxUses: envInt(process.env.DB_POSTGRES_POOL_MAX_USES), + dbPostgresPoolIdleTimeoutMs: envInt( + process.env.DB_POSTGRES_POOL_IDLE_TIMEOUT_MS, + ), + blobstoreS3Bucket: envStr(process.env.BLOBSTORE_S3_BUCKET), + blobstoreDiskLocation: envStr(process.env.BLOBSTORE_DISK_LOCATION), + blobstoreDiskTmpLocation: envStr(process.env.BLOBSTORE_DISK_TMP_LOCATION), + didPlcUrl: envStr(process.env.DID_PLC_URL), + didCacheStaleTTL: envInt(process.env.DID_CACHE_STALE_TTL), + didCacheMaxTTL: envInt(process.env.DID_CACHE_MAX_TTL), + resolverTimeout: envInt(process.env.ID_RESOLVER_TIMEOUT), + recoveryDidKey: envStr(process.env.RECOVERY_DID_KEY), + handleDomains: envList(process.env.HANDLE_DOMAINS), + inviteRequired: envBool(process.env.INVITE_REQUIRED), + inviteInterval: envInt(process.env.INVITE_INTERVAL), + emailSmtpUrl: envStr(process.env.EMAIL_SMTP_URL), + emailFromAddress: envStr(process.env.EMAIL_FROM_ADDRESS), + maxSubscriptionBuffer: envInt(process.env.MAX_SUBSCRIPTION_BUFFER), + repoBackfillLimitMs: envInt(process.env.REPO_BACKFILL_LIMIT_MS), + sequencerLeaderLockId: envInt(process.env.SEQUENCER_LEADER_LOCK_ID), + bskyAppViewEndpoint: envStr(process.env.BSKY_APP_VIEW_ENDPOINT), + bskyAppViewDid: envStr(process.env.BSKY_APP_VIEW_DID), + crawlers: envList(process.env.CRAWLERS), + jwtSecret: envStr(process.env.JWT_SECRET), + adminPassword: envStr(process.env.ADMIN_PASSWORD), + moderatorPassword: envStr(process.env.MODERATOR_PASSWORD), + repoSigningKeyKmsKeyId: envStr(process.env.REPO_SIGNING_KEY_KMS_KEY_ID), + repoSigningKeyK256PrivateKeyHex: envStr( + process.env.REPO_SIGNING_KEY_K256_PRIVATE_KEY_HEX, + ), + plcRotationKeyKmsKeyId: envStr(process.env.PLC_ROTATION_KEY_KMS_KEY_ID), + plcRotationKeyK256PrivateKeyHex: envStr( + process.env.PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX, + ), + } +} + +export type ServerEnvironment = { + // infra + port?: number + hostname?: string + serviceDid?: string + version?: string + privacyPolicyUrl?: string + termsOfServiceUrl?: string + + // db: one required + dbSqliteLocation?: string + dbPostgresUrl?: string + dbPostgresMigrationUrl?: string + dbPostgresSchema?: string + dbPostgresPoolSize?: number + dbPostgresPoolMaxUses?: number + dbPostgresPoolIdleTimeoutMs?: number + + // blobstore: one required + blobstoreS3Bucket?: string + blobstoreDiskLocation?: string + blobstoreDiskTmpLocation?: string + + // identity + didPlcUrl?: string + didCacheStaleTTL?: number + didCacheMaxTTL?: number + resolverTimeout?: number + recoveryDidKey?: string + handleDomains?: string[] // public hostname by default + + // accounts + inviteRequired?: boolean + inviteInterval?: number + + // email + emailSmtpUrl?: string + emailFromAddress?: string + + // subscription + maxSubscriptionBuffer?: number + repoBackfillLimitMs?: number + sequencerLeaderLockId?: number + + // appview + bskyAppViewEndpoint?: string + bskyAppViewDid?: string + + // crawler + crawlers?: string[] + + // secrets + jwtSecret?: string + adminPassword?: string + moderatorPassword?: string + + // keys + repoSigningKeyKmsKeyId?: string + repoSigningKeyK256PrivateKeyHex?: string + plcRotationKeyKmsKeyId?: string + plcRotationKeyK256PrivateKeyHex?: string +} diff --git a/packages/pds/src/config/index.ts b/packages/pds/src/config/index.ts new file mode 100644 index 00000000000..cd02efb6c87 --- /dev/null +++ b/packages/pds/src/config/index.ts @@ -0,0 +1,3 @@ +export * from './config' +export * from './env' +export * from './secrets' diff --git a/packages/pds/src/config/secrets.ts b/packages/pds/src/config/secrets.ts new file mode 100644 index 00000000000..bb26b1bcd80 --- /dev/null +++ b/packages/pds/src/config/secrets.ts @@ -0,0 +1,71 @@ +import { ServerEnvironment } from './env' + +export const envToSecrets = (env: ServerEnvironment): ServerSecrets => { + let repoSigningKey: ServerSecrets['repoSigningKey'] + if (env.repoSigningKeyKmsKeyId && env.repoSigningKeyK256PrivateKeyHex) { + throw new Error('Cannot set both kms & memory keys for repo signing key') + } else if (env.repoSigningKeyKmsKeyId) { + repoSigningKey = { + provider: 'kms', + keyId: env.repoSigningKeyKmsKeyId, + } + } else if (env.repoSigningKeyK256PrivateKeyHex) { + repoSigningKey = { + provider: 'memory', + privateKeyHex: env.repoSigningKeyK256PrivateKeyHex, + } + } else { + throw new Error('Must configure repo signing key') + } + + let plcRotationKey: ServerSecrets['plcRotationKey'] + if (env.plcRotationKeyKmsKeyId && env.plcRotationKeyK256PrivateKeyHex) { + throw new Error('Cannot set both kms & memory keys for plc rotation key') + } else if (env.plcRotationKeyKmsKeyId) { + plcRotationKey = { + provider: 'kms', + keyId: env.plcRotationKeyKmsKeyId, + } + } else if (env.plcRotationKeyK256PrivateKeyHex) { + plcRotationKey = { + provider: 'memory', + privateKeyHex: env.plcRotationKeyK256PrivateKeyHex, + } + } else { + throw new Error('Must configure plc rotation key') + } + + if (!env.jwtSecret) { + throw new Error('Must provide a JWT secret') + } + + if (!env.adminPassword) { + throw new Error('Must provide an admin password') + } + + return { + jwtSecret: env.jwtSecret, + adminPassword: env.adminPassword, + moderatorPassword: env.moderatorPassword || env.adminPassword, + repoSigningKey, + plcRotationKey, + } +} + +export type ServerSecrets = { + jwtSecret: string + adminPassword: string + moderatorPassword: string + repoSigningKey: SigningKeyKms | SigningKeyMemory + plcRotationKey: SigningKeyKms | SigningKeyMemory +} + +export type SigningKeyKms = { + provider: 'kms' + keyId: string +} + +export type SigningKeyMemory = { + provider: 'memory' + privateKeyHex: string +} diff --git a/packages/pds/src/config/util.ts b/packages/pds/src/config/util.ts new file mode 100644 index 00000000000..9d6cf8ea568 --- /dev/null +++ b/packages/pds/src/config/util.ts @@ -0,0 +1,19 @@ +import { parseIntWithFallback } from '@atproto/common' + +export const envInt = (str: string | undefined): number | undefined => { + return parseIntWithFallback(str, undefined) +} + +export const envStr = (str: string | undefined): string | undefined => { + if (str === undefined || str.length === 0) return undefined + return str +} + +export const envBool = (str: string | undefined): boolean => { + return str === 'true' || str === '1' +} + +export const envList = (str: string | undefined): string[] => { + if (str === undefined || str.length === 0) return [] + return str.split(',') +} diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index e16fa2f124d..d9d40240739 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -1,66 +1,182 @@ -import express from 'express' +import * as nodemailer from 'nodemailer' import * as plc from '@did-plc/lib' import * as crypto from '@atproto/crypto' import { IdResolver } from '@atproto/identity' import { AtpAgent } from '@atproto/api' import { createServiceAuthHeaders } from '@atproto/xrpc-server' import { Database } from './db' -import { ServerConfig } from './config' +import { ServerConfig, ServerSecrets } from './config' import * as auth from './auth' +import { ServerAuth } from './auth' import { ServerMailer } from './mailer' import { BlobStore } from '@atproto/repo' -import { Services } from './services' +import { Services, createServices } from './services' import { Sequencer, SequencerLeader } from './sequencer' import { BackgroundQueue } from './background' import DidSqlCache from './did-cache' import { Crawlers } from './crawlers' +import { KmsKeypair, S3BlobStore } from '@atproto/aws' +import { DiskBlobStore } from './storage' + +export type AppContextOptions = { + db: Database + blobstore: BlobStore + mailer: ServerMailer + didCache: DidSqlCache + idResolver: IdResolver + plcClient: plc.Client + services: Services + sequencer: Sequencer + sequencerLeader: SequencerLeader + backgroundQueue: BackgroundQueue + crawlers: Crawlers + appViewAgent: AtpAgent + auth: auth.ServerAuth + repoSigningKey: crypto.Keypair + plcRotationKey: crypto.Keypair + cfg: ServerConfig +} export class AppContext { - private _appviewAgent: AtpAgent | null - - constructor( - private opts: { - db: Database - blobstore: BlobStore - repoSigningKey: crypto.Keypair - plcRotationKey: crypto.Keypair - idResolver: IdResolver - didCache: DidSqlCache - auth: auth.ServerAuth - cfg: ServerConfig - mailer: ServerMailer - services: Services - sequencer: Sequencer - sequencerLeader: SequencerLeader - backgroundQueue: BackgroundQueue - crawlers: Crawlers - }, - ) { - this._appviewAgent = opts.cfg.bskyAppViewEndpoint - ? new AtpAgent({ - service: opts.cfg.bskyAppViewEndpoint, - }) - : null - } + public db: Database + public blobstore: BlobStore + public mailer: ServerMailer + public didCache: DidSqlCache + public idResolver: IdResolver + public plcClient: plc.Client + public services: Services + public sequencer: Sequencer + public sequencerLeader: SequencerLeader + public backgroundQueue: BackgroundQueue + public crawlers: Crawlers + public appViewAgent: AtpAgent + public auth: auth.ServerAuth + public repoSigningKey: crypto.Keypair + public plcRotationKey: crypto.Keypair + public cfg: ServerConfig + + constructor(opts: AppContextOptions) { + this.db = opts.db + this.blobstore = opts.blobstore + this.mailer = opts.mailer + this.didCache = opts.didCache + this.idResolver = opts.idResolver + this.plcClient = opts.plcClient + this.services = opts.services + this.sequencer = opts.sequencer + this.sequencerLeader = opts.sequencerLeader + this.backgroundQueue = opts.backgroundQueue + this.crawlers = opts.crawlers + this.appViewAgent = opts.appViewAgent + this.auth = opts.auth + this.repoSigningKey = opts.repoSigningKey + this.plcRotationKey = opts.plcRotationKey + this.cfg = opts.cfg + } + + static async fromConfig( + cfg: ServerConfig, + secrets: ServerSecrets, + overrides?: Partial, + ): Promise { + const db = + cfg.db.dialect === 'sqlite' + ? Database.sqlite(cfg.db.location) + : Database.postgres({ + url: cfg.db.url, + schema: cfg.db.schema, + poolSize: cfg.db.pool.size, + poolMaxUses: cfg.db.pool.maxUses, + poolIdleTimeoutMs: cfg.db.pool.idleTimeoutMs, + }) + const blobstore = + cfg.blobstore.provider === 's3' + ? new S3BlobStore({ bucket: cfg.blobstore.bucket }) + : await DiskBlobStore.create( + cfg.blobstore.location, + cfg.blobstore.tempLocation, + ) + + const mailTransport = + cfg.email !== null + ? nodemailer.createTransport(cfg.email.smtpUrl) + : nodemailer.createTransport({ jsonTransport: true }) + + const mailer = new ServerMailer(mailTransport, cfg) + + const didCache = new DidSqlCache( + db, + cfg.identity.cacheStaleTTL, + cfg.identity.cacheMaxTTL, + ) + const idResolver = new IdResolver({ + plcUrl: cfg.identity.plcUrl, + didCache, + timeout: cfg.identity.resolverTimeout, + }) + const plcClient = new plc.Client(cfg.identity.plcUrl) - get db(): Database { - return this.opts.db - } + const sequencer = new Sequencer(db) + const sequencerLeader = new SequencerLeader( + db, + cfg.subscription.sequencerLeaderLockId, + ) - get blobstore(): BlobStore { - return this.opts.blobstore - } + const backgroundQueue = new BackgroundQueue(db) + const crawlers = new Crawlers(cfg.service.hostname, cfg.crawlers) - get repoSigningKey(): crypto.Keypair { - return this.opts.repoSigningKey - } + const appViewAgent = new AtpAgent({ service: cfg.bskyAppView.endpoint }) - get plcRotationKey(): crypto.Keypair { - return this.opts.plcRotationKey - } + const auth = new ServerAuth({ + jwtSecret: secrets.jwtSecret, + adminPass: secrets.adminPassword, + moderatorPass: secrets.moderatorPassword, + }) - get auth(): auth.ServerAuth { - return this.opts.auth + const repoSigningKey = + secrets.repoSigningKey.provider === 'kms' + ? await KmsKeypair.load({ + keyId: secrets.repoSigningKey.keyId, + }) + : await crypto.Secp256k1Keypair.import( + secrets.repoSigningKey.privateKeyHex, + ) + + const plcRotationKey = + secrets.plcRotationKey.provider === 'kms' + ? await KmsKeypair.load({ + keyId: secrets.plcRotationKey.keyId, + }) + : await crypto.Secp256k1Keypair.import( + secrets.plcRotationKey.privateKeyHex, + ) + + const services = createServices({ + repoSigningKey, + blobstore, + backgroundQueue, + crawlers, + }) + + return new AppContext({ + db, + blobstore, + mailer, + didCache, + idResolver, + plcClient, + services, + sequencer, + sequencerLeader, + backgroundQueue, + crawlers, + appViewAgent, + auth, + repoSigningKey, + plcRotationKey, + cfg, + ...(overrides ?? {}), + }) } get accessVerifier() { @@ -87,48 +203,8 @@ export class AppContext { return auth.moderatorVerifier(this.auth) } - get cfg(): ServerConfig { - return this.opts.cfg - } - - get mailer(): ServerMailer { - return this.opts.mailer - } - - get services(): Services { - return this.opts.services - } - - get sequencer(): Sequencer { - return this.opts.sequencer - } - - get sequencerLeader(): SequencerLeader { - return this.opts.sequencerLeader - } - - get backgroundQueue(): BackgroundQueue { - return this.opts.backgroundQueue - } - - get crawlers(): Crawlers { - return this.opts.crawlers - } - - get plcClient(): plc.Client { - return new plc.Client(this.cfg.didPlcUrl) - } - - get idResolver(): IdResolver { - return this.opts.idResolver - } - - get didCache(): DidSqlCache { - return this.opts.didCache - } - async serviceAuthHeaders(did: string, audience?: string) { - const aud = audience ?? this.cfg.bskyAppViewDid + const aud = audience ?? this.cfg.bskyAppView.did if (!aud) { throw new Error('Could not find bsky appview did') } @@ -138,20 +214,6 @@ export class AppContext { keypair: this.repoSigningKey, }) } - - get appviewAgent(): AtpAgent { - if (!this._appviewAgent) { - throw new Error('Could not find bsky appview endpoint') - } - return this._appviewAgent - } - - canProxy(req: express.Request): boolean { - return ( - this.cfg.bskyAppViewEndpoint !== undefined && - req.get('x-appview-proxy') !== undefined - ) - } } export default AppContext diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index 38cfba0dae0..c5378e5ba0e 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -8,30 +8,16 @@ import express from 'express' import cors from 'cors' import http from 'http' import events from 'events' -import { createTransport } from 'nodemailer' -import * as crypto from '@atproto/crypto' -import { BlobStore } from '@atproto/repo' import API from './api' import * as basicRoutes from './basic-routes' import * as wellKnown from './well-known' -import Database from './db' -import { ServerAuth } from './auth' import * as error from './error' import { dbLogger, loggerMiddleware } from './logger' -import { ServerConfig } from './config' -import { ServerMailer } from './mailer' +import { ServerConfig, ServerSecrets } from './config' import { createServer } from './lexicon' -import { createServices } from './services' import { createHttpTerminator, HttpTerminator } from 'http-terminator' -import AppContext from './context' -import { Sequencer, SequencerLeader } from './sequencer' -import { BackgroundQueue } from './background' -import DidSqlCache from './did-cache' -import { IdResolver } from '@atproto/identity' -import { Crawlers } from './crawlers' +import AppContext, { AppContextOptions } from './context' -// export type { ServerConfigValues } from './config' -export { ServerConfig } from './config' export { Database } from './db' export { DiskBlobStore, MemoryBlobStore } from './storage' export { AppContext } from './context' @@ -43,83 +29,26 @@ export class PDS { private terminator?: HttpTerminator private dbStatsInterval?: NodeJS.Timer - constructor(opts: { - ctx: AppContext - app: express.Application - sequencerLeader: SequencerLeader - }) { + constructor(opts: { ctx: AppContext; app: express.Application }) { this.ctx = opts.ctx this.app = opts.app } - static create(opts: { - db: Database - blobstore: BlobStore - repoSigningKey: crypto.Keypair - plcRotationKey: crypto.Keypair - config: ServerConfig - }): PDS { - const { db, blobstore, repoSigningKey, plcRotationKey, config } = opts - const auth = new ServerAuth({ - jwtSecret: config.jwtSecret, - adminPass: config.adminPassword, - moderatorPass: config.moderatorPassword, - }) - - const didCache = new DidSqlCache( - db, - config.didCacheStaleTTL, - config.didCacheMaxTTL, - ) - const idResolver = new IdResolver({ plcUrl: config.didPlcUrl, didCache }) - - const sequencer = new Sequencer(db) - const sequencerLeader = new SequencerLeader( - db, - config.sequencerLeaderLockId, - ) - - const mailTransport = - config.emailSmtpUrl !== undefined - ? createTransport(config.emailSmtpUrl) - : createTransport({ jsonTransport: true }) - - const mailer = new ServerMailer(mailTransport, config) - + static async create(opts: { + cfg: ServerConfig + secrets: ServerSecrets + overrides?: Partial + }): Promise { const app = express() app.use(cors()) app.use(loggerMiddleware) - const backgroundQueue = new BackgroundQueue(db) - const crawlers = new Crawlers( - config.publicHostname, - config.crawlersToNotify ?? [], + const ctx = await AppContext.fromConfig( + opts.cfg, + opts.secrets, + opts.overrides, ) - const services = createServices({ - repoSigningKey, - blobstore, - backgroundQueue, - crawlers, - }) - - const ctx = new AppContext({ - db, - blobstore, - repoSigningKey, - plcRotationKey, - idResolver, - didCache, - cfg: config, - auth, - sequencer, - sequencerLeader, - services, - mailer, - backgroundQueue, - crawlers, - }) - let server = createServer({ validateResponse: false, payload: { @@ -139,7 +68,6 @@ export class PDS { return new PDS({ ctx, app, - sequencerLeader, }) } @@ -168,7 +96,7 @@ export class PDS { this.ctx.sequencerLeader.run() await this.ctx.sequencer.start() await this.ctx.db.startListeningToChannels() - const server = this.app.listen(this.ctx.cfg.port) + const server = this.app.listen(this.ctx.cfg.service.port) this.server = server this.server.keepAliveTimeout = 90000 this.terminator = createHttpTerminator({ server }) diff --git a/packages/pds/src/mailer/index.ts b/packages/pds/src/mailer/index.ts index 32408870675..ecba99f6fcf 100644 --- a/packages/pds/src/mailer/index.ts +++ b/packages/pds/src/mailer/index.ts @@ -56,10 +56,10 @@ export class ServerMailer { }) const res = await this.transporter.sendMail({ ...mailOpts, - from: mailOpts.from ?? this.config.emailNoReplyAddress, + from: mailOpts.from ?? this.config.email?.fromAddress, html, }) - if (!this.config.emailSmtpUrl) { + if (!this.config.email?.smtpUrl) { mailerLogger.debug( 'No SMTP URL has been configured. Intended to send email:\n' + JSON.stringify(res, null, 2), diff --git a/packages/pds/tsconfig.json b/packages/pds/tsconfig.json index c2b548249fe..25920c22d24 100644 --- a/packages/pds/tsconfig.json +++ b/packages/pds/tsconfig.json @@ -9,6 +9,7 @@ "include": ["./src","__tests__/**/**.ts"], "references": [ { "path": "../api/tsconfig.build.json" }, + { "path": "../aws/tsconfig.build.json" }, { "path": "../common/tsconfig.build.json" }, { "path": "../crypto/tsconfig.build.json" }, { "path": "../identity/tsconfig.build.json" }, From 68a2dafc54b9f4376aceb2a59cb46833bdcbd87d Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 13 Jun 2023 12:41:29 -0500 Subject: [PATCH 026/105] comments --- packages/pds/src/config/env.ts | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/pds/src/config/env.ts b/packages/pds/src/config/env.ts index f47b39a4d3d..0b64efee201 100644 --- a/packages/pds/src/config/env.ts +++ b/packages/pds/src/config/env.ts @@ -2,13 +2,18 @@ import { envInt, envStr, envBool, envList } from './util' export const readEnv = (): ServerEnvironment => { return { + // service port: envInt(process.env.PORT), hostname: envStr(process.env.HOSTNAME), serviceDid: envStr(process.env.SERVICE_DID), version: envStr(process.env.PDS_VERSION), privacyPolicyUrl: envStr(process.env.PRIVACY_POLICY_URL), termsOfServiceUrl: envStr(process.env.TERMS_OF_SERVICE_URL), + + // db: one required + // sqlite dbSqliteLocation: envStr(process.env.DB_SQLITE_LOCATION), + // postgres dbPostgresUrl: envStr(process.env.DB_POSTGRES_URL), dbPostgresMigrationUrl: envStr(process.env.DB_POSTGRES_MIGRATION_URL), dbPostgresSchema: envStr(process.env.DB_POSTGRES_SCHEMA), @@ -17,33 +22,57 @@ export const readEnv = (): ServerEnvironment => { dbPostgresPoolIdleTimeoutMs: envInt( process.env.DB_POSTGRES_POOL_IDLE_TIMEOUT_MS, ), + + // blobstore: one required + // s3 blobstoreS3Bucket: envStr(process.env.BLOBSTORE_S3_BUCKET), + // disk blobstoreDiskLocation: envStr(process.env.BLOBSTORE_DISK_LOCATION), blobstoreDiskTmpLocation: envStr(process.env.BLOBSTORE_DISK_TMP_LOCATION), + + // identity didPlcUrl: envStr(process.env.DID_PLC_URL), didCacheStaleTTL: envInt(process.env.DID_CACHE_STALE_TTL), didCacheMaxTTL: envInt(process.env.DID_CACHE_MAX_TTL), resolverTimeout: envInt(process.env.ID_RESOLVER_TIMEOUT), recoveryDidKey: envStr(process.env.RECOVERY_DID_KEY), handleDomains: envList(process.env.HANDLE_DOMAINS), + + // invites inviteRequired: envBool(process.env.INVITE_REQUIRED), inviteInterval: envInt(process.env.INVITE_INTERVAL), + + // email emailSmtpUrl: envStr(process.env.EMAIL_SMTP_URL), emailFromAddress: envStr(process.env.EMAIL_FROM_ADDRESS), + + // subscription maxSubscriptionBuffer: envInt(process.env.MAX_SUBSCRIPTION_BUFFER), repoBackfillLimitMs: envInt(process.env.REPO_BACKFILL_LIMIT_MS), sequencerLeaderLockId: envInt(process.env.SEQUENCER_LEADER_LOCK_ID), + + // appview bskyAppViewEndpoint: envStr(process.env.BSKY_APP_VIEW_ENDPOINT), bskyAppViewDid: envStr(process.env.BSKY_APP_VIEW_DID), + + // crawlers crawlers: envList(process.env.CRAWLERS), + + // secrets jwtSecret: envStr(process.env.JWT_SECRET), adminPassword: envStr(process.env.ADMIN_PASSWORD), moderatorPassword: envStr(process.env.MODERATOR_PASSWORD), + + // keys: only one of each required + // kms repoSigningKeyKmsKeyId: envStr(process.env.REPO_SIGNING_KEY_KMS_KEY_ID), + // memory repoSigningKeyK256PrivateKeyHex: envStr( process.env.REPO_SIGNING_KEY_K256_PRIVATE_KEY_HEX, ), + // kms plcRotationKeyKmsKeyId: envStr(process.env.PLC_ROTATION_KEY_KMS_KEY_ID), + // memory plcRotationKeyK256PrivateKeyHex: envStr( process.env.PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX, ), @@ -51,7 +80,7 @@ export const readEnv = (): ServerEnvironment => { } export type ServerEnvironment = { - // infra + // service port?: number hostname?: string serviceDid?: string @@ -81,7 +110,7 @@ export type ServerEnvironment = { recoveryDidKey?: string handleDomains?: string[] // public hostname by default - // accounts + // invites inviteRequired?: boolean inviteInterval?: number From bde6307f97f5218d4021472f65936cd70678d457 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 13 Jun 2023 12:52:28 -0500 Subject: [PATCH 027/105] building --- packages/pds/build.js | 2 +- packages/pds/package.json | 2 - packages/pds/src/api/app/bsky/proxied.ts | 60 +++++++++---------- .../com/atproto/admin/updateAccountHandle.ts | 2 +- .../api/com/atproto/identity/resolveHandle.ts | 2 +- .../api/com/atproto/identity/updateHandle.ts | 2 +- .../api/com/atproto/server/createAccount.ts | 19 +++--- .../api/com/atproto/server/describeServer.ts | 8 +-- .../atproto/server/getAccountInviteCodes.ts | 8 ++- .../pds/src/api/com/atproto/server/util.ts | 2 +- .../api/com/atproto/sync/subscribeRepos.ts | 4 +- packages/pds/src/basic-routes.ts | 2 +- packages/pds/src/bin.ts | 52 ---------------- packages/pds/src/config/config.ts | 6 ++ packages/pds/src/well-known.ts | 2 +- 15 files changed, 66 insertions(+), 107 deletions(-) delete mode 100644 packages/pds/src/bin.ts diff --git a/packages/pds/build.js b/packages/pds/build.js index 99800275d50..151a73e2808 100644 --- a/packages/pds/build.js +++ b/packages/pds/build.js @@ -14,7 +14,7 @@ if (process.argv.includes('--update-main-to-dist')) { require('esbuild').build({ logLevel: 'info', - entryPoints: ['src/index.ts', 'src/bin.ts', 'src/db/index.ts'], + entryPoints: ['src/index.ts', 'src/db/index.ts'], bundle: true, sourcemap: true, outdir: 'dist', diff --git a/packages/pds/package.json b/packages/pds/package.json index eed3e5345cd..e319a8daded 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -8,12 +8,10 @@ "directory": "packages/pds" }, "main": "src/index.ts", - "bin": "dist/bin.ts", "scripts": { "codegen": "lex gen-server ./src/lexicon ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", - "start": "node dist/bin.js", "test": "../pg/with-test-db.sh jest", "test:sqlite": "jest --testPathIgnorePatterns /tests/proxied/*", "test:log": "tail -50 test.log | pino-pretty", diff --git a/packages/pds/src/api/app/bsky/proxied.ts b/packages/pds/src/api/app/bsky/proxied.ts index b2bf7994481..744c736e885 100644 --- a/packages/pds/src/api/app/bsky/proxied.ts +++ b/packages/pds/src/api/app/bsky/proxied.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ auth, params }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.actor.getProfile( + const res = await ctx.appViewAgent.api.app.bsky.actor.getProfile( params, await ctx.serviceAuthHeaders(requester), ) @@ -21,7 +21,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ auth, params }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.actor.getProfiles( + const res = await ctx.appViewAgent.api.app.bsky.actor.getProfiles( params, await ctx.serviceAuthHeaders(requester), ) @@ -36,7 +36,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.actor.getSuggestions( + const res = await ctx.appViewAgent.api.app.bsky.actor.getSuggestions( params, await ctx.serviceAuthHeaders(requester), ) @@ -51,7 +51,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.actor.searchActors( + const res = await ctx.appViewAgent.api.app.bsky.actor.searchActors( params, await ctx.serviceAuthHeaders(requester), ) @@ -67,7 +67,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth }) => { const requester = auth.credentials.did const res = - await ctx.appviewAgent.api.app.bsky.actor.searchActorsTypeahead( + await ctx.appViewAgent.api.app.bsky.actor.searchActorsTypeahead( params, await ctx.serviceAuthHeaders(requester), ) @@ -82,7 +82,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.feed.getActorFeeds( + const res = await ctx.appViewAgent.api.app.bsky.feed.getActorFeeds( params, await ctx.serviceAuthHeaders(requester), ) @@ -97,7 +97,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.feed.getAuthorFeed( + const res = await ctx.appViewAgent.api.app.bsky.feed.getAuthorFeed( params, await ctx.serviceAuthHeaders(requester), ) @@ -113,11 +113,11 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth }) => { const requester = auth.credentials.did const { data: feed } = - await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( + 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( + const res = await ctx.appViewAgent.api.app.bsky.feed.getFeed( params, await ctx.serviceAuthHeaders(requester, feed.view.did), ) @@ -132,7 +132,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( + const res = await ctx.appViewAgent.api.app.bsky.feed.getFeedGenerator( params, await ctx.serviceAuthHeaders(requester), ) @@ -147,7 +147,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerators( + const res = await ctx.appViewAgent.api.app.bsky.feed.getFeedGenerators( params, await ctx.serviceAuthHeaders(requester), ) @@ -162,7 +162,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.feed.getPosts( + const res = await ctx.appViewAgent.api.app.bsky.feed.getPosts( params, await ctx.serviceAuthHeaders(requester), ) @@ -177,7 +177,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.feed.getPostThread( + const res = await ctx.appViewAgent.api.app.bsky.feed.getPostThread( params, await ctx.serviceAuthHeaders(requester), ) @@ -192,7 +192,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.feed.getLikes( + const res = await ctx.appViewAgent.api.app.bsky.feed.getLikes( params, await ctx.serviceAuthHeaders(requester), ) @@ -207,7 +207,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.feed.getRepostedBy( + const res = await ctx.appViewAgent.api.app.bsky.feed.getRepostedBy( params, await ctx.serviceAuthHeaders(requester), ) @@ -222,7 +222,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.feed.getTimeline( + const res = await ctx.appViewAgent.api.app.bsky.feed.getTimeline( params, await ctx.serviceAuthHeaders(requester), ) @@ -237,7 +237,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.graph.getBlocks( + const res = await ctx.appViewAgent.api.app.bsky.graph.getBlocks( params, await ctx.serviceAuthHeaders(requester), ) @@ -252,7 +252,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.graph.getFollowers( + const res = await ctx.appViewAgent.api.app.bsky.graph.getFollowers( params, await ctx.serviceAuthHeaders(requester), ) @@ -267,7 +267,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.graph.getFollows( + const res = await ctx.appViewAgent.api.app.bsky.graph.getFollows( params, await ctx.serviceAuthHeaders(requester), ) @@ -282,7 +282,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.graph.getList( + const res = await ctx.appViewAgent.api.app.bsky.graph.getList( params, await ctx.serviceAuthHeaders(requester), ) @@ -297,7 +297,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.graph.getListMutes( + const res = await ctx.appViewAgent.api.app.bsky.graph.getListMutes( params, await ctx.serviceAuthHeaders(requester), ) @@ -312,7 +312,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.graph.getLists( + const res = await ctx.appViewAgent.api.app.bsky.graph.getLists( params, await ctx.serviceAuthHeaders(requester), ) @@ -327,7 +327,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.graph.getMutes( + const res = await ctx.appViewAgent.api.app.bsky.graph.getMutes( params, await ctx.serviceAuthHeaders(requester), ) @@ -342,7 +342,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ input, auth }) => { const requester = auth.credentials.did - await ctx.appviewAgent.api.app.bsky.graph.muteActor(input.body, { + await ctx.appViewAgent.api.app.bsky.graph.muteActor(input.body, { ...(await ctx.serviceAuthHeaders(requester)), encoding: 'application/json', }) @@ -353,7 +353,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ input, auth }) => { const requester = auth.credentials.did - await ctx.appviewAgent.api.app.bsky.graph.muteActorList(input.body, { + await ctx.appViewAgent.api.app.bsky.graph.muteActorList(input.body, { ...(await ctx.serviceAuthHeaders(requester)), encoding: 'application/json', }) @@ -364,7 +364,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ input, auth }) => { const requester = auth.credentials.did - await ctx.appviewAgent.api.app.bsky.graph.unmuteActor(input.body, { + await ctx.appViewAgent.api.app.bsky.graph.unmuteActor(input.body, { ...(await ctx.serviceAuthHeaders(requester)), encoding: 'application/json', }) @@ -375,7 +375,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ input, auth }) => { const requester = auth.credentials.did - await ctx.appviewAgent.api.app.bsky.graph.unmuteActorList(input.body, { + await ctx.appViewAgent.api.app.bsky.graph.unmuteActorList(input.body, { ...(await ctx.serviceAuthHeaders(requester)), encoding: 'application/json', }) @@ -387,7 +387,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth }) => { const requester = auth.credentials.did const res = - await ctx.appviewAgent.api.app.bsky.notification.getUnreadCount( + await ctx.appViewAgent.api.app.bsky.notification.getUnreadCount( params, await ctx.serviceAuthHeaders(requester), ) @@ -403,7 +403,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth }) => { const requester = auth.credentials.did const res = - await ctx.appviewAgent.api.app.bsky.notification.listNotifications( + await ctx.appViewAgent.api.app.bsky.notification.listNotifications( params, await ctx.serviceAuthHeaders(requester), ) @@ -418,7 +418,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ input, auth }) => { const requester = auth.credentials.did - await ctx.appviewAgent.api.app.bsky.notification.updateSeen(input.body, { + await ctx.appViewAgent.api.app.bsky.notification.updateSeen(input.body, { ...(await ctx.serviceAuthHeaders(requester)), encoding: 'application/json', }) diff --git a/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts b/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts index 6d307701f2e..aae81471508 100644 --- a/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts +++ b/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts @@ -22,7 +22,7 @@ export default function (server: Server, ctx: AppContext) { try { ident.ensureHandleServiceConstraints( handle, - ctx.cfg.availableUserDomains, + ctx.cfg.identity.handleDomains, ) } catch (err) { if (err instanceof ident.UnsupportedDomainError) { diff --git a/packages/pds/src/api/com/atproto/identity/resolveHandle.ts b/packages/pds/src/api/com/atproto/identity/resolveHandle.ts index fd64201ebbf..68a28cfc8a3 100644 --- a/packages/pds/src/api/com/atproto/identity/resolveHandle.ts +++ b/packages/pds/src/api/com/atproto/identity/resolveHandle.ts @@ -21,7 +21,7 @@ export default function (server: Server, ctx: AppContext) { if (user) { did = user.did } else { - const supportedHandle = ctx.cfg.availableUserDomains.some( + const supportedHandle = ctx.cfg.identity.handleDomains.some( (host) => handle.endsWith(host) || handle === host.slice(1), ) // this should be in our DB & we couldn't find it, so fail diff --git a/packages/pds/src/api/com/atproto/identity/updateHandle.ts b/packages/pds/src/api/com/atproto/identity/updateHandle.ts index acf1c593faa..5c77521698b 100644 --- a/packages/pds/src/api/com/atproto/identity/updateHandle.ts +++ b/packages/pds/src/api/com/atproto/identity/updateHandle.ts @@ -24,7 +24,7 @@ export default function (server: Server, ctx: AppContext) { try { ident.ensureHandleServiceConstraints( handle, - ctx.cfg.availableUserDomains, + ctx.cfg.identity.handleDomains, ) } catch (err) { if (err instanceof ident.UnsupportedDomainError) { diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index dc2c2365e1f..fa265348881 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -14,7 +14,7 @@ export default function (server: Server, ctx: AppContext) { server.com.atproto.server.createAccount(async ({ input, req }) => { const { email, password, inviteCode } = input.body - if (ctx.cfg.inviteRequired && !inviteCode) { + if (ctx.cfg.invites.required && !inviteCode) { throw new InvalidRequestError( 'No invite code provided', 'InvalidInviteCode', @@ -25,7 +25,7 @@ export default function (server: Server, ctx: AppContext) { const handle = await ensureValidHandle(ctx, input.body) // check that the invite code still has uses - if (ctx.cfg.inviteRequired && inviteCode) { + if (ctx.cfg.invites.required && inviteCode) { await ensureCodeIsAvailable(ctx.db, inviteCode) } @@ -43,7 +43,7 @@ export default function (server: Server, ctx: AppContext) { // it's a bit goofy that we run this logic twice, // but we run it once for a sanity check before doing scrypt & plc ops // & a second time for locking + integrity check - if (ctx.cfg.inviteRequired && inviteCode) { + if (ctx.cfg.invites.required && inviteCode) { await ensureCodeIsAvailable(dbTxn, inviteCode, true) } @@ -76,7 +76,7 @@ export default function (server: Server, ctx: AppContext) { } // insert invite code use - if (ctx.cfg.inviteRequired && inviteCode) { + if (ctx.cfg.invites.required && inviteCode) { await dbTxn.db .insertInto('invite_code_use') .values({ @@ -145,7 +145,7 @@ const ensureValidHandle = async ( ): Promise => { try { const handle = ident.normalizeAndEnsureValidHandle(input.handle) - ident.ensureHandleServiceConstraints(handle, ctx.cfg.availableUserDomains) + ident.ensureHandleServiceConstraints(handle, ctx.cfg.identity.handleDomains) return handle } catch (err) { if (err instanceof ident.InvalidHandleError) { @@ -178,7 +178,10 @@ const getDidAndPlcOp = async ( // if the user is not bringing a DID, then we format a create op for PLC // but we don't send until we ensure the username & email are available if (!input.did) { - const rotationKeys = [ctx.cfg.recoveryKey, ctx.plcRotationKey.did()] + const rotationKeys = [ctx.plcRotationKey.did()] + if (ctx.cfg.identity.recoveryDidKey) { + rotationKeys.unshift(ctx.cfg.identity.recoveryDidKey) + } if (input.recoveryKey) { rotationKeys.unshift(input.recoveryKey) } @@ -186,7 +189,7 @@ const getDidAndPlcOp = async ( signingKey: ctx.repoSigningKey.did(), rotationKeys, handle, - pds: ctx.cfg.publicUrl, + pds: ctx.cfg.service.publicUrl, signer: ctx.plcRotationKey, }) return { @@ -212,7 +215,7 @@ const getDidAndPlcOp = async ( 'provided handle does not match DID document handle', 'IncompatibleDidDoc', ) - } else if (atpData.pds !== ctx.cfg.publicUrl) { + } else if (atpData.pds !== ctx.cfg.service.publicUrl) { throw new InvalidRequestError( 'DID document pds endpoint does not match service endpoint', 'IncompatibleDidDoc', diff --git a/packages/pds/src/api/com/atproto/server/describeServer.ts b/packages/pds/src/api/com/atproto/server/describeServer.ts index 0a7b1594cd4..2e55676bb8c 100644 --- a/packages/pds/src/api/com/atproto/server/describeServer.ts +++ b/packages/pds/src/api/com/atproto/server/describeServer.ts @@ -3,10 +3,10 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.server.describeServer(() => { - const availableUserDomains = ctx.cfg.availableUserDomains - const inviteCodeRequired = ctx.cfg.inviteRequired - const privacyPolicy = ctx.cfg.privacyPolicyUrl - const termsOfService = ctx.cfg.termsOfServiceUrl + const availableUserDomains = ctx.cfg.identity.handleDomains + const inviteCodeRequired = ctx.cfg.invites.required + const privacyPolicy = ctx.cfg.service.privacyPolicyUrl + const termsOfService = ctx.cfg.service.termsOfServiceUrl return { encoding: 'application/json', diff --git a/packages/pds/src/api/com/atproto/server/getAccountInviteCodes.ts b/packages/pds/src/api/com/atproto/server/getAccountInviteCodes.ts index 875ae1de7c8..7eaea02a1a2 100644 --- a/packages/pds/src/api/com/atproto/server/getAccountInviteCodes.ts +++ b/packages/pds/src/api/com/atproto/server/getAccountInviteCodes.ts @@ -31,10 +31,14 @@ export default function (server: Server, ctx: AppContext) { // we allow a max of 5 open codes at a given time // note: even if a user is disabled from future invites, we still create the invites for bookkeeping, we just immediately disable them as well const now = new Date().toISOString() - if (createAvailable && ctx.cfg.userInviteInterval !== null) { + if ( + createAvailable && + ctx.cfg.invites.required && + ctx.cfg.invites.interval !== null + ) { const accountLifespan = Date.now() - new Date(user.createdAt).getTime() const couldCreate = Math.floor( - accountLifespan / ctx.cfg.userInviteInterval, + accountLifespan / ctx.cfg.invites.interval, ) const toCreate = Math.min( 5 - unusedCodes.length, diff --git a/packages/pds/src/api/com/atproto/server/util.ts b/packages/pds/src/api/com/atproto/server/util.ts index 7182968db51..71bd1ae219c 100644 --- a/packages/pds/src/api/com/atproto/server/util.ts +++ b/packages/pds/src/api/com/atproto/server/util.ts @@ -6,7 +6,7 @@ import { ServerConfig } from '../../../../config' // ex: bsky-app-abc234-567xy // regex: bsky-app-[a-z2-7]{5}-[a-z2-7]{5} export const genInvCode = (cfg: ServerConfig): string => { - return cfg.publicHostname.replaceAll('.', '-') + '-' + getRandomToken() + return cfg.service.hostname.replaceAll('.', '-') + '-' + getRandomToken() } export const genInvCodes = (cfg: ServerConfig, count: number): string[] => { diff --git a/packages/pds/src/api/com/atproto/sync/subscribeRepos.ts b/packages/pds/src/api/com/atproto/sync/subscribeRepos.ts index 9cee8aaf77a..7a7d38cd32a 100644 --- a/packages/pds/src/api/com/atproto/sync/subscribeRepos.ts +++ b/packages/pds/src/api/com/atproto/sync/subscribeRepos.ts @@ -8,12 +8,12 @@ export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.subscribeRepos(async function* ({ params, signal }) { const { cursor } = params const outbox = new Outbox(ctx.sequencer, { - maxBufferSize: ctx.cfg.maxSubscriptionBuffer, + maxBufferSize: ctx.cfg.subscription.maxBuffer, }) httpLogger.info({ cursor }, 'request to com.atproto.sync.subscribeRepos') const backfillTime = new Date( - Date.now() - ctx.cfg.repoBackfillLimitMs, + Date.now() - ctx.cfg.subscription.repoBackfillLimitMs, ).toISOString() if (cursor !== undefined) { const [next, curr] = await Promise.all([ diff --git a/packages/pds/src/basic-routes.ts b/packages/pds/src/basic-routes.ts index a4409dac487..aa094bea635 100644 --- a/packages/pds/src/basic-routes.ts +++ b/packages/pds/src/basic-routes.ts @@ -20,7 +20,7 @@ export const createRouter = (ctx: AppContext): express.Router => { }) router.get('/xrpc/_health', async function (req, res) { - const { version } = ctx.cfg + const { version } = ctx.cfg.service try { await sql`select 1`.execute(ctx.db.db) } catch (err) { diff --git a/packages/pds/src/bin.ts b/packages/pds/src/bin.ts deleted file mode 100644 index 3d3ddf7d7e8..00000000000 --- a/packages/pds/src/bin.ts +++ /dev/null @@ -1,52 +0,0 @@ -import './env' -import { ServerConfig } from './config' -import * as crypto from '@atproto/crypto' -import Database from './db' -import PDS from './index' -import { DiskBlobStore, MemoryBlobStore } from './storage' -import { BlobStore } from '@atproto/repo' - -const run = async () => { - let db: Database - - const keypair = await crypto.EcdsaKeypair.create() - const cfg = ServerConfig.readEnv({ - serverDid: keypair.did(), - recoveryKey: keypair.did(), - }) - - if (cfg.dbPostgresUrl) { - db = Database.postgres({ - url: cfg.dbPostgresUrl, - schema: cfg.dbPostgresSchema, - }) - } else if (cfg.databaseLocation) { - db = Database.sqlite(cfg.databaseLocation) - } else { - db = Database.memory() - } - - await db.migrateToLatestOrThrow() - - let blobstore: BlobStore - if (cfg.blobstoreLocation) { - blobstore = await DiskBlobStore.create( - cfg.blobstoreLocation, - cfg.blobstoreTmp, - ) - } else { - blobstore = new MemoryBlobStore() - } - - const pds = PDS.create({ - db, - blobstore, - repoSigningKey: keypair, - plcRotationKey: keypair, - config: cfg, - }) - await pds.start() - console.log(`🌞 ATP Data server is running at ${cfg.origin}`) -} - -run() diff --git a/packages/pds/src/config/config.ts b/packages/pds/src/config/config.ts index e5684ad8961..56a0f60a377 100644 --- a/packages/pds/src/config/config.ts +++ b/packages/pds/src/config/config.ts @@ -9,10 +9,15 @@ import { ServerEnvironment } from './env' export const envToCfg = (env: ServerEnvironment): ServerConfig => { const port = env.port ?? 2583 const hostname = env.hostname ?? 'localhost' + const publicUrl = + hostname === 'localhost' + ? `http://localhost:${port}` + : `https://${hostname}` const did = env.serviceDid ?? `did:web:${hostname}` const serviceCfg: ServerConfig['service'] = { port, hostname, + publicUrl, did, version: env.version, // default? privacyPolicyUrl: env.privacyPolicyUrl, @@ -150,6 +155,7 @@ export type ServerConfig = { export type ServiceConfig = { port: number hostname: string + publicUrl: string did: string version?: string privacyPolicyUrl?: string diff --git a/packages/pds/src/well-known.ts b/packages/pds/src/well-known.ts index 64474a45303..c8a340dbd89 100644 --- a/packages/pds/src/well-known.ts +++ b/packages/pds/src/well-known.ts @@ -6,7 +6,7 @@ export const createRouter = (ctx: AppContext): express.Router => { router.get('/.well-known/atproto-did', async function (req, res) { const handle = req.hostname - const supportedHandle = ctx.cfg.availableUserDomains.some( + const supportedHandle = ctx.cfg.identity.handleDomains.some( (host) => handle.endsWith(host) || handle === host.slice(1), ) if (!supportedHandle) { From def6dc40cee99d833cad01747d3ee9743f04b4c1 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 13 Jun 2023 12:54:56 -0500 Subject: [PATCH 028/105] pds prefix on env --- packages/pds/src/config/env.ts | 78 +++++++++++++++++----------------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/packages/pds/src/config/env.ts b/packages/pds/src/config/env.ts index 0b64efee201..5e583914e73 100644 --- a/packages/pds/src/config/env.ts +++ b/packages/pds/src/config/env.ts @@ -3,78 +3,80 @@ import { envInt, envStr, envBool, envList } from './util' export const readEnv = (): ServerEnvironment => { return { // service - port: envInt(process.env.PORT), - hostname: envStr(process.env.HOSTNAME), - serviceDid: envStr(process.env.SERVICE_DID), + port: envInt(process.env.PDS_PORT), + hostname: envStr(process.env.PDS_HOSTNAME), + serviceDid: envStr(process.env.PDS_SERVICE_DID), version: envStr(process.env.PDS_VERSION), - privacyPolicyUrl: envStr(process.env.PRIVACY_POLICY_URL), - termsOfServiceUrl: envStr(process.env.TERMS_OF_SERVICE_URL), + privacyPolicyUrl: envStr(process.env.PDS_PRIVACY_POLICY_URL), + termsOfServiceUrl: envStr(process.env.PDS_TERMS_OF_SERVICE_URL), // db: one required // sqlite - dbSqliteLocation: envStr(process.env.DB_SQLITE_LOCATION), + dbSqliteLocation: envStr(process.env.PDS_DB_SQLITE_LOCATION), // postgres - dbPostgresUrl: envStr(process.env.DB_POSTGRES_URL), - dbPostgresMigrationUrl: envStr(process.env.DB_POSTGRES_MIGRATION_URL), - dbPostgresSchema: envStr(process.env.DB_POSTGRES_SCHEMA), - dbPostgresPoolSize: envInt(process.env.DB_POSTGRES_POOL_SIZE), - dbPostgresPoolMaxUses: envInt(process.env.DB_POSTGRES_POOL_MAX_USES), + dbPostgresUrl: envStr(process.env.PDS_DB_POSTGRES_URL), + dbPostgresMigrationUrl: envStr(process.env.PDS_DB_POSTGRES_MIGRATION_URL), + dbPostgresSchema: envStr(process.env.PDS_DB_POSTGRES_SCHEMA), + dbPostgresPoolSize: envInt(process.env.PDS_DB_POSTGRES_POOL_SIZE), + dbPostgresPoolMaxUses: envInt(process.env.PDS_DB_POSTGRES_POOL_MAX_USES), dbPostgresPoolIdleTimeoutMs: envInt( - process.env.DB_POSTGRES_POOL_IDLE_TIMEOUT_MS, + process.env.PDS_DB_POSTGRES_POOL_IDLE_TIMEOUT_MS, ), // blobstore: one required // s3 - blobstoreS3Bucket: envStr(process.env.BLOBSTORE_S3_BUCKET), + blobstoreS3Bucket: envStr(process.env.PDS_BLOBSTORE_S3_BUCKET), // disk - blobstoreDiskLocation: envStr(process.env.BLOBSTORE_DISK_LOCATION), - blobstoreDiskTmpLocation: envStr(process.env.BLOBSTORE_DISK_TMP_LOCATION), + blobstoreDiskLocation: envStr(process.env.PDS_BLOBSTORE_DISK_LOCATION), + blobstoreDiskTmpLocation: envStr( + process.env.PDS_BLOBSTORE_DISK_TMP_LOCATION, + ), // identity - didPlcUrl: envStr(process.env.DID_PLC_URL), - didCacheStaleTTL: envInt(process.env.DID_CACHE_STALE_TTL), - didCacheMaxTTL: envInt(process.env.DID_CACHE_MAX_TTL), - resolverTimeout: envInt(process.env.ID_RESOLVER_TIMEOUT), - recoveryDidKey: envStr(process.env.RECOVERY_DID_KEY), - handleDomains: envList(process.env.HANDLE_DOMAINS), + didPlcUrl: envStr(process.env.PDS_DID_PLC_URL), + didCacheStaleTTL: envInt(process.env.PDS_DID_CACHE_STALE_TTL), + didCacheMaxTTL: envInt(process.env.PDS_DID_CACHE_MAX_TTL), + resolverTimeout: envInt(process.env.PDS_ID_RESOLVER_TIMEOUT), + recoveryDidKey: envStr(process.env.PDS_RECOVERY_DID_KEY), + handleDomains: envList(process.env.PDS_HANDLE_DOMAINS), // invites - inviteRequired: envBool(process.env.INVITE_REQUIRED), - inviteInterval: envInt(process.env.INVITE_INTERVAL), + inviteRequired: envBool(process.env.PDS_INVITE_REQUIRED), + inviteInterval: envInt(process.env.PDS_INVITE_INTERVAL), // email - emailSmtpUrl: envStr(process.env.EMAIL_SMTP_URL), - emailFromAddress: envStr(process.env.EMAIL_FROM_ADDRESS), + emailSmtpUrl: envStr(process.env.PDS_EMAIL_SMTP_URL), + emailFromAddress: envStr(process.env.PDS_EMAIL_FROM_ADDRESS), // subscription - maxSubscriptionBuffer: envInt(process.env.MAX_SUBSCRIPTION_BUFFER), - repoBackfillLimitMs: envInt(process.env.REPO_BACKFILL_LIMIT_MS), - sequencerLeaderLockId: envInt(process.env.SEQUENCER_LEADER_LOCK_ID), + maxSubscriptionBuffer: envInt(process.env.PDS_MAX_SUBSCRIPTION_BUFFER), + repoBackfillLimitMs: envInt(process.env.PDS_REPO_BACKFILL_LIMIT_MS), + sequencerLeaderLockId: envInt(process.env.PDS_SEQUENCER_LEADER_LOCK_ID), // appview - bskyAppViewEndpoint: envStr(process.env.BSKY_APP_VIEW_ENDPOINT), - bskyAppViewDid: envStr(process.env.BSKY_APP_VIEW_DID), + bskyAppViewEndpoint: envStr(process.env.PDS_BSKY_APP_VIEW_ENDPOINT), + bskyAppViewDid: envStr(process.env.PDS_BSKY_APP_VIEW_DID), // crawlers - crawlers: envList(process.env.CRAWLERS), + crawlers: envList(process.env.PDS_CRAWLERS), // secrets - jwtSecret: envStr(process.env.JWT_SECRET), - adminPassword: envStr(process.env.ADMIN_PASSWORD), - moderatorPassword: envStr(process.env.MODERATOR_PASSWORD), + jwtSecret: envStr(process.env.PDS_JWT_SECRET), + adminPassword: envStr(process.env.PDS_ADMIN_PASSWORD), + moderatorPassword: envStr(process.env.PDS_MODERATOR_PASSWORD), // keys: only one of each required // kms - repoSigningKeyKmsKeyId: envStr(process.env.REPO_SIGNING_KEY_KMS_KEY_ID), + repoSigningKeyKmsKeyId: envStr(process.env.PDS_REPO_SIGNING_KEY_KMS_KEY_ID), // memory repoSigningKeyK256PrivateKeyHex: envStr( - process.env.REPO_SIGNING_KEY_K256_PRIVATE_KEY_HEX, + process.env.PDS_REPO_SIGNING_KEY_K256_PRIVATE_KEY_HEX, ), // kms - plcRotationKeyKmsKeyId: envStr(process.env.PLC_ROTATION_KEY_KMS_KEY_ID), + plcRotationKeyKmsKeyId: envStr(process.env.PDS_PLC_ROTATION_KEY_KMS_KEY_ID), // memory plcRotationKeyK256PrivateKeyHex: envStr( - process.env.PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX, + process.env.PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX, ), } } From 03b4b884e5aebf9e6b3a22f2dbafb11ecee7a83c Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 13 Jun 2023 13:20:50 -0500 Subject: [PATCH 029/105] test env --- packages/pds/src/index.ts | 17 +++--- packages/pds/tests/_util.ts | 106 ++++++++++++------------------------ 2 files changed, 41 insertions(+), 82 deletions(-) diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index c5378e5ba0e..47049bb3eab 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -18,6 +18,7 @@ import { createServer } from './lexicon' import { createHttpTerminator, HttpTerminator } from 'http-terminator' import AppContext, { AppContextOptions } from './context' +export type { ServerConfig, ServerSecrets } from './config' export { Database } from './db' export { DiskBlobStore, MemoryBlobStore } from './storage' export { AppContext } from './context' @@ -34,20 +35,16 @@ export class PDS { this.app = opts.app } - static async create(opts: { - cfg: ServerConfig - secrets: ServerSecrets - overrides?: Partial - }): Promise { + static async create( + cfg: ServerConfig, + secrets: ServerSecrets, + overrides?: Partial, + ): Promise { const app = express() app.use(cors()) app.use(loggerMiddleware) - const ctx = await AppContext.fromConfig( - opts.cfg, - opts.secrets, - opts.overrides, - ) + const ctx = await AppContext.fromConfig(cfg, secrets, overrides) let server = createServer({ validateResponse: false, diff --git a/packages/pds/tests/_util.ts b/packages/pds/tests/_util.ts index 0895b62b876..c44ff456728 100644 --- a/packages/pds/tests/_util.ts +++ b/packages/pds/tests/_util.ts @@ -2,18 +2,16 @@ import { AddressInfo } from 'net' import os from 'os' import path from 'path' import * as crypto from '@atproto/crypto' -import * as plc from '@did-plc/lib' import { PlcServer, Database as PlcDatabase } from '@did-plc/server' import { AtUri } from '@atproto/uri' import { randomStr } from '@atproto/crypto' import { CID } from 'multiformats/cid' -import * as uint8arrays from 'uint8arrays' -import { PDS, ServerConfig, Database, MemoryBlobStore } from '../src/index' +import * as ui8 from 'uint8arrays' +import { PDS, Database } from '../src' import { FeedViewPost } from '../src/lexicon/types/app/bsky/feed/defs' -import DiskBlobStore from '../src/storage/disk-blobstore' import AppContext from '../src/context' -import { DAY, HOUR } from '@atproto/common' import { lexToJson } from '@atproto/lexicon' +import { ServerEnvironment, envToCfg, envToSecrets } from '../src/config' const ADMIN_PASSWORD = 'admin-pass' const MODERATOR_PASSWORD = 'moderator-pass' @@ -30,110 +28,77 @@ export type TestServerOpts = { } export const runTestServer = async ( - params: Partial = {}, + params: Partial = {}, opts: TestServerOpts = {}, ): Promise => { - const repoSigningKey = await crypto.Secp256k1Keypair.create() - const plcRotationKey = await crypto.Secp256k1Keypair.create() - const dbPostgresUrl = params.dbPostgresUrl || process.env.DB_POSTGRES_URL - const dbPostgresSchema = - params.dbPostgresSchema || process.env.DB_POSTGRES_SCHEMA - // run plc server + // run plc server let plcDb if (dbPostgresUrl !== undefined) { plcDb = PlcDatabase.postgres({ url: dbPostgresUrl, - schema: `plc_test_${dbPostgresSchema}`, + schema: `plc_test_${params.dbPostgresSchema}`, }) await plcDb.migrateToLatestOrThrow() } else { plcDb = PlcDatabase.mock() } - const plcServer = PlcServer.create({ db: plcDb }) const plcListener = await plcServer.start() const plcPort = (plcListener.address() as AddressInfo).port const plcUrl = `http://localhost:${plcPort}` - const recoveryKey = (await crypto.Secp256k1Keypair.create()).did() - - const plcClient = new plc.Client(plcUrl) - const serverDid = await plcClient.createDid({ - signingKey: repoSigningKey.did(), - rotationKeys: [recoveryKey, plcRotationKey.did()], - handle: 'localhost', - pds: 'https://pds.public.url', - signer: plcRotationKey, + const repoSigningKey = await crypto.Secp256k1Keypair.create({ + exportable: true, + }) + const repoSigningPriv = ui8.toString(await repoSigningKey.export(), 'hex') + const plcRotationKey = await crypto.Secp256k1Keypair.create({ + exportable: true, }) + const plcRotationPriv = ui8.toString(await plcRotationKey.export(), 'hex') + const recoveryKey = (await crypto.Secp256k1Keypair.create()).did() const blobstoreLoc = path.join(os.tmpdir(), randomStr(5, 'base32')) - const cfg = new ServerConfig({ - version: '0.0.0', - hostname: 'localhost', - serverDid, - recoveryKey, + const env: ServerEnvironment = { + dbPostgresUrl: dbPostgresUrl, + blobstoreDiskLocation: blobstoreLoc, + recoveryDidKey: recoveryKey, + didPlcUrl: plcUrl, + handleDomains: ['.test'], + sequencerLeaderLockId: uniqueLockId(), + repoSigningKeyK256PrivateKeyHex: repoSigningPriv, + plcRotationKeyK256PrivateKeyHex: plcRotationPriv, adminPassword: ADMIN_PASSWORD, moderatorPassword: MODERATOR_PASSWORD, - inviteRequired: false, - userInviteInterval: null, - didPlcUrl: plcUrl, - didCacheMaxTTL: DAY, - didCacheStaleTTL: HOUR, jwtSecret: 'jwt-secret', - availableUserDomains: ['.test'], - appUrlPasswordReset: 'app://forgot-password', - emailNoReplyAddress: 'noreply@blueskyweb.xyz', - publicUrl: 'https://pds.public.url', - dbPostgresUrl: process.env.DB_POSTGRES_URL, - blobstoreLocation: `${blobstoreLoc}/blobs`, - blobstoreTmp: `${blobstoreLoc}/tmp`, - maxSubscriptionBuffer: 200, - repoBackfillLimitMs: HOUR, - sequencerLeaderLockId: uniqueLockId(), ...params, - }) + } - const db = - cfg.dbPostgresUrl !== undefined - ? Database.postgres({ - url: cfg.dbPostgresUrl, - schema: cfg.dbPostgresSchema, - }) - : Database.memory() + const cfg = envToCfg(env) + const secrets = envToSecrets(env) + + const pds = await PDS.create(cfg, secrets) // Separate migration db on postgres in case migration changes some // connection state that we need in the tests, e.g. "alter database ... set ..." const migrationDb = - cfg.dbPostgresUrl !== undefined + cfg.db.dialect === 'pg' ? Database.postgres({ - url: cfg.dbPostgresUrl, - schema: cfg.dbPostgresSchema, + url: cfg.db.url, + schema: cfg.db.schema, }) - : db + : pds.ctx.db if (opts.migration) { await migrationDb.migrateToOrThrow(opts.migration) } else { await migrationDb.migrateToLatestOrThrow() } - if (migrationDb !== db) { + if (migrationDb !== pds.ctx.db) { await migrationDb.close() } - const blobstore = - cfg.blobstoreLocation !== undefined - ? await DiskBlobStore.create(cfg.blobstoreLocation, cfg.blobstoreTmp) - : new MemoryBlobStore() - - const pds = PDS.create({ - db, - blobstore, - repoSigningKey, - plcRotationKey, - config: cfg, - }) const pdsServer = await pds.start() const pdsPort = (pdsServer.address() as AddressInfo).port @@ -168,10 +133,7 @@ export const moderatorAuth = () => { const basicAuth = (username: string, password: string) => { return ( 'Basic ' + - uint8arrays.toString( - uint8arrays.fromString(`${username}:${password}`, 'utf8'), - 'base64pad', - ) + ui8.toString(ui8.fromString(`${username}:${password}`, 'utf8'), 'base64pad') ) } From 8efa874b27fe095b3937a4f194366fde2d1e726f Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Tue, 13 Jun 2023 14:22:37 -0400 Subject: [PATCH 030/105] collapse pds migrations down into a single migration --- .../db/migrations/20221021T162202001Z-init.ts | 314 -------------- .../20221116T234458063Z-duplicate-records.ts | 167 -------- .../migrations/20221202T212459280Z-blobs.ts | 36 -- .../migrations/20221209T210026294Z-banners.ts | 12 - .../20221212T195416407Z-post-media.ts | 31 -- .../20221215T220356370Z-password-reset-otp.ts | 35 -- .../20221226T213635517Z-mute-init.ts | 25 -- .../20221230T215012029Z-moderation-init.ts | 117 ------ ...0230127T215753149Z-indexed-at-on-record.ts | 35 -- .../20230127T224743452Z-repo-sync-data-pt1.ts | 33 -- .../20230201T200606704Z-repo-sync-data-pt2.ts | 141 ------- ...230202T170426672Z-user-partitioned-cids.ts | 82 ---- ...0230202T170435937Z-delete-account-token.ts | 16 - ...0202T172831900Z-moderation-subject-blob.ts | 60 --- .../20230202T213952826Z-repo-seq.ts | 41 -- ...0230208T081544325Z-post-hydrate-indices.ts | 54 --- ...20230208T222001557Z-user-table-did-pkey.ts | 133 ------ .../20230210T210132396Z-post-hierarchy.ts | 65 --- .../20230214T172233550Z-embed-records.ts | 15 - .../20230301T222603402Z-repo-ops.ts | 19 - .../20230304T193548198Z-pagination-indices.ts | 20 - .../20230308T234640077Z-record-indexes.ts | 20 - .../20230309T012947663Z-app-migration.ts | 14 - .../20230310T205728933Z-subscription-init.ts | 15 - .../20230313T232322844Z-blob-creator.ts | 96 ----- ...0230314T023842127Z-refresh-grace-period.ts | 18 - .../20230323T162732466Z-remove-scenes.ts | 42 -- ...e-declarations-assertions-confirmations.ts | 79 ---- .../20230328T214311001Z-votes-to-likes.ts | 135 ------ ...0230328T214311002Z-remove-post-entities.ts | 16 - .../20230328T214311003Z-backlinks.ts | 152 ------- ...8T214311004Z-profile-display-name-empty.ts | 118 ------ .../20230328T214311005Z-rework-seq.ts | 110 ----- .../20230406T185855842Z-feed-item-init.ts | 116 ------ .../20230411T175730759Z-drop-message-queue.ts | 32 -- .../migrations/20230411T180247652Z-labels.ts | 29 -- ...412T231807162Z-moderation-action-labels.ts | 70 ---- ...30416T221236745Z-app-specific-passwords.ts | 26 -- .../20230420T143821201Z-post-profile-aggs.ts | 178 -------- .../20230427T194652255Z-notif-record-index.ts | 14 - .../20230428T195614638Z-actor-block-init.ts | 27 -- ...230508T193807762Z-acct-deletion-indexes.ts | 49 --- ...0508T232711152Z-disable-account-invites.ts | 15 - .../20230509T192324175Z-seq-invalidated.ts | 93 ----- .../20230511T154721392Z-mute-lists.ts | 66 --- .../20230511T171739449Z-actor-preferences.ts | 26 -- .../20230511T200212974Z-feed-generators.ts | 43 -- ...20230523T183902064Z-algo-whats-hot-view.ts | 92 ----- .../20230529T222706121Z-suggested-follows.ts | 13 - .../20230530T213530067Z-rebase-indices.ts | 13 - .../20230605T235529700Z-outgoing-repo-seq.ts | 45 -- .../db/migrations/20230613T164932261Z-init.ts | 386 ++++++++++++++++++ packages/pds/src/db/migrations/index.ts | 52 +-- 53 files changed, 387 insertions(+), 3264 deletions(-) delete mode 100644 packages/pds/src/db/migrations/20221021T162202001Z-init.ts delete mode 100644 packages/pds/src/db/migrations/20221116T234458063Z-duplicate-records.ts delete mode 100644 packages/pds/src/db/migrations/20221202T212459280Z-blobs.ts delete mode 100644 packages/pds/src/db/migrations/20221209T210026294Z-banners.ts delete mode 100644 packages/pds/src/db/migrations/20221212T195416407Z-post-media.ts delete mode 100644 packages/pds/src/db/migrations/20221215T220356370Z-password-reset-otp.ts delete mode 100644 packages/pds/src/db/migrations/20221226T213635517Z-mute-init.ts delete mode 100644 packages/pds/src/db/migrations/20221230T215012029Z-moderation-init.ts delete mode 100644 packages/pds/src/db/migrations/20230127T215753149Z-indexed-at-on-record.ts delete mode 100644 packages/pds/src/db/migrations/20230127T224743452Z-repo-sync-data-pt1.ts delete mode 100644 packages/pds/src/db/migrations/20230201T200606704Z-repo-sync-data-pt2.ts delete mode 100644 packages/pds/src/db/migrations/20230202T170426672Z-user-partitioned-cids.ts delete mode 100644 packages/pds/src/db/migrations/20230202T170435937Z-delete-account-token.ts delete mode 100644 packages/pds/src/db/migrations/20230202T172831900Z-moderation-subject-blob.ts delete mode 100644 packages/pds/src/db/migrations/20230202T213952826Z-repo-seq.ts delete mode 100644 packages/pds/src/db/migrations/20230208T081544325Z-post-hydrate-indices.ts delete mode 100644 packages/pds/src/db/migrations/20230208T222001557Z-user-table-did-pkey.ts delete mode 100644 packages/pds/src/db/migrations/20230210T210132396Z-post-hierarchy.ts delete mode 100644 packages/pds/src/db/migrations/20230214T172233550Z-embed-records.ts delete mode 100644 packages/pds/src/db/migrations/20230301T222603402Z-repo-ops.ts delete mode 100644 packages/pds/src/db/migrations/20230304T193548198Z-pagination-indices.ts delete mode 100644 packages/pds/src/db/migrations/20230308T234640077Z-record-indexes.ts delete mode 100644 packages/pds/src/db/migrations/20230309T012947663Z-app-migration.ts delete mode 100644 packages/pds/src/db/migrations/20230310T205728933Z-subscription-init.ts delete mode 100644 packages/pds/src/db/migrations/20230313T232322844Z-blob-creator.ts delete mode 100644 packages/pds/src/db/migrations/20230314T023842127Z-refresh-grace-period.ts delete mode 100644 packages/pds/src/db/migrations/20230323T162732466Z-remove-scenes.ts delete mode 100644 packages/pds/src/db/migrations/20230328T214311000Z-remove-declarations-assertions-confirmations.ts delete mode 100644 packages/pds/src/db/migrations/20230328T214311001Z-votes-to-likes.ts delete mode 100644 packages/pds/src/db/migrations/20230328T214311002Z-remove-post-entities.ts delete mode 100644 packages/pds/src/db/migrations/20230328T214311003Z-backlinks.ts delete mode 100644 packages/pds/src/db/migrations/20230328T214311004Z-profile-display-name-empty.ts delete mode 100644 packages/pds/src/db/migrations/20230328T214311005Z-rework-seq.ts delete mode 100644 packages/pds/src/db/migrations/20230406T185855842Z-feed-item-init.ts delete mode 100644 packages/pds/src/db/migrations/20230411T175730759Z-drop-message-queue.ts delete mode 100644 packages/pds/src/db/migrations/20230411T180247652Z-labels.ts delete mode 100644 packages/pds/src/db/migrations/20230412T231807162Z-moderation-action-labels.ts delete mode 100644 packages/pds/src/db/migrations/20230416T221236745Z-app-specific-passwords.ts delete mode 100644 packages/pds/src/db/migrations/20230420T143821201Z-post-profile-aggs.ts delete mode 100644 packages/pds/src/db/migrations/20230427T194652255Z-notif-record-index.ts delete mode 100644 packages/pds/src/db/migrations/20230428T195614638Z-actor-block-init.ts delete mode 100644 packages/pds/src/db/migrations/20230508T193807762Z-acct-deletion-indexes.ts delete mode 100644 packages/pds/src/db/migrations/20230508T232711152Z-disable-account-invites.ts delete mode 100644 packages/pds/src/db/migrations/20230509T192324175Z-seq-invalidated.ts delete mode 100644 packages/pds/src/db/migrations/20230511T154721392Z-mute-lists.ts delete mode 100644 packages/pds/src/db/migrations/20230511T171739449Z-actor-preferences.ts delete mode 100644 packages/pds/src/db/migrations/20230511T200212974Z-feed-generators.ts delete mode 100644 packages/pds/src/db/migrations/20230523T183902064Z-algo-whats-hot-view.ts delete mode 100644 packages/pds/src/db/migrations/20230529T222706121Z-suggested-follows.ts delete mode 100644 packages/pds/src/db/migrations/20230530T213530067Z-rebase-indices.ts delete mode 100644 packages/pds/src/db/migrations/20230605T235529700Z-outgoing-repo-seq.ts create mode 100644 packages/pds/src/db/migrations/20230613T164932261Z-init.ts diff --git a/packages/pds/src/db/migrations/20221021T162202001Z-init.ts b/packages/pds/src/db/migrations/20221021T162202001Z-init.ts deleted file mode 100644 index 90935b8b99c..00000000000 --- a/packages/pds/src/db/migrations/20221021T162202001Z-init.ts +++ /dev/null @@ -1,314 +0,0 @@ -import { Kysely, sql } from 'kysely' -import { Dialect } from '..' - -const userTable = 'user' -const didHandleTable = 'did_handle' -const sceneTable = 'scene' -const refreshTokenTable = 'refresh_token' -const repoRootTable = 'repo_root' -const recordTable = 'record' -const ipldBlockTable = 'ipld_block' -const ipldBlockCreatorTable = 'ipld_block_creator' -const inviteCodeTable = 'invite_code' -const inviteUseTable = 'invite_code_use' -const notificationTable = 'user_notification' -const assertionTable = 'assertion' -const profileTable = 'profile' -const confirmationTable = 'confirmation' -const followTable = 'follow' -const postTable = 'post' -const postEntityTable = 'post_entity' -const repostTable = 'repost' -const trendTable = 'trend' -const voteTable = 'vote' -const messageQueueTable = 'message_queue' -const messageQueueCursorTable = 'message_queue_cursor' -const sceneMemberCountTable = 'scene_member_count' -const sceneVotesOnPostTable = 'scene_votes_on_post' - -export async function up(db: Kysely, dialect: Dialect): Promise { - if (dialect === 'pg') { - try { - // Add trigram support, supporting user search. - // Explicitly add to public schema, so the extension can be seen in all schemas. - await sql`create extension if not exists pg_trgm with schema public`.execute( - db, - ) - } catch (err: any) { - // The "if not exists" isn't bulletproof against races, and we see test suites racing to - // create the extension. So we can just ignore errors indicating the extension already exists. - if (!err?.detail?.includes?.('(pg_trgm) already exists')) throw err - } - } - - // Postgres uses the type `bytea` for variable length bytes - const binaryDatatype = dialect === 'sqlite' ? 'blob' : sql`bytea` - - // Users - await db.schema - .createTable(userTable) - .addColumn('handle', 'varchar', (col) => col.primaryKey()) - .addColumn('email', 'varchar', (col) => col.notNull().unique()) - .addColumn('password', 'varchar', (col) => col.notNull()) - .addColumn('lastSeenNotifs', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .execute() - await db.schema - .createIndex(`${userTable}_email_lower_idx`) - .unique() - .on(userTable) - .expression(sql`lower("email")`) - .execute() - // Did Handle - await db.schema - .createTable(didHandleTable) - .addColumn('did', 'varchar', (col) => col.primaryKey()) - .addColumn('handle', 'varchar', (col) => col.unique()) - .addColumn('actorType', 'varchar') - .addColumn('declarationCid', 'varchar') - .execute() - await db.schema - .createIndex(`${didHandleTable}_handle_lower_idx`) - .unique() - .on(didHandleTable) - .expression(sql`lower("handle")`) - .execute() - if (dialect === 'pg') { - await db.schema // Supports user search - .createIndex(`${didHandleTable}_handle_tgrm_idx`) - .on(didHandleTable) - .using('gist') - .expression(sql`"handle" gist_trgm_ops`) - .execute() - } - // Scenes - await db.schema - .createTable(sceneTable) - .addColumn('handle', 'varchar', (col) => col.primaryKey()) - .addColumn('owner', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .execute() - // Refresh Tokens - await db.schema - .createTable(refreshTokenTable) - .addColumn('id', 'varchar', (col) => col.primaryKey()) - .addColumn('did', 'varchar', (col) => col.notNull()) - .addColumn('expiresAt', 'varchar', (col) => col.notNull()) - .execute() - // Repo roots - await db.schema - .createTable(repoRootTable) - .addColumn('did', 'varchar', (col) => col.primaryKey()) - .addColumn('root', 'varchar', (col) => col.notNull()) - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .execute() - // Records - await db.schema - .createTable(recordTable) - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('did', 'varchar', (col) => col.notNull()) - .addColumn('collection', 'varchar', (col) => col.notNull()) - .addColumn('rkey', 'varchar', (col) => col.notNull()) - .execute() - // Ipld Blocks - await db.schema - .createTable(ipldBlockTable) - .addColumn('cid', 'varchar', (col) => col.primaryKey()) - .addColumn('size', 'integer', (col) => col.notNull()) - .addColumn('content', binaryDatatype, (col) => col.notNull()) - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .execute() - // Ipld Block Creators - await db.schema - .createTable(ipldBlockCreatorTable) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('did', 'varchar', (col) => col.notNull()) - .addPrimaryKeyConstraint(`${ipldBlockCreatorTable}_pkey`, ['cid', 'did']) - .execute() - // Invite Codes - await db.schema - .createTable(inviteCodeTable) - .addColumn('code', 'varchar', (col) => col.primaryKey()) - .addColumn('availableUses', 'integer', (col) => col.notNull()) - .addColumn('disabled', 'int2', (col) => col.defaultTo(0)) - .addColumn('forUser', 'varchar', (col) => col.notNull()) - .addColumn('createdBy', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .execute() - await db.schema - .createTable(inviteUseTable) - .addColumn('code', 'varchar', (col) => col.notNull()) - .addColumn('usedBy', 'varchar', (col) => col.notNull()) - .addColumn('usedAt', 'varchar', (col) => col.notNull()) - // Index names need to be unique per schema for postgres - .addPrimaryKeyConstraint(`${inviteUseTable}_pkey`, ['code', 'usedBy']) - .execute() - // Notifications - await db.schema - .createTable(notificationTable) - .addColumn('userDid', 'varchar', (col) => col.notNull()) - .addColumn('recordUri', 'varchar', (col) => col.notNull()) - .addColumn('recordCid', 'varchar', (col) => col.notNull()) - .addColumn('author', 'varchar', (col) => col.notNull()) - .addColumn('reason', 'varchar', (col) => col.notNull()) - .addColumn('reasonSubject', 'varchar') - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .execute() - // Assertions - await db.schema - .createTable(assertionTable) - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('assertion', 'varchar', (col) => col.notNull()) - .addColumn('subjectDid', 'varchar', (col) => col.notNull()) - .addColumn('subjectDeclarationCid', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .addColumn('confirmUri', 'varchar') - .addColumn('confirmCid', 'varchar') - .addColumn('confirmCreated', 'varchar') - .addColumn('confirmIndexed', 'varchar') - .execute() - // Profiles - await db.schema - .createTable(profileTable) - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('displayName', 'varchar', (col) => col.notNull()) - .addColumn('description', 'varchar') - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .execute() - if (dialect === 'pg') { - await db.schema // Supports user search - .createIndex(`${profileTable}_display_name_tgrm_idx`) - .on(profileTable) - .using('gist') - .expression(sql`"displayName" gist_trgm_ops`) - .execute() - } - // Follows - await db.schema - .createTable(followTable) - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('subjectDid', 'varchar', (col) => col.notNull()) - .addColumn('subjectDeclarationCid', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .execute() - // Posts - await db.schema - .createTable(postTable) - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('text', 'varchar', (col) => col.notNull()) - .addColumn('replyRoot', 'varchar') - .addColumn('replyRootCid', 'varchar') - .addColumn('replyParent', 'varchar') - .addColumn('replyParentCid', 'varchar') - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .execute() - await db.schema - .createTable(postEntityTable) - .addColumn('postUri', 'varchar', (col) => col.notNull()) - .addColumn('startIndex', 'integer', (col) => col.notNull()) - .addColumn('endIndex', 'integer', (col) => col.notNull()) - .addColumn('type', 'varchar', (col) => col.notNull()) - .addColumn('value', 'varchar', (col) => col.notNull()) - .execute() - await db.schema - .createTable(repostTable) - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('subject', 'varchar', (col) => col.notNull()) - .addColumn('subjectCid', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .execute() - await db.schema - .createTable(trendTable) - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('subject', 'varchar', (col) => col.notNull()) - .addColumn('subjectCid', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .execute() - await db.schema - .createTable(voteTable) - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('direction', 'varchar', (col) => col.notNull()) - .addColumn('subject', 'varchar', (col) => col.notNull()) - .addColumn('subjectCid', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .execute() - - let mqBuilder = db.schema.createTable(messageQueueTable) - mqBuilder = - dialect === 'pg' - ? mqBuilder.addColumn('id', 'serial', (col) => col.primaryKey()) - : mqBuilder.addColumn('id', 'integer', (col) => - col.autoIncrement().primaryKey(), - ) - mqBuilder - .addColumn('message', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .execute() - await db.schema - .createTable(messageQueueCursorTable) - .addColumn('consumer', 'varchar', (col) => col.primaryKey()) - .addColumn('cursor', 'integer', (col) => col.notNull()) - .execute() - await db.schema - .createTable(sceneMemberCountTable) - .addColumn('did', 'varchar', (col) => col.primaryKey()) - .addColumn('count', 'integer', (col) => col.notNull()) - .execute() - await db.schema - .createTable(sceneVotesOnPostTable) - .addColumn('did', 'varchar', (col) => col.notNull()) - .addColumn('subject', 'varchar', (col) => col.notNull()) - .addColumn('count', 'integer', (col) => col.notNull()) - .addColumn('postedTrending', 'int2', (col) => col.notNull()) - .addPrimaryKeyConstraint(`${sceneVotesOnPostTable}_pkey`, [ - 'did', - 'subject', - ]) - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropTable(sceneVotesOnPostTable).execute() - await db.schema.dropTable(sceneMemberCountTable).execute() - await db.schema.dropTable(messageQueueCursorTable).execute() - await db.schema.dropTable(messageQueueTable).execute() - await db.schema.dropTable(voteTable).execute() - await db.schema.dropTable(trendTable).execute() - await db.schema.dropTable(repostTable).execute() - await db.schema.dropTable(postEntityTable).execute() - await db.schema.dropTable(postTable).execute() - await db.schema.dropTable(followTable).execute() - await db.schema.dropTable(confirmationTable).execute() - await db.schema.dropTable(assertionTable).execute() - await db.schema.dropTable(profileTable).execute() - await db.schema.dropTable(notificationTable).execute() - await db.schema.dropTable(inviteUseTable).execute() - await db.schema.dropTable(inviteCodeTable).execute() - await db.schema.dropTable(ipldBlockCreatorTable).execute() - await db.schema.dropTable(ipldBlockTable).execute() - await db.schema.dropTable(recordTable).execute() - await db.schema.dropTable(repoRootTable).execute() - await db.schema.dropTable(didHandleTable).execute() - await db.schema.dropTable(userTable).execute() -} diff --git a/packages/pds/src/db/migrations/20221116T234458063Z-duplicate-records.ts b/packages/pds/src/db/migrations/20221116T234458063Z-duplicate-records.ts deleted file mode 100644 index 94aecb5ce8c..00000000000 --- a/packages/pds/src/db/migrations/20221116T234458063Z-duplicate-records.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { Kysely } from 'kysely' - -const duplicateRecordTable = 'duplicate_record' - -export async function up(db: Kysely): Promise { - await db.schema - .createTable(duplicateRecordTable) - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('duplicateOf', 'varchar', (col) => col.notNull()) - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .execute() - - await db.schema - .createTable('repost_temp') - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('subject', 'varchar', (col) => col.notNull()) - .addColumn('subjectCid', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .addUniqueConstraint('repost_unique_subject', ['creator', 'subject']) - .execute() - await db - .insertInto('repost_temp') - .expression((exp) => - exp - .selectFrom('repost') - .selectAll() - .where('uri', 'in', (qb) => - qb - .selectFrom('repost') - .select(db.fn.min('uri').as('uri')) - .groupBy(['creator', 'subject']), - ), - ) - .execute() - await db.schema.dropTable('repost').execute() - await db.schema.alterTable('repost_temp').renameTo('repost').execute() - - await db.schema - .createTable('trend_temp') - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('subject', 'varchar', (col) => col.notNull()) - .addColumn('subjectCid', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .addUniqueConstraint('trend_unique_subject', ['creator', 'subject']) - .execute() - await db - .insertInto('trend_temp') - .expression((exp) => - exp - .selectFrom('trend') - .selectAll() - .where('uri', 'in', (qb) => - qb - .selectFrom('trend') - .select(db.fn.min('uri').as('uri')) - .groupBy(['creator', 'subject']), - ), - ) - .execute() - await db.schema.dropTable('trend').execute() - await db.schema.alterTable('trend_temp').renameTo('trend').execute() - - await db.schema - .createTable('vote_temp') - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('direction', 'varchar', (col) => col.notNull()) - .addColumn('subject', 'varchar', (col) => col.notNull()) - .addColumn('subjectCid', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .addUniqueConstraint('vote_unique_subject', ['creator', 'subject']) - .execute() - await db - .insertInto('vote_temp') - .expression((exp) => - exp - .selectFrom('vote') - .selectAll() - .where('uri', 'in', (qb) => - qb - .selectFrom('vote') - .select(db.fn.min('uri').as('uri')) - .groupBy(['creator', 'subject']), - ), - ) - .execute() - await db.schema.dropTable('vote').execute() - await db.schema.alterTable('vote_temp').renameTo('vote').execute() - - await db.schema - .createTable('follow_temp') - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('subjectDid', 'varchar', (col) => col.notNull()) - .addColumn('subjectDeclarationCid', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .addUniqueConstraint('follow_unique_subject', ['creator', 'subjectDid']) - .execute() - await db - .insertInto('follow_temp') - .expression((exp) => - exp - .selectFrom('follow') - .selectAll() - .where('uri', 'in', (qb) => - qb - .selectFrom('follow') - .select(db.fn.min('uri').as('uri')) - .groupBy(['creator', 'subjectDid']), - ), - ) - .execute() - await db.schema.dropTable('follow').execute() - await db.schema.alterTable('follow_temp').renameTo('follow').execute() - - await db.schema - .createTable('assertion_temp') - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('assertion', 'varchar', (col) => col.notNull()) - .addColumn('subjectDid', 'varchar', (col) => col.notNull()) - .addColumn('subjectDeclarationCid', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .addColumn('confirmUri', 'varchar') - .addColumn('confirmCid', 'varchar') - .addColumn('confirmCreated', 'varchar') - .addColumn('confirmIndexed', 'varchar') - .addUniqueConstraint('assertion_unique_subject', [ - 'creator', - 'subjectDid', - 'assertion', - ]) - .execute() - await db - .insertInto('assertion_temp') - .expression((exp) => - exp - .selectFrom('assertion') - .selectAll() - .where('uri', 'in', (qb) => - qb - .selectFrom('assertion') - .select(db.fn.min('uri').as('uri')) - .groupBy(['creator', 'subjectDid', 'assertion']), - ), - ) - .execute() - await db.schema.dropTable('assertion').execute() - await db.schema.alterTable('assertion_temp').renameTo('assertion').execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropTable(duplicateRecordTable) -} diff --git a/packages/pds/src/db/migrations/20221202T212459280Z-blobs.ts b/packages/pds/src/db/migrations/20221202T212459280Z-blobs.ts deleted file mode 100644 index 1be7a4cb09b..00000000000 --- a/packages/pds/src/db/migrations/20221202T212459280Z-blobs.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Kysely } from 'kysely' - -const blobTable = 'blob' -const repoBlobTable = 'repo_blob' - -export async function up(db: Kysely): Promise { - await db.schema - .createTable(blobTable) - .addColumn('cid', 'varchar', (col) => col.primaryKey()) - .addColumn('mimeType', 'varchar', (col) => col.notNull()) - .addColumn('size', 'integer', (col) => col.notNull()) - .addColumn('tempKey', 'varchar') - .addColumn('width', 'integer') - .addColumn('height', 'integer') - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .execute() - await db.schema - .createTable(repoBlobTable) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('recordUri', 'varchar', (col) => col.notNull()) - .addColumn('commit', 'varchar', (col) => col.notNull()) - .addColumn('did', 'varchar', (col) => col.notNull()) - .addPrimaryKeyConstraint(`${repoBlobTable}_pkey`, ['cid', 'recordUri']) - .execute() - - await db.schema - .alterTable('profile') - .addColumn('avatarCid', 'varchar') - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.alterTable('profile').dropColumn('avatarCid').execute() - await db.schema.dropTable(repoBlobTable).execute() - await db.schema.dropTable(blobTable).execute() -} diff --git a/packages/pds/src/db/migrations/20221209T210026294Z-banners.ts b/packages/pds/src/db/migrations/20221209T210026294Z-banners.ts deleted file mode 100644 index 3b140f957de..00000000000 --- a/packages/pds/src/db/migrations/20221209T210026294Z-banners.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .alterTable('profile') - .addColumn('bannerCid', 'varchar') - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.alterTable('profile').dropColumn('bannerCid').execute() -} diff --git a/packages/pds/src/db/migrations/20221212T195416407Z-post-media.ts b/packages/pds/src/db/migrations/20221212T195416407Z-post-media.ts deleted file mode 100644 index d82d077b671..00000000000 --- a/packages/pds/src/db/migrations/20221212T195416407Z-post-media.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Kysely } from 'kysely' - -const postEmbedImageTable = 'post_embed_image' -const postEmbedExternalTable = 'post_embed_external' - -export async function up(db: Kysely): Promise { - await db.schema - .createTable(postEmbedImageTable) - .addColumn('postUri', 'varchar', (col) => col.notNull()) - .addColumn('position', 'varchar', (col) => col.notNull()) - .addColumn('imageCid', 'varchar', (col) => col.notNull()) - .addColumn('alt', 'varchar', (col) => col.notNull()) - .addPrimaryKeyConstraint(`${postEmbedImageTable}_pkey`, [ - 'postUri', - 'position', - ]) - .execute() - await db.schema - .createTable(postEmbedExternalTable) - .addColumn('postUri', 'varchar', (col) => col.primaryKey()) - .addColumn('uri', 'varchar', (col) => col.notNull()) - .addColumn('title', 'varchar', (col) => col.notNull()) - .addColumn('description', 'varchar', (col) => col.notNull()) - .addColumn('thumbCid', 'varchar') - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropTable(postEmbedExternalTable).execute() - await db.schema.dropTable(postEmbedImageTable).execute() -} diff --git a/packages/pds/src/db/migrations/20221215T220356370Z-password-reset-otp.ts b/packages/pds/src/db/migrations/20221215T220356370Z-password-reset-otp.ts deleted file mode 100644 index 122d2451f1d..00000000000 --- a/packages/pds/src/db/migrations/20221215T220356370Z-password-reset-otp.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Kysely } from 'kysely' - -const userTable = 'user' - -export async function up(db: Kysely): Promise { - await db.schema - .alterTable(userTable) - .addColumn('passwordResetToken', 'varchar') - .execute() - await db.schema - .alterTable(userTable) - .addColumn('passwordResetGrantedAt', 'varchar') - .execute() - await db.schema - .createIndex('user_password_reset_token_idx') - .unique() - .on('user') - .column('passwordResetToken') - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema - .dropIndex('user_password_reset_token_idx') - .on('user') - .execute() - await db.schema - .alterTable(userTable) - .dropColumn('passwordResetToken') - .execute() - await db.schema - .alterTable(userTable) - .dropColumn('passwordResetGrantedAt') - .execute() -} diff --git a/packages/pds/src/db/migrations/20221226T213635517Z-mute-init.ts b/packages/pds/src/db/migrations/20221226T213635517Z-mute-init.ts deleted file mode 100644 index 79f429cc9d7..00000000000 --- a/packages/pds/src/db/migrations/20221226T213635517Z-mute-init.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .createTable('mute') - .addColumn('did', 'varchar', (col) => col.notNull()) - .addColumn('mutedByDid', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addPrimaryKeyConstraint('mute_pkey', ['mutedByDid', 'did']) - .execute() - // Unrelated to muting: add notification indexing for per-user notifications - await db.schema - .createIndex('user_notification_did_indexed_at_idx') - .on('user_notification') - .columns(['userDid', 'indexedAt']) - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropTable('mute').execute() - await db.schema - .dropIndex('user_notification_did_indexed_at_idx') - .on('user_notification') - .execute() -} diff --git a/packages/pds/src/db/migrations/20221230T215012029Z-moderation-init.ts b/packages/pds/src/db/migrations/20221230T215012029Z-moderation-init.ts deleted file mode 100644 index fad76183367..00000000000 --- a/packages/pds/src/db/migrations/20221230T215012029Z-moderation-init.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Kysely } from 'kysely' -import { Dialect } from '..' - -export async function up(db: Kysely, dialect: Dialect): Promise { - // Moderation action - let builder = db.schema.createTable('moderation_action') - builder = - dialect === 'pg' - ? builder.addColumn('id', 'serial', (col) => col.primaryKey()) - : builder.addColumn('id', 'integer', (col) => - col.autoIncrement().primaryKey(), - ) - await builder - .addColumn('action', 'varchar', (col) => col.notNull()) - .addColumn('subjectType', 'varchar', (col) => col.notNull()) - .addColumn('subjectDid', 'varchar', (col) => col.notNull()) - .addColumn('subjectUri', 'varchar') - .addColumn('subjectCid', 'varchar') - .addColumn('reason', 'text', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addColumn('createdBy', 'varchar', (col) => col.notNull()) - .addColumn('reversedAt', 'varchar') - .addColumn('reversedBy', 'varchar') - .addColumn('reversedReason', 'text') - .execute() - // Repo takedowns - await db.schema - .alterTable('repo_root') - .addColumn('takedownId', 'integer') - .execute() - // Record takedowns - await db.schema - .alterTable('record') - .addColumn('takedownId', 'integer') - .execute() - if (dialect !== 'sqlite') { - // Would have to recreate table in sqlite to add these constraints - await db.schema - .alterTable('repo_root') - .addForeignKeyConstraint( - 'repo_root_takedown_id_fkey', - ['takedownId'], - 'moderation_action', - ['id'], - ) - .execute() - await db.schema - .alterTable('record') - .addForeignKeyConstraint( - 'record_takedown_id_fkey', - ['takedownId'], - 'moderation_action', - ['id'], - ) - .execute() - } - // Moderation report - builder = db.schema.createTable('moderation_report') - builder = - dialect === 'pg' - ? builder.addColumn('id', 'serial', (col) => col.primaryKey()) - : builder.addColumn('id', 'integer', (col) => - col.autoIncrement().primaryKey(), - ) - await builder - .addColumn('subjectType', 'varchar', (col) => col.notNull()) - .addColumn('subjectDid', 'varchar', (col) => col.notNull()) - .addColumn('subjectUri', 'varchar') - .addColumn('subjectCid', 'varchar') - .addColumn('reasonType', 'varchar', (col) => col.notNull()) - .addColumn('reason', 'text') - .addColumn('reportedByDid', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .execute() - // Moderation report resolutions - await db.schema - .createTable('moderation_report_resolution') - .addColumn('reportId', 'integer', (col) => - col.notNull().references('moderation_report.id'), - ) - .addColumn('actionId', 'integer', (col) => - col.notNull().references('moderation_action.id'), - ) - .addColumn('createdBy', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addPrimaryKeyConstraint('moderation_report_resolution_pkey', [ - 'reportId', - 'actionId', - ]) - .execute() - await db.schema - .createIndex('moderation_report_resolution_action_id_idx') - .on('moderation_report_resolution') - .column('actionId') - .execute() -} - -export async function down( - db: Kysely, - dialect: Dialect, -): Promise { - await db.schema.dropTable('moderation_report_resolution').execute() - await db.schema.dropTable('moderation_report').execute() - if (dialect !== 'sqlite') { - await db.schema - .alterTable('repo_root') - .dropConstraint('repo_root_takedown_id_fkey') - .execute() - await db.schema - .alterTable('record') - .dropConstraint('record_takedown_id_fkey') - .execute() - } - await db.schema.alterTable('repo_root').dropColumn('takedownId').execute() - await db.schema.alterTable('record').dropColumn('takedownId').execute() - await db.schema.dropTable('moderation_action').execute() -} diff --git a/packages/pds/src/db/migrations/20230127T215753149Z-indexed-at-on-record.ts b/packages/pds/src/db/migrations/20230127T215753149Z-indexed-at-on-record.ts deleted file mode 100644 index d43c9a2c0b8..00000000000 --- a/packages/pds/src/db/migrations/20230127T215753149Z-indexed-at-on-record.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - const now = new Date().toISOString() - await db.schema - .alterTable('record') - .addColumn('indexedAt', 'varchar', (col) => col.notNull().defaultTo(now)) - .execute() - - const ref = db.dynamic.ref - - const indexedAtForRecordQb = db - .selectFrom('ipld_block') - .whereRef('ipld_block.cid', '=', ref('record.cid')) - .select('indexedAt') - - await db - .updateTable('record') - .set({ - indexedAt: indexedAtForRecordQb, - }) - .whereExists(indexedAtForRecordQb) - .execute() - - await db.schema.alterTable('ipld_block').dropColumn('indexedAt').execute() -} - -export async function down(db: Kysely): Promise { - const now = new Date().toISOString() - await db.schema - .alterTable('ipld_block') - .addColumn('indexedAt', 'varchar', (col) => col.notNull().defaultTo(now)) - .execute() - await db.schema.alterTable('record').dropColumn('indexedAt').execute() -} diff --git a/packages/pds/src/db/migrations/20230127T224743452Z-repo-sync-data-pt1.ts b/packages/pds/src/db/migrations/20230127T224743452Z-repo-sync-data-pt1.ts deleted file mode 100644 index 37723fcff26..00000000000 --- a/packages/pds/src/db/migrations/20230127T224743452Z-repo-sync-data-pt1.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Kysely } from 'kysely' - -const commitBlockTable = 'repo_commit_block' -const commitHistoryTable = 'repo_commit_history' - -export async function up(db: Kysely): Promise { - await db.schema - .createTable(commitBlockTable) - .addColumn('commit', 'varchar', (col) => col.notNull()) - .addColumn('block', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addPrimaryKeyConstraint(`${commitBlockTable}_pkey`, [ - 'creator', - 'commit', - 'block', - ]) - .execute() - await db.schema - .createTable(commitHistoryTable) - .addColumn('commit', 'varchar', (col) => col.notNull()) - .addColumn('prev', 'varchar') - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addPrimaryKeyConstraint(`${commitHistoryTable}_pkey`, [ - 'creator', - 'commit', - ]) - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropTable(commitHistoryTable).execute() - await db.schema.dropTable(commitBlockTable).execute() -} diff --git a/packages/pds/src/db/migrations/20230201T200606704Z-repo-sync-data-pt2.ts b/packages/pds/src/db/migrations/20230201T200606704Z-repo-sync-data-pt2.ts deleted file mode 100644 index e4d5b93c5ee..00000000000 --- a/packages/pds/src/db/migrations/20230201T200606704Z-repo-sync-data-pt2.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { chunkArray } from '@atproto/common' -import { BlockMap, MemoryBlockstore } from '@atproto/repo' -import { Kysely } from 'kysely' -import { CID } from 'multiformats/cid' -import { RepoCommitBlock } from '../tables/repo-commit-block' -import { RepoCommitHistory } from '../tables/repo-commit-history' - -export async function up(db: Kysely): Promise { - const migrateUser = async (did: string, head: CID, start: CID | null) => { - const userBlocks = await db - .selectFrom('ipld_block') - .innerJoin( - 'ipld_block_creator as creator', - 'creator.cid', - 'ipld_block.cid', - ) - .where('creator.did', '=', did) - .select(['ipld_block.cid as cid', 'ipld_block.content as content']) - .execute() - - const blocks = new BlockMap() - userBlocks.forEach((row) => { - blocks.set(CID.parse(row.cid), row.content) - }) - - const storage = new MigrationStorage(blocks, db) - - const commitData = await storage.getCommits(head, start) - if (!commitData) return - - const commitBlock: RepoCommitBlock[] = [] - const commitHistory: RepoCommitHistory[] = [] - - for (let i = 0; i < commitData.length; i++) { - const commit = commitData[i] - const prev = commitData[i - 1] - commit.blocks.forEach((_bytes, cid) => { - commitBlock.push({ - commit: commit.commit.toString(), - block: cid.toString(), - creator: did, - }) - }) - commitHistory.push({ - commit: commit.commit.toString(), - prev: prev ? prev.commit.toString() : null, - creator: did, - }) - } - const ipldBlockCreators = storage.blocks.entries().map((entry) => ({ - cid: entry.cid.toString(), - did: did, - })) - - const createRepoCommitBlocks = async () => { - for (const batch of chunkArray(commitBlock, 500)) { - await db - .insertInto('repo_commit_block') - .values(batch) - .onConflict((oc) => oc.doNothing()) - .execute() - } - } - const createRepoCommitHistory = async () => { - for (const batch of chunkArray(commitHistory, 500)) { - await db - .insertInto('repo_commit_history') - .values(batch) - .onConflict((oc) => oc.doNothing()) - .execute() - } - } - const createIpldBlockCreators = async () => { - for (const batch of chunkArray(ipldBlockCreators, 500)) { - await db - .insertInto('ipld_block_creator') - .values(batch) - .onConflict((oc) => oc.doNothing()) - .execute() - } - } - - await Promise.all([ - createRepoCommitBlocks(), - createRepoCommitHistory(), - createIpldBlockCreators(), - ]) - } - - const repoHeads = await db.selectFrom('repo_root').selectAll().execute() - const currHeads: Record = {} - for (const row of repoHeads) { - const head = CID.parse(row.root) - await migrateUser(row.did, head, null) - currHeads[row.did] = head - } -} - -export async function down(_db: Kysely): Promise {} - -class MigrationStorage extends MemoryBlockstore { - constructor(public blocks: BlockMap, public db: Kysely) { - super() - } - - async getBytes(cid: CID): Promise { - const got = this.blocks.get(cid) - if (got) return got - const fromDb = await this.db - .selectFrom('ipld_block') - .where('cid', '=', cid.toString()) - .selectAll() - .executeTakeFirst() - if (!fromDb) return null - this.blocks.set(cid, fromDb.content) - return fromDb.content - } - - async has(cid: CID): Promise { - const got = await this.getBytes(cid) - return !!got - } - - async getBlocks(cids: CID[]): Promise<{ blocks: BlockMap; missing: CID[] }> { - const got = this.blocks.getMany(cids) - if (got.missing.length === 0) return got - const fromDb = await this.db - .selectFrom('ipld_block') - .where( - 'cid', - 'in', - got.missing.map((c) => c.toString()), - ) - .selectAll() - .execute() - fromDb.forEach((row) => { - this.blocks.set(CID.parse(row.cid), row.content) - }) - return this.blocks.getMany(cids) - } -} diff --git a/packages/pds/src/db/migrations/20230202T170426672Z-user-partitioned-cids.ts b/packages/pds/src/db/migrations/20230202T170426672Z-user-partitioned-cids.ts deleted file mode 100644 index 5f37a46ef3a..00000000000 --- a/packages/pds/src/db/migrations/20230202T170426672Z-user-partitioned-cids.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Kysely, sql } from 'kysely' -import { Dialect } from '..' - -export async function up(db: Kysely, dialect: Dialect): Promise { - const binaryDatatype = dialect === 'sqlite' ? 'blob' : sql`bytea` - - await db.schema - .createTable('ipld_block_temp') - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('size', 'integer', (col) => col.notNull()) - .addColumn('content', binaryDatatype, (col) => col.notNull()) - .addPrimaryKeyConstraint('ipld_block_with_creator_pkey', ['creator', 'cid']) - .execute() - - await db - .insertInto('ipld_block_temp') - .columns(['cid', 'creator', 'size', 'content']) - .expression((exp) => - exp - .selectFrom('ipld_block') - .innerJoin( - 'ipld_block_creator', - 'ipld_block_creator.cid', - 'ipld_block.cid', - ) - .select([ - 'ipld_block.cid', - 'ipld_block_creator.did', - 'ipld_block.size', - 'ipld_block.content', - ]), - ) - .execute() - - await db.schema.dropTable('ipld_block').execute() - await db.schema.dropTable('ipld_block_creator').execute() - await db.schema.alterTable('ipld_block_temp').renameTo('ipld_block').execute() -} - -export async function down(db: Kysely, dialect: Dialect): Promise { - const binaryDatatype = dialect === 'sqlite' ? 'blob' : sql`bytea` - - await db.schema - .createTable('ipld_block_creator') - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('did', 'varchar', (col) => col.notNull()) - .addPrimaryKeyConstraint(`ipld_block_creator_pkey`, ['cid', 'did']) - .execute() - await db - .insertInto('ipld_block_creator') - .expression((exp) => - exp - .selectFrom('ipld_block') - .select(['ipld_block.cid as cid', 'ipld_block.creator as did']), - ) - .execute() - - await db.schema - .createTable('ipld_block_temp') - .addColumn('cid', 'varchar', (col) => col.primaryKey()) - .addColumn('size', 'integer', (col) => col.notNull()) - .addColumn('content', binaryDatatype, (col) => col.notNull()) - .execute() - - await db - .insertInto('ipld_block_temp') - .expression((exp) => - exp - .selectFrom('ipld_block') - .select([ - 'ipld_block.cid as cid', - 'ipld_block.size as size', - 'ipld_block.content as content', - ]) - .distinct(), - ) - .execute() - - await db.schema.dropTable('ipld_block').execute() - await db.schema.alterTable('ipld_block_temp').renameTo('ipld_block').execute() -} diff --git a/packages/pds/src/db/migrations/20230202T170435937Z-delete-account-token.ts b/packages/pds/src/db/migrations/20230202T170435937Z-delete-account-token.ts deleted file mode 100644 index c517cc27c3f..00000000000 --- a/packages/pds/src/db/migrations/20230202T170435937Z-delete-account-token.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Kysely } from 'kysely' - -const deleteTokenTable = 'delete_account_token' - -export async function up(db: Kysely): Promise { - await db.schema - .createTable(deleteTokenTable) - .addColumn('did', 'varchar', (col) => col.primaryKey()) - .addColumn('token', 'varchar', (col) => col.notNull()) - .addColumn('requestedAt', 'varchar', (col) => col.notNull()) - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropTable(deleteTokenTable).execute() -} diff --git a/packages/pds/src/db/migrations/20230202T172831900Z-moderation-subject-blob.ts b/packages/pds/src/db/migrations/20230202T172831900Z-moderation-subject-blob.ts deleted file mode 100644 index 80bddefdd09..00000000000 --- a/packages/pds/src/db/migrations/20230202T172831900Z-moderation-subject-blob.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Kysely } from 'kysely' -import { Dialect } from '..' - -export async function up(db: Kysely, dialect: Dialect): Promise { - // Track relevant subject blobs on action - await db.schema - .createTable('moderation_action_subject_blob') - .addColumn('actionId', 'integer', (col) => col.notNull()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('recordUri', 'varchar', (col) => col.notNull()) - .addForeignKeyConstraint( - 'moderation_action_subject_blob_action_id_fkey', - ['actionId'], - 'moderation_action', - ['id'], - ) - .addForeignKeyConstraint( - 'moderation_action_subject_blob_repo_blob_fkey', - ['cid', 'recordUri'], - 'repo_blob', - ['cid', 'recordUri'], - ) - .addPrimaryKeyConstraint('moderation_action_subject_blob_pkey', [ - 'actionId', - 'cid', - 'recordUri', - ]) - .execute() - // Blob takedowns - await db.schema - .alterTable('repo_blob') - .addColumn('takedownId', 'integer') - .execute() - if (dialect !== 'sqlite') { - // Would have to recreate table in sqlite to add these constraints - await db.schema - .alterTable('repo_blob') - .addForeignKeyConstraint( - 'repo_blob_takedown_id_fkey', - ['takedownId'], - 'moderation_action', - ['id'], - ) - .execute() - } -} - -export async function down( - db: Kysely, - dialect: Dialect, -): Promise { - await db.schema.dropTable('moderation_action_subject_blob').execute() - if (dialect !== 'sqlite') { - await db.schema - .alterTable('repo_blob') - .dropConstraint('repo_blob_takedown_id_fkey') - .execute() - } - await db.schema.alterTable('repo_blob').dropColumn('takedownId').execute() -} diff --git a/packages/pds/src/db/migrations/20230202T213952826Z-repo-seq.ts b/packages/pds/src/db/migrations/20230202T213952826Z-repo-seq.ts deleted file mode 100644 index 7bf67fc4ffb..00000000000 --- a/packages/pds/src/db/migrations/20230202T213952826Z-repo-seq.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Kysely } from 'kysely' -import { Dialect } from '..' - -const repoSeqTable = 'repo_seq' -const repoSeqDidIndex = 'repo_seq_did_index' -const repoSeqCommitIndex = 'repo_seq_commit_index' - -export async function up(db: Kysely, dialect: Dialect): Promise { - let builder = db.schema.createTable(repoSeqTable) - if (dialect === 'pg') { - builder = builder.addColumn('seq', 'serial', (col) => col.primaryKey()) - } else { - builder = builder.addColumn('seq', 'integer', (col) => - col.autoIncrement().primaryKey(), - ) - } - await builder - .addColumn('did', 'varchar', (col) => col.notNull()) - .addColumn('commit', 'varchar', (col) => col.notNull()) - .addColumn('eventType', 'varchar', (col) => col.notNull()) - .addColumn('sequencedAt', 'varchar', (col) => col.notNull()) - .execute() - - await db.schema - .createIndex(repoSeqDidIndex) - .on(repoSeqTable) - .column('did') - .execute() - - await db.schema - .createIndex(repoSeqCommitIndex) - .on(repoSeqTable) - .column('commit') - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropIndex(repoSeqCommitIndex).execute() - await db.schema.dropIndex(repoSeqDidIndex).execute() - await db.schema.dropTable(repoSeqTable).execute() -} diff --git a/packages/pds/src/db/migrations/20230208T081544325Z-post-hydrate-indices.ts b/packages/pds/src/db/migrations/20230208T081544325Z-post-hydrate-indices.ts deleted file mode 100644 index 4e57a4fb9af..00000000000 --- a/packages/pds/src/db/migrations/20230208T081544325Z-post-hydrate-indices.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - // for, eg, "upvoteCount" on posts in feed views - await db.schema - .createIndex('vote_subject_direction_idx') - .on('vote') - .columns(['subject', 'direction']) - .execute() - - // for, eg, "repostCount" on posts in feed views - await db.schema - .createIndex('repost_subject_idx') - .on('repost') - .column('subject') - .execute() - - // for, eg, "replyCount" on posts in feed views - await db.schema - .createIndex('post_replyparent_idx') - .on('post') - .column('replyParent') - .execute() - - // for, eg, "followersCount" on profile views - await db.schema - .createIndex('follow_subjectdid_idx') - .on('follow') - .column('subjectDid') - .execute() - - // for, eg, "postsCount" on profile views - await db.schema - .createIndex('post_creator_idx') - .on('post') - .column('creator') - .execute() - - // for, eg, profile views - await db.schema - .createIndex('profile_creator_idx') - .on('profile') - .column('creator') - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropIndex('vote_subject_direction_idx').execute() - await db.schema.dropIndex('repost_subject_idx').execute() - await db.schema.dropIndex('post_replyparent_idx').execute() - await db.schema.dropIndex('follow_subjectdid_idx').execute() - await db.schema.dropIndex('post_creator_idx').execute() - await db.schema.dropIndex('profile_creator_idx').execute() -} diff --git a/packages/pds/src/db/migrations/20230208T222001557Z-user-table-did-pkey.ts b/packages/pds/src/db/migrations/20230208T222001557Z-user-table-did-pkey.ts deleted file mode 100644 index 3de1fd0aefd..00000000000 --- a/packages/pds/src/db/migrations/20230208T222001557Z-user-table-did-pkey.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { Kysely, sql } from 'kysely' - -export async function up(db: Kysely): Promise { - // create switch user -> user_account with did primaryKey - await db.schema - .createTable('user_account') - .addColumn('did', 'varchar', (col) => col.primaryKey()) - .addColumn('email', 'varchar', (col) => col.notNull().unique()) - .addColumn('passwordScrypt', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addColumn('passwordResetToken', 'varchar') - .addColumn('passwordResetGrantedAt', 'varchar') - .execute() - await db - .insertInto('user_account') - .columns([ - 'did', - 'email', - 'passwordScrypt', - 'createdAt', - 'passwordResetToken', - 'passwordResetGrantedAt', - ]) - .expression((exp) => - exp - .selectFrom('user') - .innerJoin('did_handle', 'did_handle.handle', 'user.handle') - .select([ - 'did_handle.did', - 'user.email', - 'user.password', - 'user.createdAt', - 'user.passwordResetToken', - 'user.passwordResetGrantedAt', - ]), - ) - .execute() - - // add indices - await db.schema - .createIndex(`user_account_email_lower_idx`) - .unique() - .on('user_account') - .expression(sql`lower("email")`) - .execute() - await db.schema - .createIndex('user_account_password_reset_token_idx') - .unique() - .on('user_account') - .column('passwordResetToken') - .execute() - - // move notifsLastSeen to a new user_state table - await db.schema - .createTable('user_state') - .addColumn('did', 'varchar', (col) => col.primaryKey()) - .addColumn('lastSeenNotifs', 'varchar', (col) => col.notNull()) - .execute() - await db - .insertInto('user_state') - .columns(['did', 'lastSeenNotifs']) - .expression((exp) => - exp - .selectFrom('user') - .innerJoin('did_handle', 'did_handle.handle', 'user.handle') - .select(['did_handle.did', 'user.lastSeenNotifs']), - ) - .execute() - - // drop old tables & indices - await db.schema.dropIndex('user_email_lower_idx').execute() - await db.schema.dropIndex('user_password_reset_token_idx').execute() - await db.schema.dropTable('user').execute() -} - -export async function down(db: Kysely): Promise { - await db.schema - .createTable('user') - .addColumn('handle', 'varchar', (col) => col.primaryKey()) - .addColumn('email', 'varchar', (col) => col.notNull().unique()) - .addColumn('password', 'varchar', (col) => col.notNull()) - .addColumn('lastSeenNotifs', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addColumn('passwordResetToken', 'varchar') - .addColumn('passwordResetGrantedAt', 'varchar') - .execute() - - await db - .insertInto('user') - .columns([ - 'handle', - 'email', - 'password', - 'lastSeenNotifs', - 'createdAt', - 'passwordResetToken', - 'passwordResetGrantedAt', - ]) - .expression((exp) => - exp - .selectFrom('user_account') - .innerJoin('did_handle', 'did_handle.did', 'user_account.did') - .innerJoin('user_state', 'user_state.did', 'did_handle.did') - .select([ - 'did_handle.handle', - 'user_account.email', - 'user_account.passwordScrypt', - 'user_state.lastSeenNotifs', - 'user_account.createdAt', - 'user_account.passwordResetToken', - 'user_account.passwordResetGrantedAt', - ]), - ) - .execute() - - await db.schema - .createIndex(`user_email_lower_idx`) - .unique() - .on('user') - .expression(sql`lower("email")`) - .execute() - await db.schema - .createIndex('user_password_reset_token_idx') - .unique() - .on('user') - .column('passwordResetToken') - .execute() - - await db.schema.dropTable('user_state').execute() - await db.schema.dropIndex('user_account_email_lower_idx').execute() - await db.schema.dropIndex('user_account_password_reset_token_idx').execute() - await db.schema.dropTable('user_account').execute() -} diff --git a/packages/pds/src/db/migrations/20230210T210132396Z-post-hierarchy.ts b/packages/pds/src/db/migrations/20230210T210132396Z-post-hierarchy.ts deleted file mode 100644 index 894cb4db80c..00000000000 --- a/packages/pds/src/db/migrations/20230210T210132396Z-post-hierarchy.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Kysely, sql } from 'kysely' - -export async function up( - db: Kysely<{ post: Post; post_hierarchy: PostHierarchy }>, -): Promise { - await db.schema - .createTable('post_hierarchy') - .addColumn('uri', 'varchar', (col) => col.notNull()) - .addColumn('ancestorUri', 'varchar', (col) => col.notNull()) - .addColumn('depth', 'integer', (col) => col.notNull()) - .addPrimaryKeyConstraint('post_hierarchy_pkey', ['uri', 'ancestorUri']) - .execute() - - // Supports fetching all children for a post - await db.schema - .createIndex('post_hierarchy_ancestoruri_idx') - .on('post_hierarchy') - .column('ancestorUri') - .execute() - - const postHierarchyQb = db - .withRecursive('hierarchy(uri, ancestorUri, depth)', (cte) => { - return cte - .selectFrom('post') - .select([ - 'post.uri as uri', - 'post.uri as ancestorUri', - sql`0`.as('depth'), - ]) - .unionAll( - cte - .selectFrom('post') - .innerJoin('hierarchy', 'hierarchy.ancestorUri', 'post.uri') - .where('post.replyParent', 'is not', null) - .select([ - 'hierarchy.uri as uri', - sql`post."replyParent"`.as('ancestorUri'), - sql`hierarchy.depth + 1`.as('depth'), - ]), - ) - }) - .selectFrom('hierarchy') - - await db - .insertInto('post_hierarchy') - .columns(['uri', 'ancestorUri', 'depth']) - .expression(postHierarchyQb.select(['uri', 'ancestorUri', 'depth'])) - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropIndex('post_hierarchy_ancestoruri_idx').execute() - await db.schema.dropTable('post_hierarchy').execute() -} - -type Post = { - uri: string - replyParent: string | null -} - -type PostHierarchy = { - uri: string - ancestorUri: string - depth: number -} diff --git a/packages/pds/src/db/migrations/20230214T172233550Z-embed-records.ts b/packages/pds/src/db/migrations/20230214T172233550Z-embed-records.ts deleted file mode 100644 index 0d2c6a7afdd..00000000000 --- a/packages/pds/src/db/migrations/20230214T172233550Z-embed-records.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .createTable('post_embed_record') - .addColumn('postUri', 'varchar', (col) => col.notNull()) - .addColumn('embedUri', 'varchar', (col) => col.notNull()) - .addColumn('embedCid', 'varchar', (col) => col.notNull()) - .addPrimaryKeyConstraint('post_embed_record_pkey', ['postUri', 'embedUri']) - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropTable('post_embed_record').execute() -} diff --git a/packages/pds/src/db/migrations/20230301T222603402Z-repo-ops.ts b/packages/pds/src/db/migrations/20230301T222603402Z-repo-ops.ts deleted file mode 100644 index 5a4bd6820b6..00000000000 --- a/packages/pds/src/db/migrations/20230301T222603402Z-repo-ops.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .createTable('repo_op') - .addColumn('did', 'text', (col) => col.notNull()) - .addColumn('commit', 'text', (col) => col.notNull()) - .addColumn('action', 'text', (col) => col.notNull()) - .addColumn('path', 'text', (col) => col.notNull()) - .addColumn('cid', 'text') - .addPrimaryKeyConstraint('repo_op_pkey', ['did', 'commit', 'path']) - .execute() - - await db.deleteFrom('repo_seq').execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropTable('repo_op').execute() -} diff --git a/packages/pds/src/db/migrations/20230304T193548198Z-pagination-indices.ts b/packages/pds/src/db/migrations/20230304T193548198Z-pagination-indices.ts deleted file mode 100644 index 21278b14f59..00000000000 --- a/packages/pds/src/db/migrations/20230304T193548198Z-pagination-indices.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .createIndex('post_order_by_idx') - .on('post') - .columns(['indexedAt', 'cid']) - .execute() - - await db.schema - .createIndex('repost_order_by_idx') - .on('repost') - .columns(['indexedAt', 'cid']) - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropIndex('repost_order_by_idx').execute() - await db.schema.dropIndex('post_order_by_idx').execute() -} diff --git a/packages/pds/src/db/migrations/20230308T234640077Z-record-indexes.ts b/packages/pds/src/db/migrations/20230308T234640077Z-record-indexes.ts deleted file mode 100644 index 12e820f6d5e..00000000000 --- a/packages/pds/src/db/migrations/20230308T234640077Z-record-indexes.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .createIndex('record_cid_index') - .on('record') - .columns(['did', 'cid']) - .execute() - - await db.schema - .createIndex('record_collection_index') - .on('record') - .columns(['did', 'collection']) - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropIndex('record_cid_index').execute() - await db.schema.dropIndex('record_collection_index').execute() -} diff --git a/packages/pds/src/db/migrations/20230309T012947663Z-app-migration.ts b/packages/pds/src/db/migrations/20230309T012947663Z-app-migration.ts deleted file mode 100644 index 229c91ffe25..00000000000 --- a/packages/pds/src/db/migrations/20230309T012947663Z-app-migration.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .createTable('app_migration') - .addColumn('id', 'varchar', (col) => col.notNull().primaryKey()) - .addColumn('success', 'int2', (col) => col.notNull().defaultTo(0)) - .addColumn('completedAt', 'varchar') - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropTable('app_migration').execute() -} diff --git a/packages/pds/src/db/migrations/20230310T205728933Z-subscription-init.ts b/packages/pds/src/db/migrations/20230310T205728933Z-subscription-init.ts deleted file mode 100644 index 1c03ccb798d..00000000000 --- a/packages/pds/src/db/migrations/20230310T205728933Z-subscription-init.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .createTable('subscription') - .addColumn('service', 'varchar', (col) => col.notNull()) - .addColumn('method', 'varchar', (col) => col.notNull()) - .addColumn('state', 'varchar', (col) => col.notNull()) - .addPrimaryKeyConstraint('subscription_pkey', ['service', 'method']) - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropTable('subscription').execute() -} diff --git a/packages/pds/src/db/migrations/20230313T232322844Z-blob-creator.ts b/packages/pds/src/db/migrations/20230313T232322844Z-blob-creator.ts deleted file mode 100644 index 117a72108c7..00000000000 --- a/packages/pds/src/db/migrations/20230313T232322844Z-blob-creator.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .createTable('blob_new') - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('mimeType', 'varchar', (col) => col.notNull()) - .addColumn('size', 'integer', (col) => col.notNull()) - .addColumn('tempKey', 'varchar') - .addColumn('width', 'integer') - .addColumn('height', 'integer') - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addPrimaryKeyConstraint('blob_creator_pkey', ['creator', 'cid']) - .execute() - - await db - .insertInto('blob_new') - .columns([ - 'creator', - 'cid', - 'mimeType', - 'size', - 'tempKey', - 'width', - 'height', - 'createdAt', - ]) - .expression((exp) => - exp - .selectFrom('blob') - .innerJoin('repo_blob', 'repo_blob.cid', 'blob.cid') - .select([ - 'repo_blob.did', - 'blob.cid', - 'blob.mimeType', - 'blob.size', - 'blob.tempKey', - 'blob.width', - 'blob.height', - 'blob.createdAt', - ]) - // kinda silly, but we need a WHERE clause so that the ON CONFLICT parses correctly - .where('repo_blob.did', 'is not', null), - ) - .onConflict((oc) => oc.doNothing()) - .execute() - - await db.schema.dropTable('blob').execute() - await db.schema.alterTable('blob_new').renameTo('blob').execute() -} - -export async function down(db: Kysely): Promise { - await db.schema - .createTable('blob_new') - .addColumn('cid', 'varchar', (col) => col.primaryKey()) - .addColumn('mimeType', 'varchar', (col) => col.notNull()) - .addColumn('size', 'integer', (col) => col.notNull()) - .addColumn('tempKey', 'varchar') - .addColumn('width', 'integer') - .addColumn('height', 'integer') - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .execute() - - await db - .insertInto('blob_new') - .columns([ - 'cid', - 'mimeType', - 'size', - 'tempKey', - 'width', - 'height', - 'createdAt', - ]) - .expression((exp) => - exp - .selectFrom('blob') - .select([ - 'blob.cid', - 'blob.mimeType', - 'blob.size', - 'blob.tempKey', - 'blob.width', - 'blob.height', - 'blob.createdAt', - ]) - // kinda silly, but we need a WHERE clause so that the ON CONFLICT parses correctly - .where('cid', 'is not', null), - ) - .onConflict((oc) => oc.doNothing()) - .execute() - - await db.schema.dropTable('blob').execute() - await db.schema.alterTable('blob_new').renameTo('blob').execute() -} diff --git a/packages/pds/src/db/migrations/20230314T023842127Z-refresh-grace-period.ts b/packages/pds/src/db/migrations/20230314T023842127Z-refresh-grace-period.ts deleted file mode 100644 index f7755aea7fd..00000000000 --- a/packages/pds/src/db/migrations/20230314T023842127Z-refresh-grace-period.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .alterTable('refresh_token') - .addColumn('nextId', 'varchar') - .execute() - await db.schema // Aids in refresh token cleanup - .createIndex('refresh_token_did_idx') - .on('refresh_token') - .column('did') - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropIndex('refresh_token_did_idx').execute() - await db.schema.alterTable('refresh_token').dropColumn('nextId').execute() -} diff --git a/packages/pds/src/db/migrations/20230323T162732466Z-remove-scenes.ts b/packages/pds/src/db/migrations/20230323T162732466Z-remove-scenes.ts deleted file mode 100644 index ea2ac910754..00000000000 --- a/packages/pds/src/db/migrations/20230323T162732466Z-remove-scenes.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema.dropTable('scene').execute() - await db.schema.dropTable('trend').execute() - await db.schema.dropTable('scene_member_count').execute() - await db.schema.dropTable('scene_votes_on_post').execute() -} - -export async function down(db: Kysely): Promise { - await db.schema - .createTable('scene') - .addColumn('handle', 'varchar', (col) => col.primaryKey()) - .addColumn('owner', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .execute() - - await db.schema - .createTable('trend') - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('subject', 'varchar', (col) => col.notNull()) - .addColumn('subjectCid', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .execute() - - await db.schema - .createTable('scene_member_count') - .addColumn('did', 'varchar', (col) => col.primaryKey()) - .addColumn('count', 'integer', (col) => col.notNull()) - .execute() - await db.schema - .createTable('scene_votes_on_post') - .addColumn('did', 'varchar', (col) => col.notNull()) - .addColumn('subject', 'varchar', (col) => col.notNull()) - .addColumn('count', 'integer', (col) => col.notNull()) - .addColumn('postedTrending', 'int2', (col) => col.notNull()) - .addPrimaryKeyConstraint(`scene_votes_on_post_pkey`, ['did', 'subject']) - .execute() -} diff --git a/packages/pds/src/db/migrations/20230328T214311000Z-remove-declarations-assertions-confirmations.ts b/packages/pds/src/db/migrations/20230328T214311000Z-remove-declarations-assertions-confirmations.ts deleted file mode 100644 index 094bd68d39f..00000000000 --- a/packages/pds/src/db/migrations/20230328T214311000Z-remove-declarations-assertions-confirmations.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db - .deleteFrom('duplicate_record') - .where( - 'duplicateOf', - 'in', - db.selectFrom('assertion').select('assertion.uri'), - ) - .orWhere( - 'duplicateOf', - 'in', - db.selectFrom('assertion').select('assertion.confirmUri'), - ) - .execute() - await db.schema.dropTable('assertion').execute() - await db.schema - .alterTable('follow') - .dropColumn('subjectDeclarationCid') - .execute() - await db.schema - .alterTable('did_handle') - .dropColumn('declarationCid') - .execute() - await db.schema.alterTable('did_handle').dropColumn('actorType').execute() -} - -export async function down(db: Kysely): Promise { - await db.schema - .alterTable('did_handle') - .addColumn('actorType', 'varchar') - .execute() - await db.schema - .alterTable('did_handle') - .addColumn('declarationCid', 'varchar') - .execute() - await db.schema - .alterTable('follow') - .addColumn('subjectDeclarationCid', 'varchar', (col) => - col.notNull().defaultTo(''), - ) - .execute() - await db.schema - .createTable('assertion') - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('assertion', 'varchar', (col) => col.notNull()) - .addColumn('subjectDid', 'varchar', (col) => col.notNull()) - .addColumn('subjectDeclarationCid', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .addColumn('confirmUri', 'varchar') - .addColumn('confirmCid', 'varchar') - .addColumn('confirmCreated', 'varchar') - .addColumn('confirmIndexed', 'varchar') - .addUniqueConstraint('assertion_unique_subject', [ - 'creator', - 'subjectDid', - 'assertion', - ]) - .execute() -} - -type Schema = { - assertion: Assertion - duplicate_record: DuplicateRecord -} - -type Assertion = { - uri: string - confirmUri: string | null -} - -type DuplicateRecord = { - uri: string - duplicateOf: string -} diff --git a/packages/pds/src/db/migrations/20230328T214311001Z-votes-to-likes.ts b/packages/pds/src/db/migrations/20230328T214311001Z-votes-to-likes.ts deleted file mode 100644 index 026b710ad83..00000000000 --- a/packages/pds/src/db/migrations/20230328T214311001Z-votes-to-likes.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { Kysely, sql } from 'kysely' - -export async function up(db: Kysely): Promise { - // Nix downvotes from index - const downvotesQb = db.selectFrom('vote').where('direction', '=', 'down') - await db - .deleteFrom('duplicate_record') - .where('duplicateOf', 'in', downvotesQb.select('vote.uri')) - .execute() - await db.schema - .createTable('like') - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('subject', 'varchar', (col) => col.notNull()) - .addColumn('subjectCid', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - // Aids in index uniqueness plus post like counts - .addUniqueConstraint('like_unique_subject', ['subject', 'creator']) - .execute() - await db - .insertInto('like') - .columns([ - 'uri', - 'cid', - 'creator', - 'subject', - 'subjectCid', - 'createdAt', - 'indexedAt', - ]) - .expression((exp) => - exp - .selectFrom('vote') - .where('direction', '=', 'up') - .select([ - 'uri', - 'cid', - 'creator', - 'subject', - 'subjectCid', - 'createdAt', - 'indexedAt', - ]), - ) - .execute() - const missing = await db - .selectFrom('vote') - .select(sql`count(*)`.as('count')) - .where('direction', '=', 'up') - .whereNotExists( - db - .selectFrom('like') - .selectAll() - .whereRef('uri', '=', db.dynamic.ref('vote.uri')), - ) - .executeTakeFirstOrThrow() - if (missing.count !== 0) { - throw new Error( - `Likes were not migrated properly from votes: ${missing.count} likes missing.`, - ) - } - await db.schema.dropTable('vote').execute() -} - -export async function down(db: Kysely): Promise { - await db.schema - .createTable('vote') - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('direction', 'varchar', (col) => col.notNull()) - .addColumn('subject', 'varchar', (col) => col.notNull()) - .addColumn('subjectCid', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .addUniqueConstraint('vote_unique_subject', ['creator', 'subject']) - .execute() - await db - .insertInto('vote') - .columns([ - 'uri', - 'cid', - 'creator', - 'direction', - 'subject', - 'subjectCid', - 'createdAt', - 'indexedAt', - ]) - .expression((exp) => - exp - .selectFrom('like') - .select([ - 'uri', - 'cid', - 'creator', - sql`${'up'}`.as('direction'), - 'subject', - 'subjectCid', - 'createdAt', - 'indexedAt', - ]), - ) - .execute() - await db.schema.dropTable('like').execute() - await db.schema - .createIndex('vote_subject_direction_idx') - .on('vote') - .columns(['subject', 'direction']) - .execute() -} - -type Schema = { - vote: Vote - like: Like - duplicate_record: DuplicateRecord -} - -type Vote = { - uri: string - direction: 'up' | 'down' - [k: string]: unknown -} - -type Like = { - uri: string - [k: string]: unknown -} - -type DuplicateRecord = { - uri: string - duplicateOf: string -} diff --git a/packages/pds/src/db/migrations/20230328T214311002Z-remove-post-entities.ts b/packages/pds/src/db/migrations/20230328T214311002Z-remove-post-entities.ts deleted file mode 100644 index 480922aaf0f..00000000000 --- a/packages/pds/src/db/migrations/20230328T214311002Z-remove-post-entities.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema.dropTable('post_entity').execute() -} - -export async function down(db: Kysely): Promise { - await db.schema - .createTable('post_entity') - .addColumn('postUri', 'varchar', (col) => col.notNull()) - .addColumn('startIndex', 'integer', (col) => col.notNull()) - .addColumn('endIndex', 'integer', (col) => col.notNull()) - .addColumn('type', 'varchar', (col) => col.notNull()) - .addColumn('value', 'varchar', (col) => col.notNull()) - .execute() -} diff --git a/packages/pds/src/db/migrations/20230328T214311003Z-backlinks.ts b/packages/pds/src/db/migrations/20230328T214311003Z-backlinks.ts deleted file mode 100644 index eff4bb35381..00000000000 --- a/packages/pds/src/db/migrations/20230328T214311003Z-backlinks.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { Kysely, sql } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .createTable('backlink') - .addColumn('uri', 'varchar', (col) => col.notNull()) - .addColumn('path', 'varchar', (col) => col.notNull()) - .addColumn('linkToUri', 'varchar') - .addColumn('linkToDid', 'varchar') - .addPrimaryKeyConstraint('backlinks_pkey', ['uri', 'path']) - .addCheckConstraint( - 'backlink_link_to_chk', - // Exactly one of linkToUri or linkToDid should be set - sql`("linkToUri" is null and "linkToDid" is not null) or ("linkToUri" is not null and "linkToDid" is null)`, - ) - .execute() - - // Seed backlinks - - // Existing likes and their dupes - await db - .insertInto('backlink') - .columns(['uri', 'linkToUri', 'path']) - .expression((exp) => - exp - .selectFrom('like') - .select(['like.uri', 'like.subject', sql`${'subject.uri'}`.as('path')]), - ) - .execute() - await db - .insertInto('backlink') - .columns(['uri', 'linkToUri', 'path']) - .expression((exp) => - exp - .selectFrom('duplicate_record') - .innerJoin('like', 'like.uri', 'duplicate_record.duplicateOf') - .select([ - 'duplicate_record.uri', - 'like.subject', - sql`${'subject.uri'}`.as('path'), - ]), - ) - .execute() - - // Existing reposts and their dupes - await db - .insertInto('backlink') - .columns(['uri', 'linkToUri', 'path']) - .expression((exp) => - exp - .selectFrom('repost') - .select([ - 'repost.uri', - 'repost.subject', - sql`${'subject.uri'}`.as('path'), - ]), - ) - .execute() - await db - .insertInto('backlink') - .columns(['uri', 'linkToUri', 'path']) - .expression((exp) => - exp - .selectFrom('duplicate_record') - .innerJoin('repost', 'repost.uri', 'duplicate_record.duplicateOf') - .select([ - 'duplicate_record.uri', - 'repost.subject', - sql`${'subject.uri'}`.as('path'), - ]), - ) - .execute() - - // Existing follows and their dupes - await db - .insertInto('backlink') - .columns(['uri', 'linkToDid', 'path']) - .expression((exp) => - exp - .selectFrom('follow') - .select([ - 'follow.uri', - 'follow.subjectDid', - sql`${'subject'}`.as('path'), - ]), - ) - .execute() - await db - .insertInto('backlink') - .columns(['uri', 'linkToDid', 'path']) - .expression((exp) => - exp - .selectFrom('duplicate_record') - .innerJoin('follow', 'follow.uri', 'duplicate_record.duplicateOf') - .select([ - 'duplicate_record.uri', - 'follow.subjectDid', - sql`${'subject'}`.as('path'), - ]), - ) - .execute() - - await db.schema - .createIndex('backlink_path_to_uri_idx') - .on('backlink') - .columns(['path', 'linkToUri']) - .execute() - await db.schema - .createIndex('backlink_path_to_did_idx') - .on('backlink') - .columns(['path', 'linkToDid']) - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropTable('backlink').execute() -} - -type Schema = { - backlink: Backlink - like: Like - follow: Follow - repost: Repost - duplicate_record: DuplicateRecord -} - -interface Backlink { - uri: string - path: string - linkToUri: string | null - linkToDid: string | null -} - -interface Like { - uri: string - subject: string -} - -interface Follow { - uri: string - subjectDid: string -} - -interface Repost { - uri: string - subject: string -} - -interface DuplicateRecord { - uri: string - duplicateOf: string -} diff --git a/packages/pds/src/db/migrations/20230328T214311004Z-profile-display-name-empty.ts b/packages/pds/src/db/migrations/20230328T214311004Z-profile-display-name-empty.ts deleted file mode 100644 index f1c78ce09b8..00000000000 --- a/packages/pds/src/db/migrations/20230328T214311004Z-profile-display-name-empty.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely, dialect): Promise { - if (dialect === 'pg') { - await db.schema - .alterTable('profile') - .alterColumn('displayName') - .dropNotNull() - .execute() - return - } - - // Sqlite version below: Need to recreate table due to sqlite limitations. - - // Drop old indices - await db.schema.dropIndex('profile_creator_idx').execute() - // Create table w/ change - await db.schema - .createTable('profile_temp') - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('displayName', 'varchar') // <-- change is here, making displayName nullable - .addColumn('description', 'varchar') - .addColumn('avatarCid', 'varchar') - .addColumn('bannerCid', 'varchar') - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .execute() - // Fill table - await db - .insertInto('profile_temp') - .columns(profileColumns) - .expression((exp) => exp.selectFrom('profile').select(profileColumns)) - .execute() - // Replace table by name - await db.schema.dropTable('profile').execute() - await db.schema.alterTable('profile_temp').renameTo('profile').execute() - // Recreate indices - await db.schema // Supports profile views - .createIndex('profile_creator_idx') - .on('profile') - .column('creator') - .execute() -} - -export async function down(db: Kysely, dialect): Promise { - if (dialect === 'pg') { - await db.schema - .alterTable('profile') - .alterColumn('displayName') - .setNotNull() - .execute() - return - } - - // Sqlite version below: Need to recreate table due to sqlite limitations. - - // Drop old indices - await db.schema.dropIndex('profile_creator_idx').execute() - // Create table w/ change - await db.schema - .createTable('profile_temp') - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('displayName', 'varchar', (col) => col.notNull()) // <-- change is here, making displayName non-nullable again - .addColumn('description', 'varchar') - .addColumn('avatarCid', 'varchar') - .addColumn('bannerCid', 'varchar') - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .execute() - // Fill table - await db - .insertInto('profile_temp') - .columns(profileColumns) - .expression((exp) => exp.selectFrom('profile').select(profileColumns)) - .execute() - // Replace table by name - await db.schema.dropTable('profile').execute() - await db.schema.alterTable('profile_temp').renameTo('profile').execute() - // Recreate indices - await db.schema - .createIndex('profile_creator_idx') - .on('profile') - .column('creator') - .execute() -} - -type Schema = { - profile: Profile - profile_temp: ProfileTemp -} - -type Profile = { - uri: string - cid: string - creator: string - displayName: string - description: string | null - avatarCid: string | null - bannerCid: string | null - indexedAt: string -} - -type ProfileTemp = Omit & { - displayName: string | null -} - -const profileColumns: (keyof Profile)[] = [ - 'uri', - 'cid', - 'creator', - 'displayName', - 'description', - 'avatarCid', - 'bannerCid', - 'indexedAt', -] diff --git a/packages/pds/src/db/migrations/20230328T214311005Z-rework-seq.ts b/packages/pds/src/db/migrations/20230328T214311005Z-rework-seq.ts deleted file mode 100644 index 144fa3a2075..00000000000 --- a/packages/pds/src/db/migrations/20230328T214311005Z-rework-seq.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { Kysely, sql } from 'kysely' -import { Dialect } from '..' - -const repoSeqTable = 'repo_seq' -const repoOpTable = 'repo_op' -const repoSeqDidIndex = 'repo_seq_did_index' -const repoSeqCommitIndex = 'repo_seq_commit_index' -const repoSeqEventTypeIndex = 'repo_seq_event_type_index' -const repoSeqSequencedAtIndex = 'repo_seq_sequenced_at_index' - -export async function up(db: Kysely, dialect: Dialect): Promise { - await db.schema.dropIndex(repoSeqCommitIndex).execute() - await db.schema.dropIndex(repoSeqDidIndex).execute() - await db.schema.dropTable(repoSeqTable).execute() - await db.schema.dropTable(repoOpTable).execute() - - let builder = db.schema.createTable(repoSeqTable) - if (dialect === 'pg') { - builder = builder - .addColumn('seq', 'bigserial', (col) => col.primaryKey()) - .addColumn('invalidatedBy', 'bigint') - } else { - builder = builder - .addColumn('seq', 'integer', (col) => col.autoIncrement().primaryKey()) - .addColumn('invalidatedBy', 'integer') - } - - const binaryDatatype = dialect === 'sqlite' ? sql`blob` : sql`bytea` - await builder - .addColumn('did', 'varchar', (col) => col.notNull()) - .addColumn('eventType', 'varchar', (col) => col.notNull()) - .addColumn('event', binaryDatatype, (col) => col.notNull()) - .addColumn('sequencedAt', 'varchar', (col) => col.notNull()) - .addForeignKeyConstraint( - 'invalidated_by_fkey', - // @ts-ignore - ['invalidatedBy'], - 'repo_seq', - ['seq'], - ) - .execute() - - // for filtering seqs based on did - await db.schema - .createIndex(repoSeqDidIndex) - .on(repoSeqTable) - .column('did') - .execute() - - // for filtering seqs based on event type - await db.schema - .createIndex(repoSeqEventTypeIndex) - .on(repoSeqTable) - .column('eventType') - .execute() - - // for entering into the seq stream at a particular time - await db.schema - .createIndex(repoSeqSequencedAtIndex) - .on(repoSeqTable) - .column('sequencedAt') - .execute() -} - -export async function down( - db: Kysely, - dialect: Dialect, -): Promise { - await db.schema.dropIndex(repoSeqSequencedAtIndex).execute() - await db.schema.dropIndex(repoSeqEventTypeIndex).execute() - await db.schema.dropIndex(repoSeqDidIndex).execute() - await db.schema.dropTable(repoSeqTable).execute() - - let builder = db.schema.createTable(repoSeqTable) - if (dialect === 'pg') { - builder = builder.addColumn('seq', 'serial', (col) => col.primaryKey()) - } else { - builder = builder.addColumn('seq', 'integer', (col) => - col.autoIncrement().primaryKey(), - ) - } - await builder - .addColumn('did', 'varchar', (col) => col.notNull()) - .addColumn('commit', 'varchar', (col) => col.notNull()) - .addColumn('eventType', 'varchar', (col) => col.notNull()) - .addColumn('sequencedAt', 'varchar', (col) => col.notNull()) - .execute() - - await db.schema - .createIndex(repoSeqDidIndex) - .on(repoSeqTable) - .column('did') - .execute() - - await db.schema - .createIndex(repoSeqCommitIndex) - .on(repoSeqTable) - .column('commit') - .execute() - - await db.schema - .createTable(repoOpTable) - .addColumn('did', 'text', (col) => col.notNull()) - .addColumn('commit', 'text', (col) => col.notNull()) - .addColumn('action', 'text', (col) => col.notNull()) - .addColumn('path', 'text', (col) => col.notNull()) - .addColumn('cid', 'text') - .addPrimaryKeyConstraint('repo_op_pkey', ['did', 'commit', 'path']) - .execute() -} diff --git a/packages/pds/src/db/migrations/20230406T185855842Z-feed-item-init.ts b/packages/pds/src/db/migrations/20230406T185855842Z-feed-item-init.ts deleted file mode 100644 index 0d26fddc22b..00000000000 --- a/packages/pds/src/db/migrations/20230406T185855842Z-feed-item-init.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { DynamicModule, Kysely, sql } from 'kysely' -import { Dialect } from '..' - -export async function up(db: Kysely): Promise { - await db.schema - .createTable('feed_item') - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('type', 'varchar', (col) => col.notNull()) - .addColumn('postUri', 'varchar', (col) => col.notNull()) - .addColumn('originatorDid', 'varchar', (col) => col.notNull()) - .addColumn('sortAt', 'varchar', (col) => col.notNull()) - .execute() - await db.schema - .createIndex('feed_item_originator_idx') - .on('feed_item') - .column('originatorDid') - .execute() - await db.schema - .createIndex('feed_item_cursor_idx') - .on('feed_item') - .columns(['sortAt', 'cid']) - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropTable('feed_item').execute() -} - -// This is intentionally not called here, but exists for documentation purposes. -// This query should be safe to be run any time to update the feed_item index. -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function getFeedItemMigrationSql(db: Kysely, dialect: Dialect) { - const { ref } = db.dynamic - const migrationQb = db - .insertInto('feed_item') - .columns(['type', 'uri', 'cid', 'postUri', 'originatorDid', 'sortAt']) - .expression((qb) => { - return qb - .selectFrom('post') - .select([ - sql`'post'`.as('type'), - 'uri', - 'cid', - 'uri as postUri', - 'creator as originatorDid', - min(dialect, ref('indexedAt'), ref('createdAt')).as('sortAt'), - ]) - .unionAll( - qb - .selectFrom('repost') - .innerJoin('post', 'post.uri', 'repost.subject') - .select([ - sql`'repost'`.as('type'), - 'repost.uri as uri', - 'repost.cid as cid', - 'post.uri as postUri', - 'repost.creator as originatorDid', - min(dialect, ref('repost.indexedAt'), ref('repost.createdAt')).as( - 'sortAt', - ), - ]), - ) - }) - .onConflict((oc) => oc.doNothing()) - return migrationQb.compile().sql -} - -type Schema = { - feed_item: FeedItem - post: Post - repost: Repost -} - -interface FeedItem { - uri: string - cid: string - type: 'post' | 'repost' - postUri: string - postAuthorDid: string - originatorDid: string - sortAt: string -} - -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 -} - -interface Repost { - uri: string - cid: string - creator: string - subject: string - subjectCid: string - createdAt: string - indexedAt: string -} - -function min(dialect: Dialect, refA: DbRef, refB: DbRef) { - if (dialect === 'pg') { - return sql`least(${refA}, ${refB})` - } else { - return sql`min(${refA}, ${refB})` - } -} - -type DbRef = ReturnType diff --git a/packages/pds/src/db/migrations/20230411T175730759Z-drop-message-queue.ts b/packages/pds/src/db/migrations/20230411T175730759Z-drop-message-queue.ts deleted file mode 100644 index 808d87355c9..00000000000 --- a/packages/pds/src/db/migrations/20230411T175730759Z-drop-message-queue.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Kysely } from 'kysely' -import { Dialect } from '..' - -const messageQueueTable = 'message_queue' -const messageQueueCursorTable = 'message_queue_cursor' - -export async function up(db: Kysely): Promise { - await db.schema.dropTable(messageQueueCursorTable).execute() - await db.schema.dropTable(messageQueueTable).execute() -} - -export async function down( - db: Kysely, - dialect: Dialect, -): Promise { - let mqBuilder = db.schema.createTable(messageQueueTable) - mqBuilder = - dialect === 'pg' - ? mqBuilder.addColumn('id', 'serial', (col) => col.primaryKey()) - : mqBuilder.addColumn('id', 'integer', (col) => - col.autoIncrement().primaryKey(), - ) - mqBuilder - .addColumn('message', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .execute() - await db.schema - .createTable(messageQueueCursorTable) - .addColumn('consumer', 'varchar', (col) => col.primaryKey()) - .addColumn('cursor', 'integer', (col) => col.notNull()) - .execute() -} diff --git a/packages/pds/src/db/migrations/20230411T180247652Z-labels.ts b/packages/pds/src/db/migrations/20230411T180247652Z-labels.ts deleted file mode 100644 index 86f3b47a721..00000000000 --- a/packages/pds/src/db/migrations/20230411T180247652Z-labels.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .createTable('label') - .addColumn('sourceDid', 'varchar', (col) => col.notNull()) - .addColumn('subjectUri', 'varchar', (col) => col.notNull()) - .addColumn('subjectCid', 'varchar') - .addColumn('value', 'varchar', (col) => col.notNull()) - .addColumn('negated', 'int2', (col) => col.notNull()) // @TODO convert to boolean in appview - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addPrimaryKeyConstraint('label_pkey', [ - 'sourceDid', - 'subjectUri', - 'subjectCid', - 'value', - ]) - .execute() - - await db.schema - .createIndex('label_subject_uri_index') - .on('label') - .column('subjectUri') - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropTable('label').execute() -} diff --git a/packages/pds/src/db/migrations/20230412T231807162Z-moderation-action-labels.ts b/packages/pds/src/db/migrations/20230412T231807162Z-moderation-action-labels.ts deleted file mode 100644 index 58f015a0c39..00000000000 --- a/packages/pds/src/db/migrations/20230412T231807162Z-moderation-action-labels.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Kysely } from 'kysely' - -const moderationActionTable = 'moderation_action' - -export async function up(db: Kysely): Promise { - await db.schema - .alterTable(moderationActionTable) - .addColumn('createLabelVals', 'varchar') - .execute() - - await db.schema - .alterTable(moderationActionTable) - .addColumn('negateLabelVals', 'varchar') - .execute() - - await db.schema.dropTable('label').execute() - - await db.schema - .createTable('label') - .addColumn('src', 'varchar', (col) => col.notNull()) - .addColumn('uri', 'varchar', (col) => col.notNull()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('val', 'varchar', (col) => col.notNull()) - .addColumn('neg', 'int2', (col) => col.notNull()) // @TODO convert to boolean in appview - .addColumn('cts', 'varchar', (col) => col.notNull()) - .addPrimaryKeyConstraint('label_pkey', ['src', 'uri', 'cid', 'val']) - .execute() - - await db.schema - .createIndex('label_uri_index') - .on('label') - .column('uri') - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema - .alterTable(moderationActionTable) - .dropColumn('createLabelVals') - .execute() - - await db.schema - .alterTable(moderationActionTable) - .dropColumn('negateLabelVals') - .execute() - - await db.schema.dropTable('label').execute() - - await db.schema - .createTable('label') - .addColumn('sourceDid', 'varchar', (col) => col.notNull()) - .addColumn('subjectUri', 'varchar', (col) => col.notNull()) - .addColumn('subjectCid', 'varchar') - .addColumn('value', 'varchar', (col) => col.notNull()) - .addColumn('negated', 'int2', (col) => col.notNull()) // @TODO convert to boolean in appview - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addPrimaryKeyConstraint('label_pkey', [ - 'sourceDid', - 'subjectUri', - 'subjectCid', - 'value', - ]) - .execute() - - await db.schema - .createIndex('label_subject_uri_index') - .on('label') - .column('subjectUri') - .execute() -} diff --git a/packages/pds/src/db/migrations/20230416T221236745Z-app-specific-passwords.ts b/packages/pds/src/db/migrations/20230416T221236745Z-app-specific-passwords.ts deleted file mode 100644 index f518188c844..00000000000 --- a/packages/pds/src/db/migrations/20230416T221236745Z-app-specific-passwords.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .alterTable('refresh_token') - .addColumn('appPasswordName', 'varchar') - .execute() - - await db.schema - .createTable('app_password') - .addColumn('did', 'varchar', (col) => col.notNull()) - .addColumn('name', 'varchar', (col) => col.notNull()) - .addColumn('passwordScrypt', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addPrimaryKeyConstraint('app_password_pkey', ['did', 'name']) - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropTable('app_password').execute() - - await db.schema - .alterTable('refresh_token') - .dropColumn('appPasswordName') - .execute() -} diff --git a/packages/pds/src/db/migrations/20230420T143821201Z-post-profile-aggs.ts b/packages/pds/src/db/migrations/20230420T143821201Z-post-profile-aggs.ts deleted file mode 100644 index a42346d9201..00000000000 --- a/packages/pds/src/db/migrations/20230420T143821201Z-post-profile-aggs.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { Generated, Kysely, sql } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .createTable('post_agg') - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('likeCount', 'bigint', (col) => col.notNull().defaultTo(0)) - .addColumn('replyCount', 'bigint', (col) => col.notNull().defaultTo(0)) - .addColumn('repostCount', 'bigint', (col) => col.notNull().defaultTo(0)) - .execute() - await db.schema - .createTable('profile_agg') - .addColumn('did', 'varchar', (col) => col.primaryKey()) - .addColumn('followersCount', 'bigint', (col) => col.notNull().defaultTo(0)) - .addColumn('followsCount', 'bigint', (col) => col.notNull().defaultTo(0)) - .addColumn('postsCount', 'bigint', (col) => col.notNull().defaultTo(0)) - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropTable('profile_agg').execute() - await db.schema.dropTable('post_agg').execute() -} - -// This is intentionally not called here, but exists for documentation purposes. -// This query should be safe to be run any time to update the feed_item index. - -// @NOTE these can only update records that do not have a "zero" count, so it's suitable for an initial -// run, but it's not suitable for a general refresh (which may need to update a count down to zero). - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function getAggMigrationSql(db: Kysely) { - const { ref } = db.dynamic - const excluded = (col: string) => ref(`excluded.${col}`) - - const likeCountQb = db - .insertInto('post_agg') - .columns(['uri', 'likeCount']) - .expression((exp) => - exp - .selectFrom('like') - .groupBy('like.subject') - .select(['like.subject as uri', countAll.as('likeCount')]), - ) - .onConflict((oc) => - oc - .column('uri') - .doUpdateSet({ likeCount: sql`${excluded('likeCount')}` }), - ) - - const replyCountQb = db - .insertInto('post_agg') - .columns(['uri', 'replyCount']) - .expression((exp) => - exp - .selectFrom('post') - .where('replyParent', 'is not', null) - .groupBy('post.replyParent') - .select(['post.replyParent as uri', countAll.as('replyCount')]), - ) - .onConflict((oc) => - oc - .column('uri') - .doUpdateSet({ replyCount: sql`${excluded('replyCount')}` }), - ) - - const repostCountQb = db - .insertInto('post_agg') - .columns(['uri', 'repostCount']) - .expression((exp) => - exp - .selectFrom('repost') - .groupBy('repost.subject') - .select(['repost.subject as uri', countAll.as('repostCount')]), - ) - .onConflict((oc) => - oc - .column('uri') - .doUpdateSet({ repostCount: sql`${excluded('repostCount')}` }), - ) - - const followersCountQb = db - .insertInto('profile_agg') - .columns(['did', 'followersCount']) - .expression((exp) => - exp - .selectFrom('follow') - .groupBy('follow.subjectDid') - .select(['follow.subjectDid as did', countAll.as('followersCount')]), - ) - .onConflict((oc) => - oc - .column('did') - .doUpdateSet({ followersCount: sql`${excluded('followersCount')}` }), - ) - - const followsCountQb = db - .insertInto('profile_agg') - .columns(['did', 'followsCount']) - .expression((exp) => - exp - .selectFrom('follow') - .groupBy('follow.creator') - .select(['follow.creator as did', countAll.as('followsCount')]), - ) - .onConflict((oc) => - oc - .column('did') - .doUpdateSet({ followsCount: sql`${excluded('followsCount')}` }), - ) - - const postsCountQb = db - .insertInto('profile_agg') - .columns(['did', 'postsCount']) - .expression((exp) => - exp - .selectFrom('post') - .groupBy('post.creator') - .select(['post.creator as did', countAll.as('postsCount')]), - ) - .onConflict((oc) => - oc - .column('did') - .doUpdateSet({ postsCount: sql`${excluded('postsCount')}` }), - ) - - return [ - likeCountQb.compile().sql, - replyCountQb.compile().sql, - repostCountQb.compile().sql, - followersCountQb.compile().sql, - followsCountQb.compile().sql, - postsCountQb.compile().sql, - ].join(';\n\n') -} - -const countAll = sql`count(*)` - -type Schema = { - post_agg: PostAgg - profile_agg: ProfileAgg - like: Like - follow: Follow - post: Post - repost: Repost -} - -interface PostAgg { - uri: string - likeCount: Generated - replyCount: Generated - repostCount: Generated -} - -interface ProfileAgg { - did: string - followersCount: Generated - followsCount: Generated - postsCount: Generated -} - -interface Like { - subject: string -} - -interface Follow { - creator: string - subjectDid: string -} - -interface Post { - creator: string - replyParent: string | null -} - -interface Repost { - subject: string -} diff --git a/packages/pds/src/db/migrations/20230427T194652255Z-notif-record-index.ts b/packages/pds/src/db/migrations/20230427T194652255Z-notif-record-index.ts deleted file mode 100644 index de4031253b2..00000000000 --- a/packages/pds/src/db/migrations/20230427T194652255Z-notif-record-index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - // Supports record deletion - await db.schema - .createIndex('user_notification_record_idx') - .on('user_notification') - .column('recordUri') - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropIndex('user_notification_record_idx').execute() -} diff --git a/packages/pds/src/db/migrations/20230428T195614638Z-actor-block-init.ts b/packages/pds/src/db/migrations/20230428T195614638Z-actor-block-init.ts deleted file mode 100644 index 1da2ac2595e..00000000000 --- a/packages/pds/src/db/migrations/20230428T195614638Z-actor-block-init.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .createTable('actor_block') - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('subjectDid', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .addUniqueConstraint('actor_block_unique_subject', [ - 'creator', - 'subjectDid', - ]) - .execute() - await db.schema - .createIndex('actor_block_subjectdid_idx') - .on('actor_block') - .column('subjectDid') - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropIndex('actor_block_subjectdid_idx').execute() - await db.schema.dropTable('actor_block').execute() -} diff --git a/packages/pds/src/db/migrations/20230508T193807762Z-acct-deletion-indexes.ts b/packages/pds/src/db/migrations/20230508T193807762Z-acct-deletion-indexes.ts deleted file mode 100644 index f1504abe611..00000000000 --- a/packages/pds/src/db/migrations/20230508T193807762Z-acct-deletion-indexes.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Kysely } from 'kysely' -import { Dialect } from '..' - -// Indexes to support efficient account deletion - -export async function up(db: Kysely, dialect: Dialect): Promise { - await db.schema // Also supports record deletes - .createIndex('duplicate_record_duplicate_of_idx') - .on('duplicate_record') - .column('duplicateOf') - .execute() - await db.schema - .createIndex('like_creator_idx') - .on('like') - .column('creator') - .execute() - await db.schema - .createIndex('user_notification_author_idx') - .on('user_notification') - .column('author') - .execute() - if (dialect !== 'sqlite') { - // We want to keep record of the moderations actions even when deleting the underlying repo_blob record. - await db.schema - .alterTable('moderation_action_subject_blob') - .dropConstraint('moderation_action_subject_blob_repo_blob_fkey') - .execute() - } -} - -export async function down( - db: Kysely, - dialect: Dialect, -): Promise { - if (dialect !== 'sqlite') { - await db.schema - .alterTable('moderation_action_subject_blob') - .addForeignKeyConstraint( - 'moderation_action_subject_blob_repo_blob_fkey', - ['cid', 'recordUri'], - 'repo_blob', - ['cid', 'recordUri'], - ) - .execute() - } - await db.schema.dropIndex('user_notification_author_idx').execute() - await db.schema.dropIndex('like_creator_idx').execute() - await db.schema.dropIndex('duplicate_record_duplicate_of_idx').execute() -} diff --git a/packages/pds/src/db/migrations/20230508T232711152Z-disable-account-invites.ts b/packages/pds/src/db/migrations/20230508T232711152Z-disable-account-invites.ts deleted file mode 100644 index 254c92eccc9..00000000000 --- a/packages/pds/src/db/migrations/20230508T232711152Z-disable-account-invites.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .alterTable('user_account') - .addColumn('invitesDisabled', 'int2', (col) => col.notNull().defaultTo(0)) - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema - .alterTable('user_account') - .dropColumn('invitesDisabled') - .execute() -} diff --git a/packages/pds/src/db/migrations/20230509T192324175Z-seq-invalidated.ts b/packages/pds/src/db/migrations/20230509T192324175Z-seq-invalidated.ts deleted file mode 100644 index abdfd025b22..00000000000 --- a/packages/pds/src/db/migrations/20230509T192324175Z-seq-invalidated.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Kysely, sql } from 'kysely' -import { Dialect } from '..' - -const repoSeqDidIndex = 'repo_seq_did_index' -const repoSeqEventTypeIndex = 'repo_seq_event_type_index' -const repoSeqSequencedAtIndex = 'repo_seq_sequenced_at_index' - -export async function up(db: Kysely, dialect: Dialect): Promise { - if (dialect === 'pg') { - await db.schema - .alterTable('repo_seq') - .dropConstraint('invalidated_by_fkey') - .execute() - await db.schema.alterTable('repo_seq').dropColumn('invalidatedBy').execute() - await db.schema - .alterTable('repo_seq') - .addColumn('invalidated', 'int2', (col) => col.notNull().defaultTo(0)) - .execute() - } else { - await db.schema.dropTable('repo_seq').execute() - await db.schema - .createTable('repo_seq') - .addColumn('seq', 'integer', (col) => col.autoIncrement().primaryKey()) - .addColumn('did', 'varchar', (col) => col.notNull()) - .addColumn('eventType', 'varchar', (col) => col.notNull()) - .addColumn('event', sql`blob`, (col) => col.notNull()) - .addColumn('invalidated', 'int2', (col) => col.notNull().defaultTo(0)) - .addColumn('sequencedAt', 'varchar', (col) => col.notNull()) - .execute() - } -} - -export async function down( - db: Kysely, - dialect: Dialect, -): Promise { - if (dialect === 'pg') { - await db.schema.alterTable('repo_seq').dropColumn('invalidated').execute() - await db.schema - .alterTable('repo_seq') - .addColumn('invalidatedBy', 'bigint') - .execute() - await db.schema - .alterTable('repo_seq') - .addForeignKeyConstraint( - 'invalidated_by_fkey', - // @ts-ignore - ['invalidatedBy'], - 'repo_seq', - ['seq'], - ) - .execute() - } else { - await db.schema.dropTable('repo_seq').execute() - await db.schema - .createTable('repo_seq') - .addColumn('seq', 'integer', (col) => col.autoIncrement().primaryKey()) - .addColumn('did', 'varchar', (col) => col.notNull()) - .addColumn('eventType', 'varchar', (col) => col.notNull()) - .addColumn('event', sql`blob`, (col) => col.notNull()) - .addColumn('invalidatedBy', 'integer') - .addColumn('sequencedAt', 'varchar', (col) => col.notNull()) - .addForeignKeyConstraint( - 'invalidated_by_fkey', - // @ts-ignore - ['invalidatedBy'], - 'repo_seq', - ['seq'], - ) - .execute() - - // for filtering seqs based on did - await db.schema - .createIndex(repoSeqDidIndex) - .on('repo_seq') - .column('did') - .execute() - - // for filtering seqs based on event type - await db.schema - .createIndex(repoSeqEventTypeIndex) - .on('repo_seq') - .column('eventType') - .execute() - - // for entering into the seq stream at a particular time - await db.schema - .createIndex(repoSeqSequencedAtIndex) - .on('repo_seq') - .column('sequencedAt') - .execute() - } -} diff --git a/packages/pds/src/db/migrations/20230511T154721392Z-mute-lists.ts b/packages/pds/src/db/migrations/20230511T154721392Z-mute-lists.ts deleted file mode 100644 index 20424410ce0..00000000000 --- a/packages/pds/src/db/migrations/20230511T154721392Z-mute-lists.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .createTable('list') - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('name', 'varchar', (col) => col.notNull()) - .addColumn('purpose', 'varchar', (col) => col.notNull()) - .addColumn('description', 'varchar') - .addColumn('descriptionFacets', 'varchar') - .addColumn('avatarCid', 'varchar') - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .execute() - - await db.schema - .createIndex('list_creator_idx') - .on('list') - .column('creator') - .execute() - - await db.schema - .createTable('list_item') - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('subjectDid', 'varchar', (col) => col.notNull()) - .addColumn('listUri', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .addUniqueConstraint('list_item_unique_subject_in_list', [ - 'listUri', - 'subjectDid', - ]) - .execute() - - await db.schema - .createIndex('list_item_creator_idx') - .on('list_item') - .column('creator') - .execute() - - await db.schema - .createIndex('list_item_subject_idx') - .on('list_item') - .column('subjectDid') - .execute() - - await db.schema - .createTable('list_mute') - .addColumn('listUri', 'varchar', (col) => col.notNull()) - .addColumn('mutedByDid', 'varchar', (col) => col.notNull()) - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addPrimaryKeyConstraint('list_mute_pkey', ['mutedByDid', 'listUri']) - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropIndex('list_creator_idx').execute() - await db.schema.dropIndex('list_item_subject_idx').execute() - await db.schema.dropTable('list').execute() - await db.schema.dropTable('list_item').execute() - await db.schema.dropTable('list_mute').execute() -} diff --git a/packages/pds/src/db/migrations/20230511T171739449Z-actor-preferences.ts b/packages/pds/src/db/migrations/20230511T171739449Z-actor-preferences.ts deleted file mode 100644 index 245118c6364..00000000000 --- a/packages/pds/src/db/migrations/20230511T171739449Z-actor-preferences.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Kysely } from 'kysely' -import { Dialect } from '..' - -export async function up(db: Kysely, dialect: Dialect): Promise { - let builder = db.schema.createTable('user_pref') - builder = - dialect === 'pg' - ? builder.addColumn('id', 'bigserial', (col) => col.primaryKey()) - : builder.addColumn('id', 'integer', (col) => - col.autoIncrement().primaryKey(), - ) - await builder - .addColumn('did', 'varchar', (col) => col.notNull()) - .addColumn('name', 'varchar', (col) => col.notNull()) - .addColumn('valueJson', 'text', (col) => col.notNull()) - .execute() - await db.schema - .createIndex('user_pref_did_idx') - .on('user_pref') - .column('did') - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropTable('user_pref').execute() -} diff --git a/packages/pds/src/db/migrations/20230511T200212974Z-feed-generators.ts b/packages/pds/src/db/migrations/20230511T200212974Z-feed-generators.ts deleted file mode 100644 index 5e8b4ba0965..00000000000 --- a/packages/pds/src/db/migrations/20230511T200212974Z-feed-generators.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .createTable('feed_generator') - .addColumn('uri', 'varchar', (col) => col.primaryKey()) - .addColumn('cid', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addColumn('feedDid', 'varchar', (col) => col.notNull()) - .addColumn('displayName', 'varchar') - .addColumn('description', 'varchar') - .addColumn('descriptionFacets', 'varchar') - .addColumn('avatarCid', 'varchar') - .addColumn('createdAt', 'varchar', (col) => col.notNull()) - .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .execute() - - await db.schema - .createIndex('feed_generator_creator_index') - .on('feed_generator') - .column('creator') - .execute() - - await db.schema - .createIndex('feed_generator_feed_did_index') - .on('feed_generator') - .column('feedDid') - .execute() - - await db.schema - .createTable('did_cache') - .addColumn('did', 'varchar', (col) => col.primaryKey()) - .addColumn('doc', 'text', (col) => col.notNull()) - .addColumn('updatedAt', 'bigint', (col) => col.notNull()) - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropTable('did_cache').execute() - await db.schema.dropIndex('feed_generator_creator_index').execute() - await db.schema.dropIndex('feed_generator_feed_did_index').execute() - await db.schema.dropTable('feed_generator').execute() -} diff --git a/packages/pds/src/db/migrations/20230523T183902064Z-algo-whats-hot-view.ts b/packages/pds/src/db/migrations/20230523T183902064Z-algo-whats-hot-view.ts deleted file mode 100644 index 8e4b7e3a943..00000000000 --- a/packages/pds/src/db/migrations/20230523T183902064Z-algo-whats-hot-view.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Kysely, sql } from 'kysely' -import { Dialect } from '..' - -export async function up(db: Kysely, dialect: Dialect): Promise { - if (dialect !== 'pg') return - - const { ref } = db.dynamic - - // materialized views are difficult to change, - // so we parameterize them at runtime with contents of this table. - await db.schema - .createTable('view_param') - .addColumn('name', 'varchar', (col) => col.primaryKey()) - .addColumn('value', 'varchar') - .execute() - - await db - .insertInto('view_param') - .values([ - { name: 'whats_hot_like_threshold', value: '10' }, - { name: 'whats_hot_interval', value: '1day' }, - ]) - .execute() - - // define view query for whats-hot feed - // tldr: scored by like count depreciated over time. - - // From: https://medium.com/hacking-and-gonzo/how-hacker-news-ranking-algorithm-works-1d9b0cf2c08d - // Score = (P-1) / (T+2)^G - // where, - // P = points of an item (and -1 is to negate submitters vote) - // T = time since submission (in hours) - // G = Gravity, defaults to 1.8 in news.arc - - const likeCount = ref('post_agg.likeCount') - const indexedAt = ref('post.indexedAt') - const computeScore = sql`round(1000000 * (${likeCount} / ((EXTRACT(epoch FROM AGE(now(), ${indexedAt}::timestamp)) / 3600 + 2) ^ 1.8)))` - - const viewQb = db - .selectFrom('post') - .innerJoin('post_agg', 'post_agg.uri', 'post.uri') - .where( - 'post.indexedAt', - '>', - db - .selectFrom('view_param') - .where('name', '=', 'whats_hot_interval') - .select( - sql`to_char(now() - value::interval, 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`.as( - 'val', - ), - ), - ) - .where('post.replyParent', 'is', null) - .where( - 'post_agg.likeCount', - '>', - db // helps cull result set that needs to be sorted - .selectFrom('view_param') - .where('name', '=', 'whats_hot_like_threshold') - .select(sql`value::integer`.as('val')), - ) - .select(['post.uri as uri', 'post.cid as cid', computeScore.as('score')]) - - await db.schema - .createView('algo_whats_hot_view') - .materialized() - .as(viewQb) - .execute() - - // unique index required for pg to refresh view w/ "concurrently" param. - await db.schema - .createIndex('algo_whats_hot_view_uri_idx') - .on('algo_whats_hot_view') - .column('uri') - .unique() - .execute() - await db.schema - .createIndex('algo_whats_hot_view_cursor_idx') - .on('algo_whats_hot_view') - .columns(['score', 'cid']) - .execute() -} - -export async function down( - db: Kysely, - dialect: Dialect, -): Promise { - if (dialect !== 'pg') return - await db.schema.dropView('algo_whats_hot_view').materialized().execute() - await db.schema.dropTable('view_param').execute() -} diff --git a/packages/pds/src/db/migrations/20230529T222706121Z-suggested-follows.ts b/packages/pds/src/db/migrations/20230529T222706121Z-suggested-follows.ts deleted file mode 100644 index 11d4ce09be6..00000000000 --- a/packages/pds/src/db/migrations/20230529T222706121Z-suggested-follows.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .createTable('suggested_follow') - .addColumn('did', 'varchar', (col) => col.primaryKey()) - .addColumn('order', 'integer', (col) => col.notNull()) - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropTable('suggested_follow').execute() -} diff --git a/packages/pds/src/db/migrations/20230530T213530067Z-rebase-indices.ts b/packages/pds/src/db/migrations/20230530T213530067Z-rebase-indices.ts deleted file mode 100644 index 299609f5d1b..00000000000 --- a/packages/pds/src/db/migrations/20230530T213530067Z-rebase-indices.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .createIndex('repo_blob_did_idx') - .on('repo_blob') - .column('did') - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropIndex('repo_blob_did_idx').execute() -} diff --git a/packages/pds/src/db/migrations/20230605T235529700Z-outgoing-repo-seq.ts b/packages/pds/src/db/migrations/20230605T235529700Z-outgoing-repo-seq.ts deleted file mode 100644 index cbf0bcd6dac..00000000000 --- a/packages/pds/src/db/migrations/20230605T235529700Z-outgoing-repo-seq.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Kysely, sql } from 'kysely' -import { Dialect } from '..' - -export async function up(db: Kysely, dialect: Dialect): Promise { - if (dialect === 'sqlite') { - await db.schema.dropTable('repo_seq').execute() - await db.schema - .createTable('repo_seq') - .addColumn('id', 'integer', (col) => col.autoIncrement().primaryKey()) - .addColumn('seq', 'integer', (col) => col.unique()) - .addColumn('did', 'varchar', (col) => col.notNull()) - .addColumn('eventType', 'varchar', (col) => col.notNull()) - .addColumn('event', sql`blob`, (col) => col.notNull()) - .addColumn('invalidated', 'int2', (col) => col.notNull().defaultTo(0)) - .addColumn('sequencedAt', 'varchar', (col) => col.notNull()) - .execute() - } else { - await db.schema.alterTable('repo_seq').renameColumn('seq', 'id').execute() - await db.schema - .alterTable('repo_seq') - .addColumn('seq', 'bigint', (col) => col.unique()) - .execute() - } -} - -export async function down( - db: Kysely, - dialect: Dialect, -): Promise { - if (dialect === 'sqlite') { - await db.schema.dropTable('repo_seq').execute() - await db.schema - .createTable('repo_seq') - .addColumn('seq', 'integer', (col) => col.autoIncrement().primaryKey()) - .addColumn('did', 'varchar', (col) => col.notNull()) - .addColumn('eventType', 'varchar', (col) => col.notNull()) - .addColumn('event', sql`blob`, (col) => col.notNull()) - .addColumn('invalidated', 'int2', (col) => col.notNull().defaultTo(0)) - .addColumn('sequencedAt', 'varchar', (col) => col.notNull()) - .execute() - } else { - await db.schema.alterTable('repo_seq').dropColumn('seq').execute() - await db.schema.alterTable('repo_seq').renameColumn('id', 'seq').execute() - } -} diff --git a/packages/pds/src/db/migrations/20230613T164932261Z-init.ts b/packages/pds/src/db/migrations/20230613T164932261Z-init.ts new file mode 100644 index 00000000000..c7ebd104d8b --- /dev/null +++ b/packages/pds/src/db/migrations/20230613T164932261Z-init.ts @@ -0,0 +1,386 @@ +import { Kysely, sql } from 'kysely' +import { Dialect } from '..' + +// @TODO make takedownId a varchar w/o fkey? + +export async function up(db: Kysely, dialect: Dialect): Promise { + const binaryDatatype = dialect === 'sqlite' ? 'blob' : sql`bytea` + + await db.schema + .createTable('app_migration') + .addColumn('id', 'varchar', (col) => col.primaryKey()) + .addColumn('success', 'int2', (col) => col.notNull().defaultTo(0)) + .addColumn('completedAt', 'varchar', (col) => col) + .execute() + + await db.schema + .createTable('app_password') + .addColumn('did', 'varchar', (col) => col.notNull()) + .addColumn('name', 'varchar', (col) => col.notNull()) + .addColumn('passwordScrypt', 'varchar', (col) => col.notNull()) + .addColumn('createdAt', 'varchar', (col) => col.notNull()) + .addPrimaryKeyConstraint('app_password_pkey', ['did', 'name']) + .execute() + + await db.schema + .createTable('backlink') + .addColumn('uri', 'varchar', (col) => col.notNull()) + .addColumn('path', 'varchar', (col) => col.notNull()) + .addColumn('linkToUri', 'varchar') + .addColumn('linkToDid', 'varchar') + .addPrimaryKeyConstraint('backlinks_pkey', ['uri', 'path']) + .addCheckConstraint( + 'backlink_link_to_chk', + // Exactly one of linkToUri or linkToDid should be set + sql`("linkToUri" is null and "linkToDid" is not null) or ("linkToUri" is not null and "linkToDid" is null)`, + ) + .execute() + await db.schema + .createIndex('backlink_path_to_uri_idx') + .on('backlink') + .columns(['path', 'linkToUri']) + .execute() + await db.schema + .createIndex('backlink_path_to_did_idx') + .on('backlink') + .columns(['path', 'linkToDid']) + .execute() + + // @NOTE renamed pkey constraint + await db.schema + .createTable('blob') + .addColumn('creator', 'varchar', (col) => col.notNull()) + .addColumn('cid', 'varchar', (col) => col.notNull()) + .addColumn('mimeType', 'varchar', (col) => col.notNull()) + .addColumn('size', 'integer', (col) => col.notNull()) + .addColumn('tempKey', 'varchar') + .addColumn('width', 'integer') + .addColumn('height', 'integer') + .addColumn('createdAt', 'varchar', (col) => col.notNull()) + .addPrimaryKeyConstraint('blob_pkey', ['creator', 'cid']) + .execute() + + await db.schema + .createTable('delete_account_token') + .addColumn('did', 'varchar', (col) => col.primaryKey()) + .addColumn('token', 'varchar', (col) => col.notNull()) + .addColumn('requestedAt', 'varchar', (col) => col.notNull()) + .execute() + + await db.schema + .createTable('did_cache') + .addColumn('did', 'varchar', (col) => col.primaryKey()) + .addColumn('doc', 'text', (col) => col.notNull()) + .addColumn('updatedAt', 'bigint', (col) => col.notNull()) + .execute() + + // @NOTE dropped handle trigram index + await db.schema + .createTable('did_handle') + .addColumn('did', 'varchar', (col) => col.primaryKey()) + .addColumn('handle', 'varchar', (col) => col.unique()) + .execute() + await db.schema + .createIndex(`did_handle_handle_lower_idx`) + .unique() + .on('did_handle') + .expression(sql`lower("handle")`) + .execute() + + await db.schema + .createTable('invite_code') + .addColumn('code', 'varchar', (col) => col.primaryKey()) + .addColumn('availableUses', 'integer', (col) => col.notNull()) + .addColumn('disabled', 'int2', (col) => col.defaultTo(0)) + .addColumn('forUser', 'varchar', (col) => col.notNull()) + .addColumn('createdBy', 'varchar', (col) => col.notNull()) + .addColumn('createdAt', 'varchar', (col) => col.notNull()) + .execute() + + await db.schema + .createTable('invite_code_use') + .addColumn('code', 'varchar', (col) => col.notNull()) + .addColumn('usedBy', 'varchar', (col) => col.notNull()) + .addColumn('usedAt', 'varchar', (col) => col.notNull()) + .addPrimaryKeyConstraint(`invite_code_use_pkey`, ['code', 'usedBy']) + .execute() + + // @NOTE renamed pkey + await db.schema + .createTable('ipld_block') + .addColumn('creator', 'varchar', (col) => col.notNull()) + .addColumn('cid', 'varchar', (col) => col.notNull()) + .addColumn('size', 'integer', (col) => col.notNull()) + .addColumn('content', binaryDatatype, (col) => col.notNull()) + .addPrimaryKeyConstraint('ipld_block_pkey', ['creator', 'cid']) + .execute() + + const moderationActionBuilder = + dialect === 'pg' + ? db.schema + .createTable('moderation_action') + .addColumn('id', 'serial', (col) => col.primaryKey()) + : db.schema + .createTable('moderation_action') + .addColumn('id', 'integer', (col) => col.autoIncrement().primaryKey()) + await moderationActionBuilder + .addColumn('action', 'varchar', (col) => col.notNull()) + .addColumn('subjectType', 'varchar', (col) => col.notNull()) + .addColumn('subjectDid', 'varchar', (col) => col.notNull()) + .addColumn('subjectUri', 'varchar') + .addColumn('subjectCid', 'varchar') + .addColumn('reason', 'text', (col) => col.notNull()) + .addColumn('createdAt', 'varchar', (col) => col.notNull()) + .addColumn('createdBy', 'varchar', (col) => col.notNull()) + .addColumn('reversedAt', 'varchar') + .addColumn('reversedBy', 'varchar') + .addColumn('reversedReason', 'text') + .addColumn('createLabelVals', 'varchar') + .addColumn('negateLabelVals', 'varchar') + .execute() + + await db.schema + .createTable('moderation_action_subject_blob') + .addColumn('actionId', 'integer', (col) => + col.notNull().references('moderation_action.id'), + ) + .addColumn('cid', 'varchar', (col) => col.notNull()) + .addColumn('recordUri', 'varchar', (col) => col.notNull()) + .addPrimaryKeyConstraint('moderation_action_subject_blob_pkey', [ + 'actionId', + 'cid', + 'recordUri', + ]) + .execute() + + const moderationReportBuilder = + dialect === 'pg' + ? db.schema + .createTable('moderation_report') + .addColumn('id', 'serial', (col) => col.primaryKey()) + : db.schema + .createTable('moderation_report') + .addColumn('id', 'integer', (col) => col.autoIncrement().primaryKey()) + await moderationReportBuilder + .addColumn('subjectType', 'varchar', (col) => col.notNull()) + .addColumn('subjectDid', 'varchar', (col) => col.notNull()) + .addColumn('subjectUri', 'varchar') + .addColumn('subjectCid', 'varchar') + .addColumn('reasonType', 'varchar', (col) => col.notNull()) + .addColumn('reason', 'text') + .addColumn('reportedByDid', 'varchar', (col) => col.notNull()) + .addColumn('createdAt', 'varchar', (col) => col.notNull()) + .execute() + + await db.schema + .createTable('moderation_report_resolution') + .addColumn('reportId', 'integer', (col) => + col.notNull().references('moderation_report.id'), + ) + .addColumn('actionId', 'integer', (col) => + col.notNull().references('moderation_action.id'), + ) + .addColumn('createdBy', 'varchar', (col) => col.notNull()) + .addColumn('createdAt', 'varchar', (col) => col.notNull()) + .addPrimaryKeyConstraint('moderation_report_resolution_pkey', [ + 'reportId', + 'actionId', + ]) + .execute() + await db.schema + .createIndex('moderation_report_resolution_action_id_idx') + .on('moderation_report_resolution') + .column('actionId') + .execute() + + // @NOTE renamed indexes for consistency + await db.schema + .createTable('record') + .addColumn('uri', 'varchar', (col) => col.primaryKey()) + .addColumn('cid', 'varchar', (col) => col.notNull()) + .addColumn('did', 'varchar', (col) => col.notNull()) + .addColumn('collection', 'varchar', (col) => col.notNull()) + .addColumn('rkey', 'varchar', (col) => col.notNull()) + .addColumn('indexedAt', 'varchar', (col) => col.notNull()) + .addColumn('takedownId', 'integer', (col) => + col.references('moderation_action.id'), + ) + .execute() + await db.schema + .createIndex('record_did_cid_idx') + .on('record') + .columns(['did', 'cid']) + .execute() + await db.schema + .createIndex('record_did_collection_idx') + .on('record') + .columns(['did', 'collection']) + .execute() + + await db.schema + .createTable('refresh_token') + .addColumn('id', 'varchar', (col) => col.primaryKey()) + .addColumn('did', 'varchar', (col) => col.notNull()) + .addColumn('expiresAt', 'varchar', (col) => col.notNull()) + .addColumn('nextId', 'varchar') + .addColumn('appPasswordName', 'varchar') + .execute() + await db.schema // Aids in refresh token cleanup + .createIndex('refresh_token_did_idx') + .on('refresh_token') + .column('did') + .execute() + + await db.schema + .createTable('repo_blob') + .addColumn('cid', 'varchar', (col) => col.notNull()) + .addColumn('recordUri', 'varchar', (col) => col.notNull()) + .addColumn('commit', 'varchar', (col) => col.notNull()) + .addColumn('did', 'varchar', (col) => col.notNull()) + .addColumn('takedownId', 'integer', (col) => + col.references('moderation_action.id'), + ) + .addPrimaryKeyConstraint(`repo_blob_pkey`, ['cid', 'recordUri']) + .execute() + await db.schema // supports rebase + .createIndex('repo_blob_did_idx') + .on('repo_blob') + .column('did') + .execute() + + await db.schema + .createTable('repo_commit_block') + .addColumn('creator', 'varchar', (col) => col.notNull()) + .addColumn('commit', 'varchar', (col) => col.notNull()) + .addColumn('block', 'varchar', (col) => col.notNull()) + .addPrimaryKeyConstraint('repo_commit_block_pkey', [ + 'creator', + 'commit', + 'block', + ]) + .execute() + + await db.schema + .createTable('repo_commit_history') + .addColumn('creator', 'varchar', (col) => col.notNull()) + .addColumn('commit', 'varchar', (col) => col.notNull()) + .addColumn('prev', 'varchar') + .addPrimaryKeyConstraint('repo_commit_history_pkey', ['creator', 'commit']) + .execute() + + await db.schema + .createTable('repo_root') + .addColumn('did', 'varchar', (col) => col.primaryKey()) + .addColumn('root', 'varchar', (col) => col.notNull()) + .addColumn('indexedAt', 'varchar', (col) => col.notNull()) + .addColumn('takedownId', 'integer', (col) => + col.references('moderation_action.id'), + ) + .execute() + + // @TODO renamed indexes for consistency + const repoSeqBuilder = + dialect === 'pg' + ? db.schema + .createTable('repo_seq') + .addColumn('id', 'bigserial', (col) => col.primaryKey()) + .addColumn('seq', 'bigint', (col) => col.unique()) + : db.schema + .createTable('repo_seq') + .addColumn('id', 'integer', (col) => col.autoIncrement().primaryKey()) + .addColumn('seq', 'integer', (col) => col.unique()) + await repoSeqBuilder + .addColumn('did', 'varchar', (col) => col.notNull()) + .addColumn('eventType', 'varchar', (col) => col.notNull()) + .addColumn('event', binaryDatatype, (col) => col.notNull()) + .addColumn('invalidated', 'int2', (col) => col.notNull().defaultTo(0)) + .addColumn('sequencedAt', 'varchar', (col) => col.notNull()) + .execute() + // for filtering seqs based on did + await db.schema + .createIndex('repo_seq_did_idx') + .on('repo_seq') + .column('did') + .execute() + // for filtering seqs based on event type + await db.schema + .createIndex('repo_seq_event_type_idx') + .on('repo_seq') + .column('eventType') + .execute() + // for entering into the seq stream at a particular time + await db.schema + .createIndex('repo_seq_sequenced_at_index') + .on('repo_seq') + .column('sequencedAt') + .execute() + + // @NOTE removed unique constraint on email, since we have one on lower(email) + await db.schema + .createTable('user_account') + .addColumn('did', 'varchar', (col) => col.primaryKey()) + .addColumn('email', 'varchar', (col) => col.notNull()) + .addColumn('passwordScrypt', 'varchar', (col) => col.notNull()) + .addColumn('createdAt', 'varchar', (col) => col.notNull()) + .addColumn('passwordResetToken', 'varchar') + .addColumn('passwordResetGrantedAt', 'varchar') + .addColumn('invitesDisabled', 'int2', (col) => col.notNull().defaultTo(0)) + .execute() + await db.schema + .createIndex(`user_account_email_lower_idx`) + .unique() + .on('user_account') + .expression(sql`lower("email")`) + .execute() + await db.schema + .createIndex('user_account_password_reset_token_idx') + .unique() + .on('user_account') + .column('passwordResetToken') + .execute() + + const userPrefBuilder = + dialect === 'pg' + ? db.schema + .createTable('user_pref') + .addColumn('id', 'bigserial', (col) => col.primaryKey()) + : db.schema + .createTable('user_pref') + .addColumn('id', 'integer', (col) => col.autoIncrement().primaryKey()) + await userPrefBuilder + .addColumn('did', 'varchar', (col) => col.notNull()) + .addColumn('name', 'varchar', (col) => col.notNull()) + .addColumn('valueJson', 'text', (col) => col.notNull()) + .execute() + await db.schema + .createIndex('user_pref_did_idx') + .on('user_pref') + .column('did') + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('user_pref').execute() + await db.schema.dropTable('user_account').execute() + await db.schema.dropTable('repo_seq').execute() + await db.schema.dropTable('repo_root').execute() + await db.schema.dropTable('repo_commit_history').execute() + await db.schema.dropTable('repo_commit_block').execute() + await db.schema.dropTable('repo_blob').execute() + await db.schema.dropTable('refresh_token').execute() + await db.schema.dropTable('record').execute() + await db.schema.dropTable('moderation_report_resolution').execute() + await db.schema.dropTable('moderation_report').execute() + await db.schema.dropTable('moderation_action_subject_blob').execute() + await db.schema.dropTable('moderation_action').execute() + await db.schema.dropTable('ipld_block').execute() + await db.schema.dropTable('invite_code_use').execute() + await db.schema.dropTable('invite_code').execute() + await db.schema.dropTable('did_handle').execute() + await db.schema.dropTable('did_cache').execute() + await db.schema.dropTable('delete_account_token').execute() + await db.schema.dropTable('blob').execute() + await db.schema.dropTable('backlink').execute() + await db.schema.dropTable('app_password').execute() + await db.schema.dropTable('app_migration').execute() +} diff --git a/packages/pds/src/db/migrations/index.ts b/packages/pds/src/db/migrations/index.ts index f723e73119f..9de245dda96 100644 --- a/packages/pds/src/db/migrations/index.ts +++ b/packages/pds/src/db/migrations/index.ts @@ -2,54 +2,4 @@ // It's important that every migration is exported from here with the proper name. We'd simplify // this with kysely's FileMigrationProvider, but it doesn't play nicely with the build process. -export * as _20221021T162202001Z from './20221021T162202001Z-init' -export * as _20221116T234458063Z from './20221116T234458063Z-duplicate-records' -export * as _20221202T212459280Z from './20221202T212459280Z-blobs' -export * as _20221209T210026294Z from './20221209T210026294Z-banners' -export * as _20221212T195416407Z from './20221212T195416407Z-post-media' -export * as _20221215T220356370Z from './20221215T220356370Z-password-reset-otp' -export * as _20221226T213635517Z from './20221226T213635517Z-mute-init' -export * as _20221230T215012029Z from './20221230T215012029Z-moderation-init' -export * as _20230127T215753149Z from './20230127T215753149Z-indexed-at-on-record' -export * as _20230127T224743452Z from './20230127T224743452Z-repo-sync-data-pt1' -export * as _20230201T200606704Z from './20230201T200606704Z-repo-sync-data-pt2' -export * as _20230202T170426672Z from './20230202T170426672Z-user-partitioned-cids' -export * as _20230202T170435937Z from './20230202T170435937Z-delete-account-token' -export * as _20230202T172831900Z from './20230202T172831900Z-moderation-subject-blob' -export * as _20230202T213952826Z from './20230202T213952826Z-repo-seq' -export * as _20230208T081544325Z from './20230208T081544325Z-post-hydrate-indices' -export * as _20230208T222001557Z from './20230208T222001557Z-user-table-did-pkey' -export * as _20230210T210132396Z from './20230210T210132396Z-post-hierarchy' -export * as _20230214T172233550Z from './20230214T172233550Z-embed-records' -export * as _20230301T222603402Z from './20230301T222603402Z-repo-ops' -export * as _20230304T193548198Z from './20230304T193548198Z-pagination-indices' -export * as _20230308T234640077Z from './20230308T234640077Z-record-indexes' -export * as _20230309T012947663Z from './20230309T012947663Z-app-migration' -export * as _20230310T205728933Z from './20230310T205728933Z-subscription-init' -export * as _20230313T232322844Z from './20230313T232322844Z-blob-creator' -export * as _20230314T023842127Z from './20230314T023842127Z-refresh-grace-period' -export * as _20230323T162732466Z from './20230323T162732466Z-remove-scenes' -export * as _20230328T214311000Z from './20230328T214311000Z-remove-declarations-assertions-confirmations' -export * as _20230328T214311001Z from './20230328T214311001Z-votes-to-likes' -export * as _20230328T214311002Z from './20230328T214311002Z-remove-post-entities' -export * as _20230328T214311003Z from './20230328T214311003Z-backlinks' -export * as _20230328T214311004Z from './20230328T214311004Z-profile-display-name-empty' -export * as _20230328T214311005Z from './20230328T214311005Z-rework-seq' -export * as _20230406T185855842Z from './20230406T185855842Z-feed-item-init' -export * as _20230411T175730759Z from './20230411T175730759Z-drop-message-queue' -export * as _20230411T180247652Z from './20230411T180247652Z-labels' -export * as _20230412T231807162Z from './20230412T231807162Z-moderation-action-labels' -export * as _20230416T221236745Z from './20230416T221236745Z-app-specific-passwords' -export * as _20230420T143821201Z from './20230420T143821201Z-post-profile-aggs' -export * as _20230427T194652255Z from './20230427T194652255Z-notif-record-index' -export * as _20230428T195614638Z from './20230428T195614638Z-actor-block-init' -export * as _20230508T193807762Z from './20230508T193807762Z-acct-deletion-indexes' -export * as _20230508T232711152Z from './20230508T232711152Z-disable-account-invites' -export * as _20230509T192324175Z from './20230509T192324175Z-seq-invalidated' -export * as _20230511T154721392Z from './20230511T154721392Z-mute-lists' -export * as _20230511T171739449Z from './20230511T171739449Z-actor-preferences' -export * as _20230511T200212974Z from './20230511T200212974Z-feed-generators' -export * as _20230523T183902064Z from './20230523T183902064Z-algo-whats-hot-view' -export * as _20230529T222706121Z from './20230529T222706121Z-suggested-follows' -export * as _20230530T213530067Z from './20230530T213530067Z-rebase-indices' -export * as _20230605T235529700Z from './20230605T235529700Z-outgoing-repo-seq' +export * as _20230613T164932261Z from './20230613T164932261Z-init' From 27ab9e6873d59fab110d2f224417e9bb9642b75c Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 13 Jun 2023 13:29:06 -0500 Subject: [PATCH 031/105] fix up dev-env --- packages/dev-env/src/pds.ts | 73 ++++++++++++----------------------- packages/dev-env/src/types.ts | 6 +-- packages/pds/src/index.ts | 2 +- 3 files changed, 28 insertions(+), 53 deletions(-) diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index 825b5d3be66..45d6e04f75a 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -1,13 +1,15 @@ +import path from 'node:path' +import os from 'node:os' import getPort from 'get-port' import * as ui8 from 'uint8arrays' import * as pds from '@atproto/pds' -import { Secp256k1Keypair } from '@atproto/crypto' +import { Secp256k1Keypair, randomStr } from '@atproto/crypto' import { AtpAgent } from '@atproto/api' -import { Client as PlcClient } from '@did-plc/lib' -import { DAY, HOUR } from '@atproto/common-web' import { PdsConfig } from './types' import { uniqueLockId } from './util' +const ADMIN_PASSWORD = 'admin-pass' + export class TestPds { constructor( public url: string, @@ -16,64 +18,37 @@ export class TestPds { ) {} static async create(cfg: PdsConfig): Promise { - const repoSigningKey = await Secp256k1Keypair.create() - const plcRotationKey = await Secp256k1Keypair.create() - const recoveryKey = await Secp256k1Keypair.create() + const repoSigningKey = await Secp256k1Keypair.create({ exportable: true }) + const repoSigningPriv = ui8.toString(await repoSigningKey.export(), 'hex') + const plcRotationKey = await Secp256k1Keypair.create({ exportable: true }) + const plcRotationPriv = ui8.toString(await plcRotationKey.export(), 'hex') + const recoveryKey = (await Secp256k1Keypair.create()).did() const port = cfg.port || (await getPort()) const url = `http://localhost:${port}` - const plcClient = new PlcClient(cfg.plcUrl) - const serverDid = await plcClient.createDid({ - signingKey: repoSigningKey.did(), - rotationKeys: [recoveryKey.did(), plcRotationKey.did()], - handle: 'pds.test', - pds: `http://localhost:${port}`, - signer: plcRotationKey, - }) + const blobstoreLoc = path.join(os.tmpdir(), randomStr(8, 'base32')) - const config = new pds.ServerConfig({ - debugMode: true, - version: '0.0.0', - scheme: 'http', + const env = pds.envToCfg({ port, - hostname: 'localhost', - serverDid, - recoveryKey: recoveryKey.did(), - adminPassword: 'admin-pass', - inviteRequired: false, - userInviteInterval: null, + blobstoreDiskLocation: blobstoreLoc, + recoveryDidKey: recoveryKey, + adminPassword: ADMIN_PASSWORD, didPlcUrl: cfg.plcUrl, - didCacheMaxTTL: DAY, - didCacheStaleTTL: HOUR, jwtSecret: 'jwt-secret', - availableUserDomains: ['.test'], - appUrlPasswordReset: 'app://forgot-password', - emailNoReplyAddress: 'noreply@blueskyweb.xyz', - publicUrl: 'https://pds.public.url', - dbPostgresUrl: cfg.dbPostgresUrl, - maxSubscriptionBuffer: 200, - repoBackfillLimitMs: 1000 * 60 * 60, // 1hr + handleDomains: ['.test'], sequencerLeaderLockId: uniqueLockId(), + repoSigningKeyK256PrivateKeyHex: repoSigningPriv, + plcRotationKeyK256PrivateKeyHex: plcRotationPriv, ...cfg, }) - const blobstore = new pds.MemoryBlobStore() - const db = config.dbPostgresUrl - ? pds.Database.postgres({ - url: config.dbPostgresUrl, - schema: config.dbPostgresSchema, - }) - : pds.Database.memory() - await db.migrateToLatestOrThrow() + const server = await pds.PDS.create( + pds.envToCfg(env), + pds.envToSecrets(env), + ) - const server = pds.PDS.create({ - db, - blobstore, - repoSigningKey, - plcRotationKey, - config, - }) + await server.ctx.db.migrateToLatestOrThrow() await server.start() return new TestPds(url, port, server) @@ -91,7 +66,7 @@ export class TestPds { return ( 'Basic ' + ui8.toString( - ui8.fromString(`admin:${this.ctx.cfg.adminPassword}`, 'utf8'), + ui8.fromString(`admin:${ADMIN_PASSWORD}`, 'utf8'), 'base64pad', ) ) diff --git a/packages/dev-env/src/types.ts b/packages/dev-env/src/types.ts index ec088ec4806..aac141d7445 100644 --- a/packages/dev-env/src/types.ts +++ b/packages/dev-env/src/types.ts @@ -6,7 +6,7 @@ export type PlcConfig = { version?: string } -export type PdsConfig = Partial & { +export type PdsConfig = Partial & { plcUrl: string migration?: string } @@ -22,7 +22,7 @@ export type BskyConfig = Partial & { export type TestServerParams = { dbPostgresUrl: string dbPostgresSchema: string - pds: Partial - plc: Partial + pds: Partial + plc: Partial bsky: Partial } diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index 47049bb3eab..52834df7b85 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -18,7 +18,7 @@ import { createServer } from './lexicon' import { createHttpTerminator, HttpTerminator } from 'http-terminator' import AppContext, { AppContextOptions } from './context' -export type { ServerConfig, ServerSecrets } from './config' +export * from './config' export { Database } from './db' export { DiskBlobStore, MemoryBlobStore } from './storage' export { AppContext } from './context' From 906513db3e67ca727605edc01b52f83f4aa4b391 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 13 Jun 2023 13:36:17 -0500 Subject: [PATCH 032/105] tidy --- packages/pds/tests/_util.ts | 4 ++++ packages/pds/tests/account.test.ts | 19 ++++++++++--------- packages/pds/tests/handles.test.ts | 2 +- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/pds/tests/_util.ts b/packages/pds/tests/_util.ts index c44ff456728..68e36e02a36 100644 --- a/packages/pds/tests/_util.ts +++ b/packages/pds/tests/_util.ts @@ -1,6 +1,7 @@ import { AddressInfo } from 'net' import os from 'os' import path from 'path' +import getPort from 'get-port' import * as crypto from '@atproto/crypto' import { PlcServer, Database as PlcDatabase } from '@did-plc/server' import { AtUri } from '@atproto/uri' @@ -61,7 +62,10 @@ export const runTestServer = async ( const blobstoreLoc = path.join(os.tmpdir(), randomStr(5, 'base32')) + const port = await getPort() + const env: ServerEnvironment = { + port, dbPostgresUrl: dbPostgresUrl, blobstoreDiskLocation: blobstoreLoc, recoveryDidKey: recoveryKey, diff --git a/packages/pds/tests/account.test.ts b/packages/pds/tests/account.test.ts index 2b6a32426c4..5fc79705025 100644 --- a/packages/pds/tests/account.test.ts +++ b/packages/pds/tests/account.test.ts @@ -46,10 +46,11 @@ describe('account', () => { beforeAll(async () => { const server = await util.runTestServer({ + hostname: 'pds.public.url', inviteRequired: true, - userInviteInterval: DAY, + inviteInterval: DAY, termsOfServiceUrl: 'https://example.com/tos', - privacyPolicyUrl: '/privacy-policy', + privacyPolicyUrl: 'https://example.com/privacy-policy', dbPostgresSchema: 'account', }) close = server.close @@ -58,7 +59,7 @@ describe('account', () => { ctx = server.ctx serverUrl = server.url repoSigningKey = server.ctx.repoSigningKey.did() - idResolver = new IdResolver({ plcUrl: ctx.cfg.didPlcUrl }) + idResolver = server.ctx.idResolver agent = new AtpAgent({ service: serverUrl }) // Catch emails for use in tests @@ -94,7 +95,7 @@ describe('account', () => { expect(res.data.availableUserDomains[0]).toBe('.test') expect(typeof res.data.inviteCodeRequired).toBe('boolean') expect(res.data.links?.privacyPolicy).toBe( - 'https://pds.public.url/privacy-policy', + 'https://example.com/privacy-policy', ) expect(res.data.links?.termsOfService).toBe('https://example.com/tos') }) @@ -163,7 +164,7 @@ describe('account', () => { expect(didData.rotationKeys).toEqual([ recoveryKey, - ctx.cfg.recoveryKey, + ctx.cfg.identity.recoveryDidKey, ctx.plcRotationKey.did(), ]) }) @@ -177,10 +178,10 @@ describe('account', () => { handle, rotationKeys: [ userKey.did(), - ctx.cfg.recoveryKey, + ctx.cfg.identity.recoveryDidKey ?? '', ctx.plcRotationKey.did(), ], - pds: ctx.cfg.publicUrl, + pds: ctx.cfg.service.publicUrl, signer: userKey, }) @@ -204,10 +205,10 @@ describe('account', () => { handle: 'byo-did.test', rotationKeys: [ userKey.did(), - ctx.cfg.recoveryKey, + ctx.cfg.identity.recoveryDidKey ?? '', ctx.plcRotationKey.did(), ], - pds: ctx.cfg.publicUrl, + pds: ctx.cfg.service.publicUrl, signer: userKey, } const baseAccntInfo = { diff --git a/packages/pds/tests/handles.test.ts b/packages/pds/tests/handles.test.ts index 7fdde0ce2fa..c6ecb39bf7c 100644 --- a/packages/pds/tests/handles.test.ts +++ b/packages/pds/tests/handles.test.ts @@ -38,7 +38,7 @@ describe('handles', () => { dbPostgresSchema: 'handles', }) ctx = server.ctx - idResolver = new IdResolver({ plcUrl: ctx.cfg.didPlcUrl }) + idResolver = ctx.idResolver close = server.close agent = new AtpAgent({ service: server.url }) sc = new SeedClient(agent) From 197d1a2e43ea61287190acf6666e71083397da97 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 13 Jun 2023 14:24:16 -0500 Subject: [PATCH 033/105] cleanup --- packages/dev-env/src/network.ts | 2 +- packages/dev-env/src/pds.ts | 32 ++++++++++++++++-------- packages/dev-env/src/types.ts | 2 +- packages/pds/tests/proxied/views.test.ts | 2 +- packages/pds/tests/server.test.ts | 1 + 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/dev-env/src/network.ts b/packages/dev-env/src/network.ts index a54bfde5359..d2422d9abd0 100644 --- a/packages/dev-env/src/network.ts +++ b/packages/dev-env/src/network.ts @@ -42,7 +42,7 @@ export class TestNetwork extends TestNetworkNoAppView { port: pdsPort, dbPostgresUrl, dbPostgresSchema, - plcUrl: plc.url, + didPlcUrl: plc.url, bskyAppViewEndpoint: bsky.url, bskyAppViewDid: bsky.ctx.cfg.serverDid, ...params.pds, diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index 45d6e04f75a..4cada378eea 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -17,38 +17,48 @@ export class TestPds { public server: pds.PDS, ) {} - static async create(cfg: PdsConfig): Promise { + static async create(config: PdsConfig): Promise { const repoSigningKey = await Secp256k1Keypair.create({ exportable: true }) const repoSigningPriv = ui8.toString(await repoSigningKey.export(), 'hex') const plcRotationKey = await Secp256k1Keypair.create({ exportable: true }) const plcRotationPriv = ui8.toString(await plcRotationKey.export(), 'hex') const recoveryKey = (await Secp256k1Keypair.create()).did() - const port = cfg.port || (await getPort()) + const port = config.port || (await getPort()) const url = `http://localhost:${port}` const blobstoreLoc = path.join(os.tmpdir(), randomStr(8, 'base32')) - const env = pds.envToCfg({ + const env: pds.ServerEnvironment = { port, blobstoreDiskLocation: blobstoreLoc, recoveryDidKey: recoveryKey, adminPassword: ADMIN_PASSWORD, - didPlcUrl: cfg.plcUrl, jwtSecret: 'jwt-secret', handleDomains: ['.test'], sequencerLeaderLockId: uniqueLockId(), repoSigningKeyK256PrivateKeyHex: repoSigningPriv, plcRotationKeyK256PrivateKeyHex: plcRotationPriv, - ...cfg, - }) + ...config, + } + const cfg = pds.envToCfg(env) + const secrets = pds.envToSecrets(env) - const server = await pds.PDS.create( - pds.envToCfg(env), - pds.envToSecrets(env), - ) + const server = await pds.PDS.create(cfg, secrets) - await server.ctx.db.migrateToLatestOrThrow() + // Separate migration db on postgres in case migration changes some + // connection state that we need in the tests, e.g. "alter database ... set ..." + const migrationDb = + cfg.db.dialect === 'pg' + ? pds.Database.postgres({ + url: cfg.db.url, + schema: cfg.db.schema, + }) + : server.ctx.db + await migrationDb.migrateToLatestOrThrow() + if (migrationDb !== server.ctx.db) { + await migrationDb.close() + } await server.start() return new TestPds(url, port, server) diff --git a/packages/dev-env/src/types.ts b/packages/dev-env/src/types.ts index aac141d7445..2dc96040d06 100644 --- a/packages/dev-env/src/types.ts +++ b/packages/dev-env/src/types.ts @@ -7,7 +7,7 @@ export type PlcConfig = { } export type PdsConfig = Partial & { - plcUrl: string + didPlcUrl: string migration?: string } diff --git a/packages/pds/tests/proxied/views.test.ts b/packages/pds/tests/proxied/views.test.ts index fec0e13cd20..27679063c2b 100644 --- a/packages/pds/tests/proxied/views.test.ts +++ b/packages/pds/tests/proxied/views.test.ts @@ -32,7 +32,7 @@ describe('proxies view requests', () => { await network.close() }) - it('actor.getProfile', async () => { + it.only('actor.getProfile', async () => { const res = await agent.api.app.bsky.actor.getProfile( { actor: bob, diff --git a/packages/pds/tests/server.test.ts b/packages/pds/tests/server.test.ts index 5912af3a0aa..3036ddcf43a 100644 --- a/packages/pds/tests/server.test.ts +++ b/packages/pds/tests/server.test.ts @@ -19,6 +19,7 @@ describe('server', () => { beforeAll(async () => { server = await runTestServer({ dbPostgresSchema: 'server', + version: '0.0.0', }) close = server.close db = server.ctx.db From 0c38164b485b5def473a57a3c80786a90a83f9be Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Tue, 13 Jun 2023 15:35:50 -0400 Subject: [PATCH 034/105] fix pds admin tests --- packages/pds/tests/admin/invites.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/tests/admin/invites.test.ts b/packages/pds/tests/admin/invites.test.ts index fdd76c24d8e..6a598666038 100644 --- a/packages/pds/tests/admin/invites.test.ts +++ b/packages/pds/tests/admin/invites.test.ts @@ -17,7 +17,7 @@ describe('pds admin invite views', () => { server = await runTestServer({ dbPostgresSchema: 'views_admin_invites', inviteRequired: true, - userInviteInterval: 1, + inviteInterval: 1, }) agent = new AtpAgent({ service: server.url }) sc = new SeedClient(agent) From caf670d94115a7b90bc5e43e5b9f23f4e47eb530 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 13 Jun 2023 14:45:36 -0500 Subject: [PATCH 035/105] fix handle test --- packages/pds/tests/handles.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/tests/handles.test.ts b/packages/pds/tests/handles.test.ts index c6ecb39bf7c..57976f2d9dd 100644 --- a/packages/pds/tests/handles.test.ts +++ b/packages/pds/tests/handles.test.ts @@ -38,7 +38,7 @@ describe('handles', () => { dbPostgresSchema: 'handles', }) ctx = server.ctx - idResolver = ctx.idResolver + idResolver = new IdResolver({ plcUrl: ctx.cfg.identity.plcUrl }) close = server.close agent = new AtpAgent({ service: server.url }) sc = new SeedClient(agent) From 6632166937c9893cc705408e59e69cdf8ed3bfa9 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Tue, 13 Jun 2023 15:45:57 -0400 Subject: [PATCH 036/105] fix pds proxy tests --- packages/dev-env/src/util.ts | 4 ++-- packages/pds/tests/proxied/views.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/dev-env/src/util.ts b/packages/dev-env/src/util.ts index 398e81b6c58..a4fe2821dea 100644 --- a/packages/dev-env/src/util.ts +++ b/packages/dev-env/src/util.ts @@ -11,7 +11,7 @@ export const mockNetworkUtilities = (pds: TestPds) => { const service = result?.service?.find((svc) => svc.id === '#atproto_pds') if (typeof service?.serviceEndpoint === 'string') { service.serviceEndpoint = service.serviceEndpoint.replace( - pds.ctx.cfg.publicUrl, + pds.ctx.cfg.service.publicUrl, `http://localhost:${pds.port}`, ) } @@ -19,7 +19,7 @@ export const mockNetworkUtilities = (pds: TestPds) => { } HandleResolver.prototype.resolve = async function (handle: string) { - const isPdsHandle = pds.ctx.cfg.availableUserDomains.some((domain) => + const isPdsHandle = pds.ctx.cfg.identity.handleDomains.some((domain) => handle.endsWith(domain), ) if (!isPdsHandle) return undefined diff --git a/packages/pds/tests/proxied/views.test.ts b/packages/pds/tests/proxied/views.test.ts index 27679063c2b..fec0e13cd20 100644 --- a/packages/pds/tests/proxied/views.test.ts +++ b/packages/pds/tests/proxied/views.test.ts @@ -32,7 +32,7 @@ describe('proxies view requests', () => { await network.close() }) - it.only('actor.getProfile', async () => { + it('actor.getProfile', async () => { const res = await agent.api.app.bsky.actor.getProfile( { actor: bob, From 88e7de44f5c8c09a0ba96753a5e0da6cb022d03a Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 13 Jun 2023 14:48:48 -0500 Subject: [PATCH 037/105] fix subscribe repos test --- packages/pds/tests/sync/subscribe-repos.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/pds/tests/sync/subscribe-repos.test.ts b/packages/pds/tests/sync/subscribe-repos.test.ts index ebeb8649150..0fb1cc8638d 100644 --- a/packages/pds/tests/sync/subscribe-repos.test.ts +++ b/packages/pds/tests/sync/subscribe-repos.test.ts @@ -44,6 +44,7 @@ describe('repo subscribe repos', () => { beforeAll(async () => { const server = await runTestServer({ dbPostgresSchema: 'repo_subscribe_repos', + repoBackfillLimitMs: HOUR, }) serverHost = server.url.replace('http://', '') ctx = server.ctx From 8eb6cf6dc7522f1287037d1297cc626ec925abae Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Tue, 13 Jun 2023 15:53:20 -0400 Subject: [PATCH 038/105] fix sqlite config in pds tests --- packages/pds/tests/_util.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/pds/tests/_util.ts b/packages/pds/tests/_util.ts index 68e36e02a36..cb497296980 100644 --- a/packages/pds/tests/_util.ts +++ b/packages/pds/tests/_util.ts @@ -67,6 +67,7 @@ export const runTestServer = async ( const env: ServerEnvironment = { port, dbPostgresUrl: dbPostgresUrl, + dbSqliteLocation: dbPostgresUrl ? undefined : ':memory:', blobstoreDiskLocation: blobstoreLoc, recoveryDidKey: recoveryKey, didPlcUrl: plcUrl, From 453c0d2713774bc467b78414cea5605aca62d1d1 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 13 Jun 2023 14:57:25 -0500 Subject: [PATCH 039/105] add sqlite clause in sequencer-leader --- packages/pds/src/sequencer/sequencer-leader.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/pds/src/sequencer/sequencer-leader.ts b/packages/pds/src/sequencer/sequencer-leader.ts index 3cf68f609a9..9531b3ffce9 100644 --- a/packages/pds/src/sequencer/sequencer-leader.ts +++ b/packages/pds/src/sequencer/sequencer-leader.ts @@ -119,6 +119,9 @@ export class SequencerLeader { } async isCaughtUp(): Promise { + if (this.db.dialect === 'sqlite') { + return true + } const unsequenced = await this.getUnsequenced() return unsequenced.length === 0 } From b8d1f0f7734860cb39333d800343bf37acd7118c Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Tue, 13 Jun 2023 15:57:29 -0400 Subject: [PATCH 040/105] fix actor search w/ sqlite on pds --- packages/pds/src/services/account/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts index 13c4b92eba0..dd38aaf1b0c 100644 --- a/packages/pds/src/services/account/index.ts +++ b/packages/pds/src/services/account/index.ts @@ -245,13 +245,15 @@ export class AccountService { qb.where(notSoftDeletedClause(ref('repo_root'))), ) .where((qb) => { + // sqlite doesn't support "ilike", but performs "like" case-insensitively + const likeOp = this.db.dialect === 'pg' ? 'ilike' : 'like' if (term.includes('@')) { - return qb.where('user_account.email', 'ilike', `%${term}%`) + return qb.where('user_account.email', likeOp, `%${term}%`) } if (term.startsWith('did:')) { return qb.where('did_handle.did', '=', term) } - return qb.where('did_handle.handle', 'ilike', `${term}%`) + return qb.where('did_handle.handle', likeOp, `${term}%`) }) .selectAll(['did_handle', 'repo_root']) From 853dc056291407f03411e69266ab7666b1992a43 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 13 Jun 2023 15:05:50 -0500 Subject: [PATCH 041/105] fixes --- packages/dev-env/src/pds.ts | 2 +- packages/dev-env/src/util.ts | 4 ++-- .../api/com/atproto/admin/updateAccountHandle.ts | 2 +- .../api/com/atproto/identity/resolveHandle.ts | 2 +- .../src/api/com/atproto/identity/updateHandle.ts | 2 +- .../src/api/com/atproto/server/createAccount.ts | 5 ++++- .../src/api/com/atproto/server/describeServer.ts | 2 +- packages/pds/src/config/config.ts | 16 ++++++++-------- packages/pds/src/config/env.ts | 4 ++-- packages/pds/src/sequencer/events.ts | 1 + packages/pds/src/sequencer/sequencer-leader.ts | 3 --- packages/pds/src/well-known.ts | 2 +- packages/pds/tests/_util.ts | 2 +- 13 files changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index 4cada378eea..bd668950e1e 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -35,7 +35,7 @@ export class TestPds { recoveryDidKey: recoveryKey, adminPassword: ADMIN_PASSWORD, jwtSecret: 'jwt-secret', - handleDomains: ['.test'], + serviceHandleDomains: ['.test'], sequencerLeaderLockId: uniqueLockId(), repoSigningKeyK256PrivateKeyHex: repoSigningPriv, plcRotationKeyK256PrivateKeyHex: plcRotationPriv, diff --git a/packages/dev-env/src/util.ts b/packages/dev-env/src/util.ts index a4fe2821dea..aae6dfb5b35 100644 --- a/packages/dev-env/src/util.ts +++ b/packages/dev-env/src/util.ts @@ -19,8 +19,8 @@ export const mockNetworkUtilities = (pds: TestPds) => { } HandleResolver.prototype.resolve = async function (handle: string) { - const isPdsHandle = pds.ctx.cfg.identity.handleDomains.some((domain) => - handle.endsWith(domain), + const isPdsHandle = pds.ctx.cfg.identity.serviceHandleDomains.some( + (domain) => handle.endsWith(domain), ) if (!isPdsHandle) return undefined diff --git a/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts b/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts index aae81471508..1d9426b87af 100644 --- a/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts +++ b/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts @@ -22,7 +22,7 @@ export default function (server: Server, ctx: AppContext) { try { ident.ensureHandleServiceConstraints( handle, - ctx.cfg.identity.handleDomains, + ctx.cfg.identity.serviceHandleDomains, ) } catch (err) { if (err instanceof ident.UnsupportedDomainError) { diff --git a/packages/pds/src/api/com/atproto/identity/resolveHandle.ts b/packages/pds/src/api/com/atproto/identity/resolveHandle.ts index 68a28cfc8a3..72c5837598b 100644 --- a/packages/pds/src/api/com/atproto/identity/resolveHandle.ts +++ b/packages/pds/src/api/com/atproto/identity/resolveHandle.ts @@ -21,7 +21,7 @@ export default function (server: Server, ctx: AppContext) { if (user) { did = user.did } else { - const supportedHandle = ctx.cfg.identity.handleDomains.some( + const supportedHandle = ctx.cfg.identity.serviceHandleDomains.some( (host) => handle.endsWith(host) || handle === host.slice(1), ) // this should be in our DB & we couldn't find it, so fail diff --git a/packages/pds/src/api/com/atproto/identity/updateHandle.ts b/packages/pds/src/api/com/atproto/identity/updateHandle.ts index 5c77521698b..9c021a87d80 100644 --- a/packages/pds/src/api/com/atproto/identity/updateHandle.ts +++ b/packages/pds/src/api/com/atproto/identity/updateHandle.ts @@ -24,7 +24,7 @@ export default function (server: Server, ctx: AppContext) { try { ident.ensureHandleServiceConstraints( handle, - ctx.cfg.identity.handleDomains, + ctx.cfg.identity.serviceHandleDomains, ) } catch (err) { if (err instanceof ident.UnsupportedDomainError) { diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index fa265348881..714edc329ae 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -145,7 +145,10 @@ const ensureValidHandle = async ( ): Promise => { try { const handle = ident.normalizeAndEnsureValidHandle(input.handle) - ident.ensureHandleServiceConstraints(handle, ctx.cfg.identity.handleDomains) + ident.ensureHandleServiceConstraints( + handle, + ctx.cfg.identity.serviceHandleDomains, + ) return handle } catch (err) { if (err instanceof ident.InvalidHandleError) { diff --git a/packages/pds/src/api/com/atproto/server/describeServer.ts b/packages/pds/src/api/com/atproto/server/describeServer.ts index 2e55676bb8c..0ad3b2d66eb 100644 --- a/packages/pds/src/api/com/atproto/server/describeServer.ts +++ b/packages/pds/src/api/com/atproto/server/describeServer.ts @@ -3,7 +3,7 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.server.describeServer(() => { - const availableUserDomains = ctx.cfg.identity.handleDomains + const availableUserDomains = ctx.cfg.identity.serviceHandleDomains const inviteCodeRequired = ctx.cfg.invites.required const privacyPolicy = ctx.cfg.service.privacyPolicyUrl const termsOfService = ctx.cfg.service.termsOfServiceUrl diff --git a/packages/pds/src/config/config.ts b/packages/pds/src/config/config.ts index 56a0f60a377..d6d2ea2cafe 100644 --- a/packages/pds/src/config/config.ts +++ b/packages/pds/src/config/config.ts @@ -66,17 +66,17 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { throw new Error('Must configure either S3 or disk blobstore') } - let handleDomains: string[] - if (env.handleDomains && env.handleDomains.length > 0) { - handleDomains = env.handleDomains + let serviceHandleDomains: string[] + if (env.serviceHandleDomains && env.serviceHandleDomains.length > 0) { + serviceHandleDomains = env.serviceHandleDomains } else { if (hostname === 'localhost') { - handleDomains = ['.test'] + serviceHandleDomains = ['.test'] } else { - handleDomains = [`.${hostname}`] + serviceHandleDomains = [`.${hostname}`] } } - const invalidDomain = handleDomains.find( + const invalidDomain = serviceHandleDomains.find( (domain) => domain.length < 1 || !domain.startsWith('.'), ) if (invalidDomain) { @@ -89,7 +89,7 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { cacheStaleTTL: env.didCacheStaleTTL || HOUR, resolverTimeout: env.resolverTimeout || 3 * SECOND, recoveryDidKey: env.recoveryDidKey ?? null, - handleDomains, + serviceHandleDomains, } const invitesCfg: ServerConfig['invites'] = env.inviteRequired @@ -198,7 +198,7 @@ export type IdentityConfig = { cacheStaleTTL: number cacheMaxTTL: number recoveryDidKey: string | null - handleDomains: string[] + serviceHandleDomains: string[] } export type InvitesConfig = diff --git a/packages/pds/src/config/env.ts b/packages/pds/src/config/env.ts index 5e583914e73..635dfd41059 100644 --- a/packages/pds/src/config/env.ts +++ b/packages/pds/src/config/env.ts @@ -38,7 +38,7 @@ export const readEnv = (): ServerEnvironment => { didCacheMaxTTL: envInt(process.env.PDS_DID_CACHE_MAX_TTL), resolverTimeout: envInt(process.env.PDS_ID_RESOLVER_TIMEOUT), recoveryDidKey: envStr(process.env.PDS_RECOVERY_DID_KEY), - handleDomains: envList(process.env.PDS_HANDLE_DOMAINS), + serviceHandleDomains: envList(process.env.PDS_SERVICE_HANDLE_DOMAINS), // invites inviteRequired: envBool(process.env.PDS_INVITE_REQUIRED), @@ -110,7 +110,7 @@ export type ServerEnvironment = { didCacheMaxTTL?: number resolverTimeout?: number recoveryDidKey?: string - handleDomains?: string[] // public hostname by default + serviceHandleDomains?: string[] // public hostname by default // invites inviteRequired?: boolean diff --git a/packages/pds/src/sequencer/events.ts b/packages/pds/src/sequencer/events.ts index 2d4e21767b5..29f9aef13f3 100644 --- a/packages/pds/src/sequencer/events.ts +++ b/packages/pds/src/sequencer/events.ts @@ -34,6 +34,7 @@ export const sequenceEvt = async (dbTxn: Database, evt: RepoSeqInsert) => { .set({ seq: res.id }) .where('id', '=', res.id) .execute() + await dbTxn.notify('outgoing_repo_seq') } } diff --git a/packages/pds/src/sequencer/sequencer-leader.ts b/packages/pds/src/sequencer/sequencer-leader.ts index 9531b3ffce9..3cf68f609a9 100644 --- a/packages/pds/src/sequencer/sequencer-leader.ts +++ b/packages/pds/src/sequencer/sequencer-leader.ts @@ -119,9 +119,6 @@ export class SequencerLeader { } async isCaughtUp(): Promise { - if (this.db.dialect === 'sqlite') { - return true - } const unsequenced = await this.getUnsequenced() return unsequenced.length === 0 } diff --git a/packages/pds/src/well-known.ts b/packages/pds/src/well-known.ts index c8a340dbd89..cc19434e42f 100644 --- a/packages/pds/src/well-known.ts +++ b/packages/pds/src/well-known.ts @@ -6,7 +6,7 @@ export const createRouter = (ctx: AppContext): express.Router => { router.get('/.well-known/atproto-did', async function (req, res) { const handle = req.hostname - const supportedHandle = ctx.cfg.identity.handleDomains.some( + const supportedHandle = ctx.cfg.identity.serviceHandleDomains.some( (host) => handle.endsWith(host) || handle === host.slice(1), ) if (!supportedHandle) { diff --git a/packages/pds/tests/_util.ts b/packages/pds/tests/_util.ts index cb497296980..84120e93568 100644 --- a/packages/pds/tests/_util.ts +++ b/packages/pds/tests/_util.ts @@ -71,7 +71,7 @@ export const runTestServer = async ( blobstoreDiskLocation: blobstoreLoc, recoveryDidKey: recoveryKey, didPlcUrl: plcUrl, - handleDomains: ['.test'], + serviceHandleDomains: ['.test'], sequencerLeaderLockId: uniqueLockId(), repoSigningKeyK256PrivateKeyHex: repoSigningPriv, plcRotationKeyK256PrivateKeyHex: plcRotationPriv, From 4a5a7a49094e910e08173b1dafaf8d597b88d522 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 13 Jun 2023 15:14:05 -0500 Subject: [PATCH 042/105] fix dev env build --- packages/dev-env/src/bin.ts | 2 +- packages/dev-env/src/mock/index.ts | 49 +++++++++++----------- packages/dev-env/src/network-no-appview.ts | 5 ++- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/packages/dev-env/src/bin.ts b/packages/dev-env/src/bin.ts index 3028f6fbcc9..b9581a2cf5b 100644 --- a/packages/dev-env/src/bin.ts +++ b/packages/dev-env/src/bin.ts @@ -13,7 +13,7 @@ const run = async () => { [ created by Bluesky ]`) const network = await TestNetworkNoAppView.create({ - pds: { port: 2583, publicUrl: 'http://localhost:2583' }, + pds: { port: 2583, hostname: 'localhost' }, plc: { port: 2582 }, }) await generateMockSetup(network) diff --git a/packages/dev-env/src/mock/index.ts b/packages/dev-env/src/mock/index.ts index a056d6c916e..ee99ae0a839 100644 --- a/packages/dev-env/src/mock/index.ts +++ b/packages/dev-env/src/mock/index.ts @@ -186,30 +186,31 @@ export async function generateMockSetup(env: TestNetworkNoAppView) { }, ) - const ctx = env.pds.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() - } + // @TODO get from appview instead + // const ctx = env.pds.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() + // } // a set of replies for (let i = 0; i < 100; i++) { diff --git a/packages/dev-env/src/network-no-appview.ts b/packages/dev-env/src/network-no-appview.ts index f5244eff2db..b99faea3759 100644 --- a/packages/dev-env/src/network-no-appview.ts +++ b/packages/dev-env/src/network-no-appview.ts @@ -15,12 +15,15 @@ export class TestNetworkNoAppView { const dbPostgresUrl = params.dbPostgresUrl || process.env.DB_POSTGRES_URL const dbPostgresSchema = params.dbPostgresSchema || process.env.DB_POSTGRES_SCHEMA + const dbSqliteLocation = + dbPostgresUrl === undefined ? ':memory:' : undefined const plc = await TestPlc.create(params.plc ?? {}) const pds = await TestPds.create({ dbPostgresUrl, dbPostgresSchema, - plcUrl: plc.url, + dbSqliteLocation, + didPlcUrl: plc.url, ...params.pds, }) From 61b77bc1c47d131181a7a743d48610262f9a0110 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Tue, 13 Jun 2023 16:18:19 -0400 Subject: [PATCH 043/105] update pds service entrypoint --- packages/pds/service/index.js | 84 +++++++++-------------------------- 1 file changed, 22 insertions(+), 62 deletions(-) diff --git a/packages/pds/service/index.js b/packages/pds/service/index.js index acae70d3dd0..773d311d9e6 100644 --- a/packages/pds/service/index.js +++ b/packages/pds/service/index.js @@ -11,33 +11,30 @@ require('dd-trace/init') // Only works with commonjs // Tracer code above must come before anything else const path = require('path') -const { KmsKeypair, S3BlobStore } = require('@atproto/aws') -const { Database, ServerConfig, PDS, DiskBlobStore } = require('@atproto/pds') -const { Secp256k1Keypair } = require('@atproto/crypto') +const { + PDS, + Database, + envToCfg, + envToSecrets, + readEnv, +} = require('@atproto/pds') const main = async () => { - const env = ServerConfig.getEnv() - const config = new ServerConfig(env) - - // Migrate using credentialed user - const migrateDb = getMigrationDb(config) - await migrateDb.migrateToLatestOrThrow() - await migrateDb.close() - - // @TODO config for keys - const repoSigningKey = await Secp256k1Keypair.import(env.repoSigningKey) - const plcRotationKey = await KmsKeypair.load({ - keyId: env.plcRotationKeyId, - }) - - const pds = PDS.create({ - config, - db: getDb(config), - blobstore: getBlobstore(config), - repoSigningKey, - plcRotationKey, - }) - + const env = readEnv() + const cfg = envToCfg(env) + const secrets = envToSecrets(env) + const pds = await PDS.create(cfg, secrets) + if (cfg.db.dialect === 'pg') { + // Migrate using credentialed user + const migrateDb = Database.postgres({ + url: cfg.db.migrationUrl, + schema: cfg.db.schema, + }) + await migrateDb.migrateToLatestOrThrow() + await migrateDb.close() + } else { + await pds.ctx.db.migrateToLatestOrThrow() + } await pds.start() // Graceful shutdown (see also https://aws.amazon.com/blogs/containers/graceful-shutdowns-with-ecs/) process.on('SIGTERM', async () => { @@ -45,43 +42,6 @@ const main = async () => { }) } -const getDb = (config) => { - if (config.db.dialect === 'pg') { - return Database.postgres({ - url: config.db.url, - schema: config.db.schema, - poolSize: config.db.poolSize, - poolMaxUses: config.db.poolMaxUses, - poolIdleTimeoutMs: config.db.poolIdleTimeoutMs, - }) - } else { - return Database.sqlite(config.db.location) - } -} - -const getMigrationDb = (config) => { - if (config.db.dialect === 'pg') { - return Database.postgres({ - url: config.db.migrationUrl, - schema: config.db.schema, - }) - } else { - return Database.sqlite(config.db.location) - } -} - -const getBlobstore = (config) => { - if (config.blobstore.provider === 's3') { - return new S3BlobStore({ bucket: config.blobstore.bucket }) - } else { - return new DiskBlobStore( - config.blobstore.location, - config.blobstore.tempLocation, - config.blobstore.quarantineLocation - ) - } -} - const maintainXrpcResource = (span, req) => { // Show actual xrpc method as resource rather than the route pattern if (span && req.originalUrl?.startsWith('/xrpc/')) { From a569bacf0e3cf5124bc27a78b668db65d334e3f7 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 13 Jun 2023 15:23:58 -0500 Subject: [PATCH 044/105] simple env example --- packages/pds/.env.example | 13 +++++++++++++ packages/pds/src/storage/disk-blobstore.ts | 3 +-- 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 packages/pds/.env.example diff --git a/packages/pds/.env.example b/packages/pds/.env.example new file mode 100644 index 00000000000..2955138110b --- /dev/null +++ b/packages/pds/.env.example @@ -0,0 +1,13 @@ +# see more env options in src/config/env.ts + +LOG_ENABLED=true + +PDS_HOSTNAME="example.com" + +PDS_DB_SQLITE_LOCATION="db.test" +# PDS_DB_POSTGRES_URL="postgresql://pg:password@localhost:5433/postgres" + +PDS_BLOBSTORE_DISK_LOCATION="blobs" + +PDS_REPO_SIGNING_KEY_K256_PRIVATE_KEY_HEX="3ee68..." +PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX="e049f..." \ No newline at end of file diff --git a/packages/pds/src/storage/disk-blobstore.ts b/packages/pds/src/storage/disk-blobstore.ts index fe826bf273e..f1407ed74eb 100644 --- a/packages/pds/src/storage/disk-blobstore.ts +++ b/packages/pds/src/storage/disk-blobstore.ts @@ -31,8 +31,7 @@ export class DiskBlobStore implements BlobStore { quarantineLocation?: string, ): Promise { const tmp = tmpLocation || path.join(os.tmpdir(), 'atproto/blobs') - const quarantine = - quarantineLocation || path.join(os.tmpdir(), 'atproto/blobs/quarantine') + const quarantine = quarantineLocation || path.join(location, 'quarantine') await Promise.all([ fs.mkdir(location, { recursive: true }), fs.mkdir(tmp, { recursive: true }), From e555345619190ef630bb2d5606b45064a50661cc Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Tue, 13 Jun 2023 16:47:18 -0400 Subject: [PATCH 045/105] make takedown ids opaque identifiers in the pds --- .../db/migrations/20230613T164932261Z-init.ts | 17 +++-------------- packages/pds/src/db/tables/record.ts | 3 ++- packages/pds/src/db/tables/repo-blob.ts | 3 ++- packages/pds/src/db/tables/repo-root.ts | 3 ++- packages/pds/src/db/util.ts | 2 +- packages/pds/src/services/moderation/index.ts | 6 +++--- packages/pds/src/services/moderation/views.ts | 2 +- packages/pds/src/services/record/index.ts | 2 +- 8 files changed, 15 insertions(+), 23 deletions(-) diff --git a/packages/pds/src/db/migrations/20230613T164932261Z-init.ts b/packages/pds/src/db/migrations/20230613T164932261Z-init.ts index c7ebd104d8b..a150ff06cf9 100644 --- a/packages/pds/src/db/migrations/20230613T164932261Z-init.ts +++ b/packages/pds/src/db/migrations/20230613T164932261Z-init.ts @@ -46,7 +46,6 @@ export async function up(db: Kysely, dialect: Dialect): Promise { .columns(['path', 'linkToDid']) .execute() - // @NOTE renamed pkey constraint await db.schema .createTable('blob') .addColumn('creator', 'varchar', (col) => col.notNull()) @@ -74,7 +73,6 @@ export async function up(db: Kysely, dialect: Dialect): Promise { .addColumn('updatedAt', 'bigint', (col) => col.notNull()) .execute() - // @NOTE dropped handle trigram index await db.schema .createTable('did_handle') .addColumn('did', 'varchar', (col) => col.primaryKey()) @@ -105,7 +103,6 @@ export async function up(db: Kysely, dialect: Dialect): Promise { .addPrimaryKeyConstraint(`invite_code_use_pkey`, ['code', 'usedBy']) .execute() - // @NOTE renamed pkey await db.schema .createTable('ipld_block') .addColumn('creator', 'varchar', (col) => col.notNull()) @@ -193,7 +190,6 @@ export async function up(db: Kysely, dialect: Dialect): Promise { .column('actionId') .execute() - // @NOTE renamed indexes for consistency await db.schema .createTable('record') .addColumn('uri', 'varchar', (col) => col.primaryKey()) @@ -202,9 +198,7 @@ export async function up(db: Kysely, dialect: Dialect): Promise { .addColumn('collection', 'varchar', (col) => col.notNull()) .addColumn('rkey', 'varchar', (col) => col.notNull()) .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .addColumn('takedownId', 'integer', (col) => - col.references('moderation_action.id'), - ) + .addColumn('takedownId', 'varchar') .execute() await db.schema .createIndex('record_did_cid_idx') @@ -237,9 +231,7 @@ export async function up(db: Kysely, dialect: Dialect): Promise { .addColumn('recordUri', 'varchar', (col) => col.notNull()) .addColumn('commit', 'varchar', (col) => col.notNull()) .addColumn('did', 'varchar', (col) => col.notNull()) - .addColumn('takedownId', 'integer', (col) => - col.references('moderation_action.id'), - ) + .addColumn('takedownId', 'varchar') .addPrimaryKeyConstraint(`repo_blob_pkey`, ['cid', 'recordUri']) .execute() await db.schema // supports rebase @@ -273,9 +265,7 @@ export async function up(db: Kysely, dialect: Dialect): Promise { .addColumn('did', 'varchar', (col) => col.primaryKey()) .addColumn('root', 'varchar', (col) => col.notNull()) .addColumn('indexedAt', 'varchar', (col) => col.notNull()) - .addColumn('takedownId', 'integer', (col) => - col.references('moderation_action.id'), - ) + .addColumn('takedownId', 'varchar') .execute() // @TODO renamed indexes for consistency @@ -315,7 +305,6 @@ export async function up(db: Kysely, dialect: Dialect): Promise { .column('sequencedAt') .execute() - // @NOTE removed unique constraint on email, since we have one on lower(email) await db.schema .createTable('user_account') .addColumn('did', 'varchar', (col) => col.primaryKey()) diff --git a/packages/pds/src/db/tables/record.ts b/packages/pds/src/db/tables/record.ts index 105932276a0..6ce60928be3 100644 --- a/packages/pds/src/db/tables/record.ts +++ b/packages/pds/src/db/tables/record.ts @@ -6,7 +6,8 @@ export interface Record { collection: string rkey: string indexedAt: string - takedownId: number | null + // opaque identifier, though currently tends to reference a moderation_action + takedownId: string | null } export const tableName = 'record' diff --git a/packages/pds/src/db/tables/repo-blob.ts b/packages/pds/src/db/tables/repo-blob.ts index 5d7fd87b41d..2cfe40fb84d 100644 --- a/packages/pds/src/db/tables/repo-blob.ts +++ b/packages/pds/src/db/tables/repo-blob.ts @@ -3,7 +3,8 @@ export interface RepoBlob { recordUri: string commit: string did: string - takedownId: number | null + // opaque identifier, though currently tends to reference a moderation_action + takedownId: string | null } export const tableName = 'repo_blob' diff --git a/packages/pds/src/db/tables/repo-root.ts b/packages/pds/src/db/tables/repo-root.ts index 723a171308c..c6b0ff1d76b 100644 --- a/packages/pds/src/db/tables/repo-root.ts +++ b/packages/pds/src/db/tables/repo-root.ts @@ -3,7 +3,8 @@ export interface RepoRoot { did: string root: string indexedAt: string - takedownId: number | null + // opaque identifier, though currently tends to reference a moderation_action + takedownId: string | null } export const tableName = 'repo_root' diff --git a/packages/pds/src/db/util.ts b/packages/pds/src/db/util.ts index 8b5b9677515..3ba0c8509e2 100644 --- a/packages/pds/src/db/util.ts +++ b/packages/pds/src/db/util.ts @@ -22,7 +22,7 @@ export const notSoftDeletedClause = (alias: DbRef) => { return sql`${alias}."takedownId" is null` } -export const softDeleted = (repoOrRecord: { takedownId: number | null }) => { +export const softDeleted = (repoOrRecord: { takedownId: string | null }) => { return repoOrRecord.takedownId !== null } diff --git a/packages/pds/src/services/moderation/index.ts b/packages/pds/src/services/moderation/index.ts index 89044990e01..d1342a0058e 100644 --- a/packages/pds/src/services/moderation/index.ts +++ b/packages/pds/src/services/moderation/index.ts @@ -342,7 +342,7 @@ export class ModerationService { async takedownRepo(info: { takedownId: number; did: string }) { await this.db.db .updateTable('repo_root') - .set({ takedownId: info.takedownId }) + .set({ takedownId: String(info.takedownId) }) .where('did', '=', info.did) .where('takedownId', 'is', null) .executeTakeFirst() @@ -364,14 +364,14 @@ export class ModerationService { this.db.assertTransaction() await this.db.db .updateTable('record') - .set({ takedownId: info.takedownId }) + .set({ takedownId: String(info.takedownId) }) .where('uri', '=', info.uri.toString()) .where('takedownId', 'is', null) .executeTakeFirst() if (info.blobCids?.length) { await this.db.db .updateTable('repo_blob') - .set({ takedownId: info.takedownId }) + .set({ takedownId: String(info.takedownId) }) .where('recordUri', '=', info.uri.toString()) .where( 'cid', diff --git a/packages/pds/src/services/moderation/views.ts b/packages/pds/src/services/moderation/views.ts index d263b9b77a5..e1da1383dd1 100644 --- a/packages/pds/src/services/moderation/views.ts +++ b/packages/pds/src/services/moderation/views.ts @@ -565,7 +565,7 @@ type RecordResult = { cid: string value: object indexedAt: string - takedownId: number | null + takedownId: string | null } type SubjectResult = Pick< diff --git a/packages/pds/src/services/record/index.ts b/packages/pds/src/services/record/index.ts index 4d6b901a1a8..114def95e5c 100644 --- a/packages/pds/src/services/record/index.ts +++ b/packages/pds/src/services/record/index.ts @@ -160,7 +160,7 @@ export class RecordService { cid: string value: object indexedAt: string - takedownId: number | null + takedownId: string | null } | null> { const { ref } = this.db.db.dynamic let builder = this.db.db From 397b10b74a35f13c3ef7f55cf305d1ee75132804 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 13 Jun 2023 15:51:39 -0500 Subject: [PATCH 046/105] use pds routes for api tests --- packages/api/tests/agent.test.ts | 19 +++++++++++--- packages/api/tests/bsky-agent.test.ts | 38 ++++++++++++++++++--------- 2 files changed, 41 insertions(+), 16 deletions(-) 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 4876bc5089e..c65abeb6013 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 }) @@ -29,8 +43,8 @@ describe('agent', () => { password: 'password', }) - const profile1 = await agent.getProfile({ actor: agent.session?.did || '' }) - expect(profile1.data.displayName).toBeFalsy() + const displayName1 = await await getProfileDisplayName(agent) + expect(displayName1).toBeFalsy() await agent.upsertProfile((existing) => { expect(existing).toBeFalsy() @@ -39,8 +53,8 @@ describe('agent', () => { } }) - const profile2 = await agent.getProfile({ actor: agent.session?.did || '' }) - expect(profile2.data.displayName).toBe('Bob') + const displayName2 = await await getProfileDisplayName(agent) + expect(displayName2).toBe('Bob') await agent.upsertProfile((existing) => { expect(existing).toBeTruthy() @@ -49,8 +63,8 @@ describe('agent', () => { } }) - const profile3 = await agent.getProfile({ actor: agent.session?.did || '' }) - expect(profile3.data.displayName).toBe('BOB') + const displayName3 = await await getProfileDisplayName(agent) + expect(displayName3).toBe('BOB') }) it('upsertProfile correctly handles CAS failures.', async () => { @@ -62,8 +76,8 @@ describe('agent', () => { password: 'password', }) - const profile1 = await agent.getProfile({ actor: agent.session?.did || '' }) - expect(profile1.data.displayName).toBeFalsy() + const displayName1 = await await getProfileDisplayName(agent) + expect(displayName1).toBeFalsy() let hasConflicted = false let ranTwice = false @@ -88,8 +102,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 await getProfileDisplayName(agent) + expect(displayName2).toBe('Bob') }) it('upsertProfile wont endlessly retry CAS failures.', async () => { @@ -101,8 +115,8 @@ describe('agent', () => { password: 'password', }) - const profile1 = await agent.getProfile({ actor: agent.session?.did || '' }) - expect(profile1.data.displayName).toBeFalsy() + const displayName1 = await await getProfileDisplayName(agent) + expect(displayName1).toBeFalsy() const p = agent.upsertProfile(async (existing) => { await agent.com.atproto.repo.putRecord({ From 68634170d65ce2eeb759afff1034bce94fa95ae2 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Wed, 14 Jun 2023 11:05:46 -0400 Subject: [PATCH 047/105] update pds dockerfile with volume and correct port env var --- packages/pds/Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/pds/Dockerfile b/packages/pds/Dockerfile index c3340350669..e93b3e0e031 100644 --- a/packages/pds/Dockerfile +++ b/packages/pds/Dockerfile @@ -39,9 +39,11 @@ ENTRYPOINT ["dumb-init", "--"] WORKDIR /app/packages/pds/service COPY --from=build /app /app +RUN mkdir /app/data && chown node /app/data +VOLUME /app/data EXPOSE 3000 -ENV PORT=3000 +ENV PDS_PORT=3000 ENV NODE_ENV=production # https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#non-root-user From 09095c8508a664e957f3aa397fae78f40ffa281b Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 14 Jun 2023 10:14:50 -0500 Subject: [PATCH 048/105] add a couple env vars to example --- packages/pds/.env.example | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/pds/.env.example b/packages/pds/.env.example index 2955138110b..21da3c4e74a 100644 --- a/packages/pds/.env.example +++ b/packages/pds/.env.example @@ -10,4 +10,7 @@ PDS_DB_SQLITE_LOCATION="db.test" PDS_BLOBSTORE_DISK_LOCATION="blobs" PDS_REPO_SIGNING_KEY_K256_PRIVATE_KEY_HEX="3ee68..." -PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX="e049f..." \ No newline at end of file +PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX="e049f..." + +PDS_JWT_SECRET="jwt-secret" +PDS_ADMIN_PASSWORD="admin-pass" From 1077d87215375ab82e8262b0539269ea738a88da Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 14 Jun 2023 14:14:00 -0500 Subject: [PATCH 049/105] add comments to env example --- packages/pds/.env.example | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/pds/.env.example b/packages/pds/.env.example index 21da3c4e74a..fc58abb32ed 100644 --- a/packages/pds/.env.example +++ b/packages/pds/.env.example @@ -1,16 +1,29 @@ -# see more env options in src/config/env.ts +# See more env options in src/config/env.ts +# Logging +# Outputs to stdout LOG_ENABLED=true +# Hostname - the public domain that you intend to deploy your service at PDS_HOSTNAME="example.com" +# Database config - use one or the other PDS_DB_SQLITE_LOCATION="db.test" # PDS_DB_POSTGRES_URL="postgresql://pg:password@localhost:5433/postgres" +# Blobstore - filesystem location to store uploaded blobs PDS_BLOBSTORE_DISK_LOCATION="blobs" +# Private keys - these are each expected to be 64 char hex strings (256 bit) PDS_REPO_SIGNING_KEY_K256_PRIVATE_KEY_HEX="3ee68..." PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX="e049f..." +# Secrets - update to secure high-entropy strings PDS_JWT_SECRET="jwt-secret" PDS_ADMIN_PASSWORD="admin-pass" + +# Environment - example is for sandbox +PDS_DID_PLC_URL="plc.bsky-sandbox.dev" +PDS_BSKY_APP_VIEW_ENDPOINT="api.bsky-sandbox.dev" +PDS_BSKY_APP_VIEW_DID="" +PDS_CRAWLERS="bgs.bsky-sandbox.dev" \ No newline at end of file From 8646d8d71fb5fa7c36c9ec98d14f2f75b60ba9bd Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Wed, 14 Jun 2023 15:18:10 -0400 Subject: [PATCH 050/105] @atproto/pds 0.2.0-beta.0 --- packages/pds/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/package.json b/packages/pds/package.json index e319a8daded..1b4a2f7d9f5 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/pds", - "version": "0.1.11", + "version": "0.2.0-beta.0", "license": "MIT", "repository": { "type": "git", From 9c81f682070114e1662e313fab22de4d9ae86a54 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Wed, 14 Jun 2023 15:38:51 -0400 Subject: [PATCH 051/105] @atproto/aws 0.0.1-beta.0 --- packages/aws/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws/package.json b/packages/aws/package.json index 8fdc5baf67d..660bbf56605 100644 --- a/packages/aws/package.json +++ b/packages/aws/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/aws", - "version": "0.0.1", + "version": "0.0.1-beta.0", "main": "src/index.ts", "license": "MIT", "repository": { From e4da9515860e8747bbea90bae4743d214bc5402a Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 14 Jun 2023 14:43:13 -0500 Subject: [PATCH 052/105] appview did --- packages/pds/.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/.env.example b/packages/pds/.env.example index fc58abb32ed..37253fc8a43 100644 --- a/packages/pds/.env.example +++ b/packages/pds/.env.example @@ -25,5 +25,5 @@ PDS_ADMIN_PASSWORD="admin-pass" # Environment - example is for sandbox PDS_DID_PLC_URL="plc.bsky-sandbox.dev" PDS_BSKY_APP_VIEW_ENDPOINT="api.bsky-sandbox.dev" -PDS_BSKY_APP_VIEW_DID="" +PDS_BSKY_APP_VIEW_DID="did:web:api.bsky-sandbox.dev" PDS_CRAWLERS="bgs.bsky-sandbox.dev" \ No newline at end of file From 89c412a304c3bb7d86849325005c15bc9ba65e4e Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Wed, 14 Jun 2023 15:58:43 -0400 Subject: [PATCH 053/105] @atproto/aws 0.0.1 --- packages/aws/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws/package.json b/packages/aws/package.json index 660bbf56605..8fdc5baf67d 100644 --- a/packages/aws/package.json +++ b/packages/aws/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/aws", - "version": "0.0.1-beta.0", + "version": "0.0.1", "main": "src/index.ts", "license": "MIT", "repository": { From 2d4fa3a03f120159a693f96d96c462f0c952af53 Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 14 Jun 2023 16:47:57 -0500 Subject: [PATCH 054/105] enable logs by default --- package.json | 4 ++-- packages/common/src/logger.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index fff908f3f59..81b5bca8ed8 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "verify": "lerna run verify --stream", "prettier": "lerna run prettier", "build": "lerna run build", - "test": "NODE_ENV=development lerna run test --stream", - "test:withFlags": "NODE_ENV=development lerna run test --stream --" + "test": "LOG_ENABLED=false NODE_ENV=development lerna run test --stream", + "test:withFlags": "LOG_ENABLED=false NODE_ENV=development lerna run test --stream --" }, "devDependencies": { "@babel/core": "^7.18.6", diff --git a/packages/common/src/logger.ts b/packages/common/src/logger.ts index 857d32ee9f2..a302c6ed3c5 100644 --- a/packages/common/src/logger.ts +++ b/packages/common/src/logger.ts @@ -7,7 +7,7 @@ const enabledSystems = (process.env.LOG_SYSTEMS || '') const enabledEnv = process.env.LOG_ENABLED const enabled = - enabledEnv === 'true' || enabledEnv === 't' || enabledEnv === '1' + enabledEnv !== 'false' && enabledEnv !== 'f' && enabledEnv !== '0' const level = process.env.LOG_LEVEL || 'info' From 2c604c2cbc51de0ee873089a3ce8a8ed7acc6937 Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 14 Jun 2023 16:49:25 -0500 Subject: [PATCH 055/105] update env example --- packages/pds/.env.example | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/pds/.env.example b/packages/pds/.env.example index 37253fc8a43..fc3c3520eb0 100644 --- a/packages/pds/.env.example +++ b/packages/pds/.env.example @@ -1,9 +1,4 @@ # See more env options in src/config/env.ts - -# Logging -# Outputs to stdout -LOG_ENABLED=true - # Hostname - the public domain that you intend to deploy your service at PDS_HOSTNAME="example.com" @@ -23,7 +18,7 @@ PDS_JWT_SECRET="jwt-secret" PDS_ADMIN_PASSWORD="admin-pass" # Environment - example is for sandbox -PDS_DID_PLC_URL="plc.bsky-sandbox.dev" -PDS_BSKY_APP_VIEW_ENDPOINT="api.bsky-sandbox.dev" +PDS_DID_PLC_URL="https://plc.bsky-sandbox.dev" +PDS_BSKY_APP_VIEW_ENDPOINT="https://api.bsky-sandbox.dev" PDS_BSKY_APP_VIEW_DID="did:web:api.bsky-sandbox.dev" -PDS_CRAWLERS="bgs.bsky-sandbox.dev" \ No newline at end of file +PDS_CRAWLERS="https://bgs.bsky-sandbox.dev" \ No newline at end of file From a109e140134ac416cc7dd2f5e62b96c7c185df0f Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 14 Jun 2023 19:48:33 -0500 Subject: [PATCH 056/105] bugfixing sandbox issues --- .../workflows/build-and-push-bsky-aws.yaml | 2 +- .../workflows/build-and-push-bsky-ghcr.yaml | 1 + .../src/api/com/atproto/repo/getRecord.ts | 38 +++++++++++++++++++ packages/bsky/src/api/index.ts | 6 ++- .../pds/src/api/com/atproto/repo/getRecord.ts | 32 +++++++++------- packages/pds/src/crawlers.ts | 2 +- 6 files changed, 63 insertions(+), 18 deletions(-) create mode 100644 packages/bsky/src/api/com/atproto/repo/getRecord.ts diff --git a/.github/workflows/build-and-push-bsky-aws.yaml b/.github/workflows/build-and-push-bsky-aws.yaml index cd8e694e959..88904771f62 100644 --- a/.github/workflows/build-and-push-bsky-aws.yaml +++ b/.github/workflows/build-and-push-bsky-aws.yaml @@ -3,7 +3,7 @@ on: push: branches: - main - - bsky-app-view + - simplify-pds env: REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }} diff --git a/.github/workflows/build-and-push-bsky-ghcr.yaml b/.github/workflows/build-and-push-bsky-ghcr.yaml index 26f46e8c4db..d9338f67f1b 100644 --- a/.github/workflows/build-and-push-bsky-ghcr.yaml +++ b/.github/workflows/build-and-push-bsky-ghcr.yaml @@ -3,6 +3,7 @@ on: push: branches: - main + - simplify-pds env: REGISTRY: ghcr.io USERNAME: ${{ github.actor }} diff --git a/packages/bsky/src/api/com/atproto/repo/getRecord.ts b/packages/bsky/src/api/com/atproto/repo/getRecord.ts new file mode 100644 index 00000000000..9903d171b7f --- /dev/null +++ b/packages/bsky/src/api/com/atproto/repo/getRecord.ts @@ -0,0 +1,38 @@ +import { InvalidRequestError } from '@atproto/xrpc-server' +import { AtUri } from '@atproto/uri' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { jsonStringToLex } from '@atproto/lexicon' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.repo.getRecord(async ({ params }) => { + const { repo, collection, rkey, cid } = params + const did = await ctx.services.actor(ctx.db).getActorDid(repo) + if (!did) { + throw new InvalidRequestError(`Could not find repo: ${repo}`) + } + + const uri = AtUri.make(did, collection, rkey) + + let builder = ctx.db.db + .selectFrom('record') + .selectAll() + .where('uri', '=', uri.toString()) + if (cid) { + builder = builder.where('cid', '=', cid) + } + + const record = await builder.executeTakeFirst() + if (!record) { + throw new InvalidRequestError(`Could not locate record: ${uri}`) + } + return { + encoding: 'application/json', + body: { + uri: record.uri, + cid: record.cid, + value: jsonStringToLex(record.json) as Record, + }, + } + }) +} diff --git a/packages/bsky/src/api/index.ts b/packages/bsky/src/api/index.ts index 1a53162bae1..c56765c8d56 100644 --- a/packages/bsky/src/api/index.ts +++ b/packages/bsky/src/api/index.ts @@ -36,13 +36,14 @@ import resolveModerationReports from './com/atproto/admin/resolveModerationRepor import reverseModerationAction from './com/atproto/admin/reverseModerationAction' import takeModerationAction from './com/atproto/admin/takeModerationAction' import searchRepos from './com/atproto/admin/searchRepos' -import getRecord from './com/atproto/admin/getRecord' +import adminGetRecord from './com/atproto/admin/getRecord' import getRepo from './com/atproto/admin/getRepo' import getModerationAction from './com/atproto/admin/getModerationAction' import getModerationActions from './com/atproto/admin/getModerationActions' import getModerationReport from './com/atproto/admin/getModerationReport' import getModerationReports from './com/atproto/admin/getModerationReports' import resolveHandle from './com/atproto/identity/resolveHandle' +import getRecord from './com/atproto/repo/getRecord' export * as health from './health' @@ -87,12 +88,13 @@ export default function (server: Server, ctx: AppContext) { reverseModerationAction(server, ctx) takeModerationAction(server, ctx) searchRepos(server, ctx) - getRecord(server, ctx) + adminGetRecord(server, ctx) getRepo(server, ctx) getModerationAction(server, ctx) getModerationActions(server, ctx) getModerationReport(server, ctx) getModerationReports(server, ctx) resolveHandle(server, ctx) + getRecord(server, ctx) return server } diff --git a/packages/pds/src/api/com/atproto/repo/getRecord.ts b/packages/pds/src/api/com/atproto/repo/getRecord.ts index 45aada2bbf7..f62e64e16c0 100644 --- a/packages/pds/src/api/com/atproto/repo/getRecord.ts +++ b/packages/pds/src/api/com/atproto/repo/getRecord.ts @@ -1,4 +1,3 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' import { AtUri } from '@atproto/uri' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' @@ -8,23 +7,28 @@ export default function (server: Server, ctx: AppContext) { const { repo, collection, rkey, cid } = params const did = await ctx.services.account(ctx.db).getDidForActor(repo) - if (!did) { - throw new InvalidRequestError(`Could not find repo: ${repo}`) + // fetch from pds if available, if not then fetch from appview + if (did) { + const uri = AtUri.make(did, collection, rkey) + 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, + }, + } + } } - const uri = AtUri.make(did, collection, rkey) - - const record = await ctx.services.record(ctx.db).getRecord(uri, cid || null) - if (!record) { - throw new InvalidRequestError(`Could not locate record: ${uri}`) - } + const res = await ctx.appViewAgent.api.com.atproto.repo.getRecord(params) return { encoding: 'application/json', - body: { - uri: record.uri, - cid: record.cid, - value: record.value, - }, + body: res.data, } }) } diff --git a/packages/pds/src/crawlers.ts b/packages/pds/src/crawlers.ts index 193aa284941..5fd855217c1 100644 --- a/packages/pds/src/crawlers.ts +++ b/packages/pds/src/crawlers.ts @@ -21,7 +21,7 @@ export class Crawlers { await Promise.all( this.agents.map(async (agent) => { try { - await agent.api.com.atproto.sync.notifyOfUpdate({ + await agent.api.com.atproto.sync.requestCrawl({ hostname: this.hostname, }) } catch (err) { From 78effb332350685fb493785136c44cb554bcc1eb Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Wed, 14 Jun 2023 18:33:22 -0400 Subject: [PATCH 057/105] consistency in pds env var name for appview url --- packages/dev-env/src/network.ts | 2 +- packages/pds/src/config/config.ts | 4 ++-- packages/pds/src/config/env.ts | 4 ++-- packages/pds/src/context.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/dev-env/src/network.ts b/packages/dev-env/src/network.ts index d2422d9abd0..3fc8351f466 100644 --- a/packages/dev-env/src/network.ts +++ b/packages/dev-env/src/network.ts @@ -43,7 +43,7 @@ export class TestNetwork extends TestNetworkNoAppView { dbPostgresUrl, dbPostgresSchema, didPlcUrl: plc.url, - bskyAppViewEndpoint: bsky.url, + bskyAppViewUrl: bsky.url, bskyAppViewDid: bsky.ctx.cfg.serverDid, ...params.pds, }) diff --git a/packages/pds/src/config/config.ts b/packages/pds/src/config/config.ts index d6d2ea2cafe..ffd5501bd84 100644 --- a/packages/pds/src/config/config.ts +++ b/packages/pds/src/config/config.ts @@ -121,7 +121,7 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { } const bskyAppViewCfg: ServerConfig['bskyAppView'] = { - endpoint: env.bskyAppViewEndpoint ?? 'https://api.bsky-sandbox.dev', + url: env.bskyAppViewUrl ?? 'https://api.bsky-sandbox.dev', did: env.bskyAppViewDid ?? 'did:plc:abc', // get real did } @@ -222,6 +222,6 @@ export type SubscriptionConfig = { } export type BksyAppViewConfig = { - endpoint: string + url: string did: string } diff --git a/packages/pds/src/config/env.ts b/packages/pds/src/config/env.ts index 635dfd41059..40d783935e0 100644 --- a/packages/pds/src/config/env.ts +++ b/packages/pds/src/config/env.ts @@ -54,7 +54,7 @@ export const readEnv = (): ServerEnvironment => { sequencerLeaderLockId: envInt(process.env.PDS_SEQUENCER_LEADER_LOCK_ID), // appview - bskyAppViewEndpoint: envStr(process.env.PDS_BSKY_APP_VIEW_ENDPOINT), + bskyAppViewUrl: envStr(process.env.PDS_BSKY_APP_VIEW_URL), bskyAppViewDid: envStr(process.env.PDS_BSKY_APP_VIEW_DID), // crawlers @@ -126,7 +126,7 @@ export type ServerEnvironment = { sequencerLeaderLockId?: number // appview - bskyAppViewEndpoint?: string + bskyAppViewUrl?: string bskyAppViewDid?: string // crawler diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index d9d40240739..672910ea871 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -125,7 +125,7 @@ export class AppContext { const backgroundQueue = new BackgroundQueue(db) const crawlers = new Crawlers(cfg.service.hostname, cfg.crawlers) - const appViewAgent = new AtpAgent({ service: cfg.bskyAppView.endpoint }) + const appViewAgent = new AtpAgent({ service: cfg.bskyAppView.url }) const auth = new ServerAuth({ jwtSecret: secrets.jwtSecret, From 94a4fececbc0f7a8ea5d322e30052fa6a9e5499f Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Wed, 14 Jun 2023 18:48:37 -0400 Subject: [PATCH 058/105] log on pds start and stop, configure version at runtime --- packages/pds/service/index.js | 6 ++++++ packages/pds/src/index.ts | 1 + 2 files changed, 7 insertions(+) diff --git a/packages/pds/service/index.js b/packages/pds/service/index.js index 773d311d9e6..59a71492363 100644 --- a/packages/pds/service/index.js +++ b/packages/pds/service/index.js @@ -17,10 +17,13 @@ const { envToCfg, envToSecrets, readEnv, + httpLogger, } = require('@atproto/pds') +const pkg = require('@atproto/pds/package.json') const main = async () => { const env = readEnv() + env.version ||= pkg.version const cfg = envToCfg(env) const secrets = envToSecrets(env) const pds = await PDS.create(cfg, secrets) @@ -36,9 +39,12 @@ const main = async () => { await pds.ctx.db.migrateToLatestOrThrow() } await pds.start() + httpLogger.info('pds is running') // Graceful shutdown (see also https://aws.amazon.com/blogs/containers/graceful-shutdowns-with-ecs/) process.on('SIGTERM', async () => { + httpLogger.info('pds is stopping') await pds.destroy() + httpLogger.info('pds is stopped') }) } diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index 52834df7b85..bd90c62b70d 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -22,6 +22,7 @@ export * from './config' export { Database } from './db' export { DiskBlobStore, MemoryBlobStore } from './storage' export { AppContext } from './context' +export { httpLogger } from './logger' export class PDS { public ctx: AppContext From ba609d89c2133cdb485a691f44818b4e2715c4f1 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Wed, 14 Jun 2023 18:51:28 -0400 Subject: [PATCH 059/105] @atproto/pds 0.2.0-beta.1 --- packages/pds/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/package.json b/packages/pds/package.json index 1b4a2f7d9f5..e062b6036a9 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/pds", - "version": "0.2.0-beta.0", + "version": "0.2.0-beta.1", "license": "MIT", "repository": { "type": "git", From dc584ef93c207aa9928231df5cb55de5fcaea41d Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Wed, 14 Jun 2023 19:22:06 -0400 Subject: [PATCH 060/105] fix semver matching for pds beta version --- packages/api/package.json | 2 +- packages/bsky/package.json | 2 +- packages/dev-env/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/api/package.json b/packages/api/package.json index c39c8c62875..034f5abecd5 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -29,6 +29,6 @@ }, "devDependencies": { "@atproto/lex-cli": "*", - "@atproto/pds": "*" + "@atproto/pds": "* | >=0.2.0-beta.0" } } diff --git a/packages/bsky/package.json b/packages/bsky/package.json index 1716950c796..c0ab6df0826 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -60,7 +60,7 @@ "@atproto/api": "*", "@atproto/dev-env": "*", "@atproto/lex-cli": "*", - "@atproto/pds": "*", + "@atproto/pds": "* | >=0.2.0-beta.0", "@atproto/xrpc": "*", "@did-plc/server": "^0.0.1", "@types/cors": "^2.8.12", diff --git a/packages/dev-env/package.json b/packages/dev-env/package.json index 4a1caaecdd3..dd539918e98 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -25,7 +25,7 @@ "@atproto/bsky": "*", "@atproto/crypto": "*", "@atproto/identity": "*", - "@atproto/pds": "*", + "@atproto/pds": "* | >=0.2.0-beta.0", "@atproto/uri": "*", "@atproto/xrpc-server": "*", "@did-plc/lib": "^0.0.1", From 5099be5019269bfe32f1c106691ac4d0b26185f4 Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 14 Jun 2023 20:51:42 -0500 Subject: [PATCH 061/105] v0.2.0-beta.2 --- packages/pds/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/package.json b/packages/pds/package.json index e062b6036a9..04d4c65d588 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/pds", - "version": "0.2.0-beta.1", + "version": "0.2.0-beta.2", "license": "MIT", "repository": { "type": "git", From 719dfd622d1c8f0e2b20827c6e88072f88f7b4ac Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 14 Jun 2023 21:21:00 -0500 Subject: [PATCH 062/105] default invites to being not required --- packages/dev-env/src/pds.ts | 1 + packages/pds/src/config/config.ts | 8 ++++---- packages/pds/tests/_util.ts | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index bd668950e1e..fbc8a69172c 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -39,6 +39,7 @@ export class TestPds { sequencerLeaderLockId: uniqueLockId(), repoSigningKeyK256PrivateKeyHex: repoSigningPriv, plcRotationKeyK256PrivateKeyHex: plcRotationPriv, + inviteRequired: false, ...config, } const cfg = pds.envToCfg(env) diff --git a/packages/pds/src/config/config.ts b/packages/pds/src/config/config.ts index ffd5501bd84..44827360528 100644 --- a/packages/pds/src/config/config.ts +++ b/packages/pds/src/config/config.ts @@ -92,13 +92,13 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { serviceHandleDomains, } - const invitesCfg: ServerConfig['invites'] = env.inviteRequired + const invitesCfg: ServerConfig['invites'] = !env.inviteRequired ? { - required: true, - interval: env.inviteInterval ?? null, + required: false, } : { - required: false, + required: true, + interval: env.inviteInterval ?? null, } let emailCfg: ServerConfig['email'] diff --git a/packages/pds/tests/_util.ts b/packages/pds/tests/_util.ts index 84120e93568..b3f21fe45ad 100644 --- a/packages/pds/tests/_util.ts +++ b/packages/pds/tests/_util.ts @@ -78,6 +78,7 @@ export const runTestServer = async ( adminPassword: ADMIN_PASSWORD, moderatorPassword: MODERATOR_PASSWORD, jwtSecret: 'jwt-secret', + inviteRequired: false, ...params, } From 999e45a7d4a61ced4a439c50f2585af70651cc51 Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 14 Jun 2023 21:30:32 -0500 Subject: [PATCH 063/105] fix flaky test --- packages/pds/tests/proxied/procedures.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/pds/tests/proxied/procedures.test.ts b/packages/pds/tests/proxied/procedures.test.ts index 4fdf2482dba..74d1ea1704b 100644 --- a/packages/pds/tests/proxied/procedures.test.ts +++ b/packages/pds/tests/proxied/procedures.test.ts @@ -93,6 +93,8 @@ describe('proxies appview procedures', () => { }, sc.getHeaders(carol), ) + await network.processAll() + // mute lists await agent.api.app.bsky.graph.muteActorList( { list: bobList.uri }, From 3e0582a7aca9170558189efcf28b18aba85d446b Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Fri, 16 Jun 2023 16:23:07 -0400 Subject: [PATCH 064/105] limit db connections in tests --- packages/dev-env/src/network-no-appview.ts | 1 + packages/dev-env/src/network.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/dev-env/src/network-no-appview.ts b/packages/dev-env/src/network-no-appview.ts index b99faea3759..45b7eb66edf 100644 --- a/packages/dev-env/src/network-no-appview.ts +++ b/packages/dev-env/src/network-no-appview.ts @@ -22,6 +22,7 @@ export class TestNetworkNoAppView { const pds = await TestPds.create({ dbPostgresUrl, dbPostgresSchema, + dbPostgresPoolSize: 5, dbSqliteLocation, didPlcUrl: plc.url, ...params.pds, diff --git a/packages/dev-env/src/network.ts b/packages/dev-env/src/network.ts index 3fc8351f466..b6550d783ba 100644 --- a/packages/dev-env/src/network.ts +++ b/packages/dev-env/src/network.ts @@ -42,6 +42,7 @@ export class TestNetwork extends TestNetworkNoAppView { port: pdsPort, dbPostgresUrl, dbPostgresSchema, + dbPostgresPoolSize: 5, didPlcUrl: plc.url, bskyAppViewUrl: bsky.url, bskyAppViewDid: bsky.ctx.cfg.serverDid, From 2ffca7ba76e5180473b3e58cc6107738a066a55e Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 20 Jun 2023 12:21:35 -0500 Subject: [PATCH 065/105] publish 0.2.0-beta.d3 --- packages/pds/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/package.json b/packages/pds/package.json index 04d4c65d588..900c4b63b81 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/pds", - "version": "0.2.0-beta.2", + "version": "0.2.0-beta.3", "license": "MIT", "repository": { "type": "git", From a8bd59a0228afee2952e674f2c2e60e8fb420f21 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 20 Jun 2023 13:17:49 -0500 Subject: [PATCH 066/105] fix invite required parsing --- packages/pds/src/config/config.ts | 20 +++++++++++--------- packages/pds/src/config/util.ts | 6 ++++-- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/pds/src/config/config.ts b/packages/pds/src/config/config.ts index eed00070843..c103a418fe7 100644 --- a/packages/pds/src/config/config.ts +++ b/packages/pds/src/config/config.ts @@ -92,15 +92,17 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { serviceHandleDomains, } - const invitesCfg: ServerConfig['invites'] = !env.inviteRequired - ? { - required: false, - } - : { - required: true, - interval: env.inviteInterval ?? null, - epoch: env.inviteEpoch ?? 0, - } + // default to being required if left undefined + const invitesCfg: ServerConfig['invites'] = + env.inviteRequired === false + ? { + required: false, + } + : { + required: true, + interval: env.inviteInterval ?? null, + epoch: env.inviteEpoch ?? 0, + } let emailCfg: ServerConfig['email'] if (!env.emailFromAddress && !env.emailSmtpUrl) { diff --git a/packages/pds/src/config/util.ts b/packages/pds/src/config/util.ts index 9d6cf8ea568..653e84bcf3b 100644 --- a/packages/pds/src/config/util.ts +++ b/packages/pds/src/config/util.ts @@ -9,8 +9,10 @@ export const envStr = (str: string | undefined): string | undefined => { return str } -export const envBool = (str: string | undefined): boolean => { - return str === 'true' || str === '1' +export const envBool = (str: string | undefined): boolean | undefined => { + if (str === 'true' || str === '1') return true + if (str === 'false' || str === '0') return false + return undefined } export const envList = (str: string | undefined): string[] => { From 3752284f4e510a2f73eb8e11bf9d7a8cea776a7c Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Tue, 20 Jun 2023 14:24:17 -0400 Subject: [PATCH 067/105] @atproto/pds 0.2.0-beta.5 --- packages/pds/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/package.json b/packages/pds/package.json index 900c4b63b81..fa08718c99c 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/pds", - "version": "0.2.0-beta.3", + "version": "0.2.0-beta.5", "license": "MIT", "repository": { "type": "git", From c8f1d2e102f181161a556863ada12f8e138c887d Mon Sep 17 00:00:00 2001 From: devin ivy Date: Wed, 28 Jun 2023 23:28:30 -0400 Subject: [PATCH 068/105] Proxy getPopularFeedGenerators on simplified pds (#1222) proxy getPopularFeedGenerators on pds Co-authored-by: dholms --- packages/pds/src/api/app/bsky/proxied.ts | 16 +++++++++++ .../proxied/__snapshots__/views.test.ts.snap | 28 +++++++++++++++++++ packages/pds/tests/proxied/views.test.ts | 24 ++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/packages/pds/src/api/app/bsky/proxied.ts b/packages/pds/src/api/app/bsky/proxied.ts index 744c736e885..09d4ec7a57e 100644 --- a/packages/pds/src/api/app/bsky/proxied.ts +++ b/packages/pds/src/api/app/bsky/proxied.ts @@ -233,6 +233,22 @@ export default function (server: Server, ctx: AppContext) { }, }) + server.app.bsky.unspecced.getPopularFeedGenerators({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + 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, + } + }, + }) + server.app.bsky.graph.getBlocks({ auth: ctx.accessVerifier, handler: async ({ params, auth }) => { diff --git a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap index 85dd466aba8..d47563e2632 100644 --- a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap @@ -2117,3 +2117,31 @@ Object { ], } `; + +exports[`proxies view requests unspecced.getPopularFeedGenerators 1`] = ` +Object { + "feeds": Array [ + Object { + "cid": "cids(0)", + "creator": Object { + "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "description": "Provides all feed candidates", + "did": "did:example:feedgen", + "displayName": "All", + "indexedAt": "1970-01-01T00:00:00.000Z", + "likeCount": 0, + "uri": "record(0)", + "viewer": Object {}, + }, + ], +} +`; diff --git a/packages/pds/tests/proxied/views.test.ts b/packages/pds/tests/proxied/views.test.ts index fec0e13cd20..f45384dc449 100644 --- a/packages/pds/tests/proxied/views.test.ts +++ b/packages/pds/tests/proxied/views.test.ts @@ -28,6 +28,20 @@ describe('proxies view requests', () => { dan = sc.dids.dan }) + beforeAll(async () => { + await agent.api.app.bsky.feed.generator.create( + { repo: alice, rkey: 'all' }, + { + did: 'did:example:feedgen', + displayName: 'All', + description: 'Provides all feed candidates', + createdAt: new Date().toISOString(), + }, + sc.getHeaders(alice), + ) + await network.processAll() + }) + afterAll(async () => { await network.close() }) @@ -274,6 +288,16 @@ describe('proxies view requests', () => { expect([...pt1.data.feed, ...pt2.data.feed]).toEqual(res.data.feed) }) + it('unspecced.getPopularFeedGenerators', async () => { + const res = await agent.api.app.bsky.unspecced.getPopularFeedGenerators( + {}, + { + headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' }, + }, + ) + expect(forSnapshot(res.data)).toMatchSnapshot() + }) + let feedUri: string it('feed.getFeedGenerator', async () => { feedUri = AtUri.make( From 2ecfe879dca8347218808966fbc5b6d652b91c0b Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 13 Sep 2023 20:53:36 -0500 Subject: [PATCH 069/105] tidy migrations --- ...18T170914772Z-sequencer-leader-sequence.ts | 25 --- ...0727T172043676Z-user-account-cursor-idx.ts | 13 -- .../20230801T141349990Z-invite-note.ts | 12 -- ...1Z-feed-item-delete-invite-for-user-idx.ts | 14 -- .../20230808T172813122Z-repo-rev.ts | 15 -- .../20230810T203412859Z-action-duration.ts | 23 --- .../20230818T134357818Z-runtime-flags.ts | 13 -- .../20230825T142507884Z-blob-tempkey-idx.ts | 13 -- ...0230828T153013575Z-repo-history-rewrite.ts | 62 ------- .../migrations/20230914T014727199Z-repo-v3.ts | 165 ++++++++++++++++++ packages/pds/src/db/migrations/index.ts | 10 +- 11 files changed, 166 insertions(+), 199 deletions(-) delete mode 100644 packages/pds/src/db/migrations/20230718T170914772Z-sequencer-leader-sequence.ts delete mode 100644 packages/pds/src/db/migrations/20230727T172043676Z-user-account-cursor-idx.ts delete mode 100644 packages/pds/src/db/migrations/20230801T141349990Z-invite-note.ts delete mode 100644 packages/pds/src/db/migrations/20230807T035309811Z-feed-item-delete-invite-for-user-idx.ts delete mode 100644 packages/pds/src/db/migrations/20230808T172813122Z-repo-rev.ts delete mode 100644 packages/pds/src/db/migrations/20230810T203412859Z-action-duration.ts delete mode 100644 packages/pds/src/db/migrations/20230818T134357818Z-runtime-flags.ts delete mode 100644 packages/pds/src/db/migrations/20230825T142507884Z-blob-tempkey-idx.ts delete mode 100644 packages/pds/src/db/migrations/20230828T153013575Z-repo-history-rewrite.ts create mode 100644 packages/pds/src/db/migrations/20230914T014727199Z-repo-v3.ts diff --git a/packages/pds/src/db/migrations/20230718T170914772Z-sequencer-leader-sequence.ts b/packages/pds/src/db/migrations/20230718T170914772Z-sequencer-leader-sequence.ts deleted file mode 100644 index aae6db339b9..00000000000 --- a/packages/pds/src/db/migrations/20230718T170914772Z-sequencer-leader-sequence.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Kysely, sql } from 'kysely' -import { Dialect } from '..' - -export async function up(db: Kysely, dialect: Dialect): Promise { - if (dialect === 'sqlite') return - const res = await db - .selectFrom('repo_seq') - .select('seq') - .where('seq', 'is not', null) - .orderBy('seq', 'desc') - .limit(1) - .executeTakeFirst() - const startAt = res?.seq ? res.seq + 50000 : 1 - await sql`CREATE SEQUENCE repo_seq_sequence START ${sql.literal( - startAt, - )};`.execute(db) -} - -export async function down( - db: Kysely, - dialect: Dialect, -): Promise { - if (dialect === 'sqlite') return - await sql`DROP SEQUENCE repo_seq_sequence;`.execute(db) -} diff --git a/packages/pds/src/db/migrations/20230727T172043676Z-user-account-cursor-idx.ts b/packages/pds/src/db/migrations/20230727T172043676Z-user-account-cursor-idx.ts deleted file mode 100644 index 15f38eafd65..00000000000 --- a/packages/pds/src/db/migrations/20230727T172043676Z-user-account-cursor-idx.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .createIndex('user_account_cursor_idx') - .on('user_account') - .columns(['createdAt', 'did']) - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropIndex('user_account_cursor_idx').execute() -} diff --git a/packages/pds/src/db/migrations/20230801T141349990Z-invite-note.ts b/packages/pds/src/db/migrations/20230801T141349990Z-invite-note.ts deleted file mode 100644 index c83a3030350..00000000000 --- a/packages/pds/src/db/migrations/20230801T141349990Z-invite-note.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .alterTable('user_account') - .addColumn('inviteNote', 'varchar') - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.alterTable('user_account').dropColumn('inviteNote').execute() -} diff --git a/packages/pds/src/db/migrations/20230807T035309811Z-feed-item-delete-invite-for-user-idx.ts b/packages/pds/src/db/migrations/20230807T035309811Z-feed-item-delete-invite-for-user-idx.ts deleted file mode 100644 index f93e277ffe4..00000000000 --- a/packages/pds/src/db/migrations/20230807T035309811Z-feed-item-delete-invite-for-user-idx.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - // supports listing user invites - await db.schema - .createIndex('invite_code_for_user_idx') - .on('invite_code') - .column('forUser') - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropIndex('invite_code_for_user_idx').execute() -} diff --git a/packages/pds/src/db/migrations/20230808T172813122Z-repo-rev.ts b/packages/pds/src/db/migrations/20230808T172813122Z-repo-rev.ts deleted file mode 100644 index e4c17d73291..00000000000 --- a/packages/pds/src/db/migrations/20230808T172813122Z-repo-rev.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema.alterTable('record').addColumn('repoRev', 'varchar').execute() - await db.schema - .createIndex('record_repo_rev_idx') - .on('record') - .columns(['did', 'repoRev']) - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropIndex('record_repo_rev_idx').execute() - await db.schema.alterTable('record').dropColumn('repoRev').execute() -} diff --git a/packages/pds/src/db/migrations/20230810T203412859Z-action-duration.ts b/packages/pds/src/db/migrations/20230810T203412859Z-action-duration.ts deleted file mode 100644 index 0530d4d74fd..00000000000 --- a/packages/pds/src/db/migrations/20230810T203412859Z-action-duration.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .alterTable('moderation_action') - .addColumn('durationInHours', 'integer') - .execute() - await db.schema - .alterTable('moderation_action') - .addColumn('expiresAt', 'varchar') - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema - .alterTable('moderation_action') - .dropColumn('durationInHours') - .execute() - await db.schema - .alterTable('moderation_action') - .dropColumn('expiresAt') - .execute() -} diff --git a/packages/pds/src/db/migrations/20230818T134357818Z-runtime-flags.ts b/packages/pds/src/db/migrations/20230818T134357818Z-runtime-flags.ts deleted file mode 100644 index c93ccd74158..00000000000 --- a/packages/pds/src/db/migrations/20230818T134357818Z-runtime-flags.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .createTable('runtime_flag') - .addColumn('name', 'varchar', (col) => col.primaryKey()) - .addColumn('value', 'varchar', (col) => col.notNull()) - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropTable('runtime_flag').execute() -} diff --git a/packages/pds/src/db/migrations/20230825T142507884Z-blob-tempkey-idx.ts b/packages/pds/src/db/migrations/20230825T142507884Z-blob-tempkey-idx.ts deleted file mode 100644 index 42cb843661e..00000000000 --- a/packages/pds/src/db/migrations/20230825T142507884Z-blob-tempkey-idx.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .createIndex('blob_tempkey_idx') - .on('blob') - .column('tempKey') - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema.dropIndex('blob_tempkey_idx').execute() -} diff --git a/packages/pds/src/db/migrations/20230828T153013575Z-repo-history-rewrite.ts b/packages/pds/src/db/migrations/20230828T153013575Z-repo-history-rewrite.ts deleted file mode 100644 index 368d7cbdbe5..00000000000 --- a/packages/pds/src/db/migrations/20230828T153013575Z-repo-history-rewrite.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema.alterTable('repo_root').addColumn('rev', 'varchar').execute() - await db.schema - .alterTable('ipld_block') - .addColumn('repoRev', 'varchar') - .execute() - await db.schema - .alterTable('repo_blob') - .addColumn('repoRev', 'varchar') - .execute() - await db.schema.alterTable('repo_blob').dropColumn('commit').execute() - - await db.schema - .createIndex('ipld_block_repo_rev_idx') - .on('ipld_block') - .columns(['creator', 'repoRev', 'cid']) - .execute() - - await db.schema - .createIndex('repo_blob_repo_rev_idx') - .on('repo_blob') - .columns(['did', 'repoRev']) - .execute() - - await db.schema.dropTable('repo_commit_history').execute() - await db.schema.dropTable('repo_commit_block').execute() -} - -export async function down(db: Kysely): Promise { - await db.schema - .createTable('repo_commit_block') - .addColumn('commit', 'varchar', (col) => col.notNull()) - .addColumn('block', 'varchar', (col) => col.notNull()) - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addPrimaryKeyConstraint('repo_commit_block_pkey', [ - 'creator', - 'commit', - 'block', - ]) - .execute() - await db.schema - .createTable('repo_commit_history') - .addColumn('commit', 'varchar', (col) => col.notNull()) - .addColumn('prev', 'varchar') - .addColumn('creator', 'varchar', (col) => col.notNull()) - .addPrimaryKeyConstraint('repo_commit_history_pkey', ['creator', 'commit']) - .execute() - - await db.schema.dropIndex('ipld_block_repo_rev_idx').execute() - - await db.schema.dropIndex('repo_blob_repo_rev_idx').execute() - - await db.schema.alterTable('repo_root').dropColumn('rev').execute() - await db.schema.alterTable('ipld_block').dropColumn('repoRev').execute() - await db.schema.alterTable('repo_blob').dropColumn('repoRev').execute() - await db.schema - .alterTable('repo_blob') - .addColumn('commit', 'varchar') - .execute() -} diff --git a/packages/pds/src/db/migrations/20230914T014727199Z-repo-v3.ts b/packages/pds/src/db/migrations/20230914T014727199Z-repo-v3.ts new file mode 100644 index 00000000000..cd9569bc33c --- /dev/null +++ b/packages/pds/src/db/migrations/20230914T014727199Z-repo-v3.ts @@ -0,0 +1,165 @@ +import { Kysely, sql } from 'kysely' +import { Dialect } from '..' + +export async function up(db: Kysely, dialect: Dialect): Promise { + // sequencer leader sequence + if (dialect !== 'sqlite') { + const res = await db + .selectFrom('repo_seq') + .select('seq') + .where('seq', 'is not', null) + .orderBy('seq', 'desc') + .limit(1) + .executeTakeFirst() + const startAt = res?.seq ? res.seq + 50000 : 1 + await sql`CREATE SEQUENCE repo_seq_sequence START ${sql.literal( + startAt, + )};`.execute(db) + } + + // user account cursor idx + await db.schema + .createIndex('user_account_cursor_idx') + .on('user_account') + .columns(['createdAt', 'did']) + .execute() + + // invite note + await db.schema + .alterTable('user_account') + .addColumn('inviteNote', 'varchar') + .execute() + + // listing user invites + await db.schema + .createIndex('invite_code_for_user_idx') + .on('invite_code') + .column('forUser') + .execute() + + // mod action duration + await db.schema + .alterTable('moderation_action') + .addColumn('durationInHours', 'integer') + .execute() + await db.schema + .alterTable('moderation_action') + .addColumn('expiresAt', 'varchar') + .execute() + + // runtime flag + await db.schema + .createTable('runtime_flag') + .addColumn('name', 'varchar', (col) => col.primaryKey()) + .addColumn('value', 'varchar', (col) => col.notNull()) + .execute() + + // blob tempkey idx + await db.schema + .createIndex('blob_tempkey_idx') + .on('blob') + .column('tempKey') + .execute() + + // repo v3 + await db.schema.alterTable('repo_root').addColumn('rev', 'varchar').execute() + await db.schema.alterTable('record').addColumn('repoRev', 'varchar').execute() + await db.schema + .alterTable('ipld_block') + .addColumn('repoRev', 'varchar') + .execute() + await db.schema + .alterTable('repo_blob') + .addColumn('repoRev', 'varchar') + .execute() + await db.schema.alterTable('repo_blob').dropColumn('commit').execute() + + await db.schema + .createIndex('record_repo_rev_idx') + .on('record') + .columns(['did', 'repoRev']) + .execute() + + await db.schema + .createIndex('ipld_block_repo_rev_idx') + .on('ipld_block') + .columns(['creator', 'repoRev', 'cid']) + .execute() + + await db.schema + .createIndex('repo_blob_repo_rev_idx') + .on('repo_blob') + .columns(['did', 'repoRev']) + .execute() + + await db.schema.dropTable('repo_commit_history').execute() + await db.schema.dropTable('repo_commit_block').execute() +} + +export async function down( + db: Kysely, + dialect: Dialect, +): Promise { + // repo v3 + await db.schema + .createTable('repo_commit_block') + .addColumn('commit', 'varchar', (col) => col.notNull()) + .addColumn('block', 'varchar', (col) => col.notNull()) + .addColumn('creator', 'varchar', (col) => col.notNull()) + .addPrimaryKeyConstraint('repo_commit_block_pkey', [ + 'creator', + 'commit', + 'block', + ]) + .execute() + await db.schema + .createTable('repo_commit_history') + .addColumn('commit', 'varchar', (col) => col.notNull()) + .addColumn('prev', 'varchar') + .addColumn('creator', 'varchar', (col) => col.notNull()) + .addPrimaryKeyConstraint('repo_commit_history_pkey', ['creator', 'commit']) + .execute() + + await db.schema.dropIndex('record_repo_rev_idx').execute() + await db.schema.dropIndex('ipld_block_repo_rev_idx').execute() + await db.schema.dropIndex('repo_blob_repo_rev_idx').execute() + + await db.schema.alterTable('repo_root').dropColumn('rev').execute() + await db.schema.alterTable('record').dropColumn('repoRev').execute() + await db.schema.alterTable('ipld_block').dropColumn('repoRev').execute() + await db.schema.alterTable('repo_blob').dropColumn('repoRev').execute() + await db.schema + .alterTable('repo_blob') + .addColumn('commit', 'varchar') + .execute() + + // blob tempkey idx + await db.schema.dropIndex('blob_tempkey_idx').execute() + + // runtime flag + await db.schema.dropTable('runtime_flag').execute() + + // mod action duration + await db.schema + .alterTable('moderation_action') + .dropColumn('durationInHours') + .execute() + await db.schema + .alterTable('moderation_action') + .dropColumn('expiresAt') + .execute() + + // listing user invites + await db.schema.dropIndex('invite_code_for_user_idx').execute() + + // invite note + await db.schema.alterTable('user_account').dropColumn('inviteNote').execute() + + // user account cursor idx + await db.schema.dropIndex('user_account_cursor_idx').execute() + + // sequencer leader sequence + if (dialect !== 'sqlite') { + await sql`DROP SEQUENCE repo_seq_sequence;`.execute(db) + } +} diff --git a/packages/pds/src/db/migrations/index.ts b/packages/pds/src/db/migrations/index.ts index c8fd3d13b23..8ebd59d6ef3 100644 --- a/packages/pds/src/db/migrations/index.ts +++ b/packages/pds/src/db/migrations/index.ts @@ -3,12 +3,4 @@ // this with kysely's FileMigrationProvider, but it doesn't play nicely with the build process. export * as _20230613T164932261Z from './20230613T164932261Z-init' -export * as _20230718T170914772Z from './20230718T170914772Z-sequencer-leader-sequence' -export * as _20230727T172043676Z from './20230727T172043676Z-user-account-cursor-idx' -export * as _20230801T141349990Z from './20230801T141349990Z-invite-note' -export * as _20230807T035309811Z from './20230807T035309811Z-feed-item-delete-invite-for-user-idx' -export * as _20230808T172813122Z from './20230808T172813122Z-repo-rev' -export * as _20230810T203412859Z from './20230810T203412859Z-action-duration' -export * as _20230818T134357818Z from './20230818T134357818Z-runtime-flags' -export * as _20230825T142507884Z from './20230825T142507884Z-blob-tempkey-idx' -export * as _20230828T153013575Z from './20230828T153013575Z-repo-history-rewrite' +export * as _20230914T014727199Z from './20230914T014727199Z-repo-v3' From cd78e8ae6c1c5906966f34162f05b351a6f0ecc6 Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 13 Sep 2023 20:57:03 -0500 Subject: [PATCH 070/105] fix service entry --- services/pds/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/pds/index.js b/services/pds/index.js index c2cabea4f73..a99b7ae980e 100644 --- a/services/pds/index.js +++ b/services/pds/index.js @@ -44,7 +44,7 @@ const main = async () => { // If the PDS is configured to proxy moderation, this will be running on appview instead of pds. // Also don't run this on the sequencer leader, which may not be configured regarding moderation proxying at all. const periodicModerationActionReversal = - pds.ctx.shouldProxyModeration() || pds.ctx.cfg.sequencerLeaderEnabled + pds.cfg.bskyAppView.proxyModeration || pds.ctx.cfg.sequencerLeaderEnabled ? null : new PeriodicModerationActionReversal(pds.ctx) const periodicModerationActionReversalRunning = From 85fb95f9b418d4785b7d332a0f626b44fac8b374 Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 13 Sep 2023 21:13:16 -0500 Subject: [PATCH 071/105] bump version --- packages/pds/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/package.json b/packages/pds/package.json index 2d78352f092..349be3a5c52 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/pds", - "version": "0.2.0-beta.5", + "version": "0.3.0-beta.0", "license": "MIT", "repository": { "type": "git", From c1cafd716abd4d385f5669ab6c277b4941cd41d8 Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 13 Sep 2023 21:37:59 -0500 Subject: [PATCH 072/105] change auth order --- packages/pds/src/auth.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/pds/src/auth.ts b/packages/pds/src/auth.ts index 66c9b1f0780..6d75f1fd920 100644 --- a/packages/pds/src/auth.ts +++ b/packages/pds/src/auth.ts @@ -120,14 +120,14 @@ export class ServerAuth { return { status: Missing, admin: false, moderator: false, triage: false } } const { username, password } = parsed - if (username === 'admin' && password === this._triagePass) { - return { status: Valid, admin: false, moderator: false, triage: true } + if (username === 'admin' && password === this._adminPass) { + return { status: Valid, admin: true, moderator: true, triage: true } } if (username === 'admin' && password === this._moderatorPass) { return { status: Valid, admin: false, moderator: true, triage: true } } - if (username === 'admin' && password === this._adminPass) { - return { status: Valid, admin: true, moderator: true, triage: true } + if (username === 'admin' && password === this._triagePass) { + return { status: Valid, admin: false, moderator: false, triage: true } } return { status: Invalid, admin: false, moderator: false, triage: false } } From 3a9d3aa887e9e313412e272261f0e2d6809c1a30 Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 13 Sep 2023 21:38:08 -0500 Subject: [PATCH 073/105] bump version --- packages/pds/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/package.json b/packages/pds/package.json index 349be3a5c52..acd5a87cefb 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/pds", - "version": "0.3.0-beta.0", + "version": "0.3.0-beta.1", "license": "MIT", "repository": { "type": "git", From d948ca6fe0b86520e129d04307ec74e836cef20b Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 14 Sep 2023 11:33:31 -0500 Subject: [PATCH 074/105] bump version --- packages/pds/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/package.json b/packages/pds/package.json index acd5a87cefb..3520c3e10c4 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/pds", - "version": "0.3.0-beta.1", + "version": "0.3.0-beta.2", "license": "MIT", "repository": { "type": "git", From 7f99cbfd8f13013ea0a5c57234198a4de24dfa5e Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 14 Sep 2023 19:23:46 -0500 Subject: [PATCH 075/105] add upgradeRepoVersion & fallback url for cdn --- .../com/atproto/temp/upgradeRepoVersion.json | 21 +++ packages/api/src/client/index.ts | 23 +++ packages/api/src/client/lexicons.ts | 27 ++++ .../com/atproto/temp/upgradeRepoVersion.ts | 33 +++++ packages/bsky/src/lexicon/index.ts | 22 +++ packages/bsky/src/lexicon/lexicons.ts | 27 ++++ .../com/atproto/temp/upgradeRepoVersion.ts | 39 +++++ packages/pds/src/api/com/atproto/index.ts | 2 + .../src/api/com/atproto/upgradeRepoVersion.ts | 140 ++++++++++++++++++ packages/pds/src/context.ts | 1 + packages/pds/src/lexicon/index.ts | 22 +++ packages/pds/src/lexicon/lexicons.ts | 27 ++++ .../com/atproto/temp/upgradeRepoVersion.ts | 39 +++++ packages/pds/src/services/index.ts | 3 + packages/pds/src/services/local/index.ts | 5 +- 15 files changed, 430 insertions(+), 1 deletion(-) create mode 100644 lexicons/com/atproto/temp/upgradeRepoVersion.json create mode 100644 packages/api/src/client/types/com/atproto/temp/upgradeRepoVersion.ts create mode 100644 packages/bsky/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts create mode 100644 packages/pds/src/api/com/atproto/upgradeRepoVersion.ts create mode 100644 packages/pds/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts diff --git a/lexicons/com/atproto/temp/upgradeRepoVersion.json b/lexicons/com/atproto/temp/upgradeRepoVersion.json new file mode 100644 index 00000000000..05d8c7197fd --- /dev/null +++ b/lexicons/com/atproto/temp/upgradeRepoVersion.json @@ -0,0 +1,21 @@ +{ + "lexicon": 1, + "id": "com.atproto.temp.upgradeRepoVersion", + "defs": { + "main": { + "type": "procedure", + "description": "Upgrade a repo to v3", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["did"], + "properties": { + "did": { "type": "string", "format": "did" }, + "force": { "type": "boolean" } + } + } + } + } + } +} diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index 761097aad7c..dfd5e51ff57 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -70,6 +70,7 @@ import * as ComAtprotoSyncListRepos from './types/com/atproto/sync/listRepos' import * as ComAtprotoSyncNotifyOfUpdate from './types/com/atproto/sync/notifyOfUpdate' import * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCrawl' import * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos' +import * as ComAtprotoTempUpgradeRepoVersion from './types/com/atproto/temp/upgradeRepoVersion' import * as AppBskyActorDefs from './types/app/bsky/actor/defs' import * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences' import * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile' @@ -194,6 +195,7 @@ export * as ComAtprotoSyncListRepos from './types/com/atproto/sync/listRepos' export * as ComAtprotoSyncNotifyOfUpdate from './types/com/atproto/sync/notifyOfUpdate' export * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCrawl' export * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos' +export * as ComAtprotoTempUpgradeRepoVersion from './types/com/atproto/temp/upgradeRepoVersion' export * as AppBskyActorDefs from './types/app/bsky/actor/defs' export * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences' export * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile' @@ -322,6 +324,7 @@ export class AtprotoNS { repo: RepoNS server: ServerNS sync: SyncNS + temp: TempNS constructor(service: AtpServiceClient) { this._service = service @@ -332,6 +335,7 @@ export class AtprotoNS { this.repo = new RepoNS(service) this.server = new ServerNS(service) this.sync = new SyncNS(service) + this.temp = new TempNS(service) } } @@ -1007,6 +1011,25 @@ export class SyncNS { } } +export class TempNS { + _service: AtpServiceClient + + constructor(service: AtpServiceClient) { + this._service = service + } + + upgradeRepoVersion( + data?: ComAtprotoTempUpgradeRepoVersion.InputSchema, + opts?: ComAtprotoTempUpgradeRepoVersion.CallOptions, + ): Promise { + return this._service.xrpc + .call('com.atproto.temp.upgradeRepoVersion', opts?.qp, data, opts) + .catch((e) => { + throw ComAtprotoTempUpgradeRepoVersion.toKnownErr(e) + }) + } +} + export class AppNS { _service: AtpServiceClient bsky: BskyNS diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index f3c93c5e805..c682f239a3b 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -3517,6 +3517,32 @@ export const schemaDict = { }, }, }, + ComAtprotoTempUpgradeRepoVersion: { + lexicon: 1, + id: 'com.atproto.temp.upgradeRepoVersion', + defs: { + main: { + type: 'procedure', + description: 'Upgrade a repo to v3', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + }, + force: { + type: 'boolean', + }, + }, + }, + }, + }, + }, + }, AppBskyActorDefs: { lexicon: 1, id: 'app.bsky.actor.defs', @@ -6838,6 +6864,7 @@ export const ids = { ComAtprotoSyncNotifyOfUpdate: 'com.atproto.sync.notifyOfUpdate', ComAtprotoSyncRequestCrawl: 'com.atproto.sync.requestCrawl', ComAtprotoSyncSubscribeRepos: 'com.atproto.sync.subscribeRepos', + ComAtprotoTempUpgradeRepoVersion: 'com.atproto.temp.upgradeRepoVersion', AppBskyActorDefs: 'app.bsky.actor.defs', AppBskyActorGetPreferences: 'app.bsky.actor.getPreferences', AppBskyActorGetProfile: 'app.bsky.actor.getProfile', diff --git a/packages/api/src/client/types/com/atproto/temp/upgradeRepoVersion.ts b/packages/api/src/client/types/com/atproto/temp/upgradeRepoVersion.ts new file mode 100644 index 00000000000..abaf3a9f1b0 --- /dev/null +++ b/packages/api/src/client/types/com/atproto/temp/upgradeRepoVersion.ts @@ -0,0 +1,33 @@ +/** + * 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' + +export interface QueryParams {} + +export interface InputSchema { + did: string + force?: boolean + [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/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index 93435056503..c0840abea2c 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -67,6 +67,7 @@ import * as ComAtprotoSyncListRepos from './types/com/atproto/sync/listRepos' import * as ComAtprotoSyncNotifyOfUpdate from './types/com/atproto/sync/notifyOfUpdate' import * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCrawl' import * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos' +import * as ComAtprotoTempUpgradeRepoVersion from './types/com/atproto/temp/upgradeRepoVersion' import * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences' import * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile' import * as AppBskyActorGetProfiles from './types/app/bsky/actor/getProfiles' @@ -163,6 +164,7 @@ export class AtprotoNS { repo: RepoNS server: ServerNS sync: SyncNS + temp: TempNS constructor(server: Server) { this._server = server @@ -173,6 +175,7 @@ export class AtprotoNS { this.repo = new RepoNS(server) this.server = new ServerNS(server) this.sync = new SyncNS(server) + this.temp = new TempNS(server) } } @@ -870,6 +873,25 @@ export class SyncNS { } } +export class TempNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + upgradeRepoVersion( + cfg: ConfigOf< + AV, + ComAtprotoTempUpgradeRepoVersion.Handler>, + ComAtprotoTempUpgradeRepoVersion.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.temp.upgradeRepoVersion' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + export class AppNS { _server: Server bsky: BskyNS diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index f3c93c5e805..c682f239a3b 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -3517,6 +3517,32 @@ export const schemaDict = { }, }, }, + ComAtprotoTempUpgradeRepoVersion: { + lexicon: 1, + id: 'com.atproto.temp.upgradeRepoVersion', + defs: { + main: { + type: 'procedure', + description: 'Upgrade a repo to v3', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + }, + force: { + type: 'boolean', + }, + }, + }, + }, + }, + }, + }, AppBskyActorDefs: { lexicon: 1, id: 'app.bsky.actor.defs', @@ -6838,6 +6864,7 @@ export const ids = { ComAtprotoSyncNotifyOfUpdate: 'com.atproto.sync.notifyOfUpdate', ComAtprotoSyncRequestCrawl: 'com.atproto.sync.requestCrawl', ComAtprotoSyncSubscribeRepos: 'com.atproto.sync.subscribeRepos', + ComAtprotoTempUpgradeRepoVersion: 'com.atproto.temp.upgradeRepoVersion', AppBskyActorDefs: 'app.bsky.actor.defs', AppBskyActorGetPreferences: 'app.bsky.actor.getPreferences', AppBskyActorGetProfile: 'app.bsky.actor.getProfile', diff --git a/packages/bsky/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts b/packages/bsky/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts new file mode 100644 index 00000000000..c5b77876fec --- /dev/null +++ b/packages/bsky/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts @@ -0,0 +1,39 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + did: string + force?: boolean + [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/api/com/atproto/index.ts b/packages/pds/src/api/com/atproto/index.ts index a5c26c80495..59fca0fd5a9 100644 --- a/packages/pds/src/api/com/atproto/index.ts +++ b/packages/pds/src/api/com/atproto/index.ts @@ -6,6 +6,7 @@ import moderation from './moderation' import repo from './repo' import serverMethods from './server' import sync from './sync' +import upgradeRepoVersion from './upgradeRepoVersion' export default function (server: Server, ctx: AppContext) { admin(server, ctx) @@ -14,4 +15,5 @@ export default function (server: Server, ctx: AppContext) { repo(server, ctx) serverMethods(server, ctx) sync(server, ctx) + upgradeRepoVersion(server, ctx) } diff --git a/packages/pds/src/api/com/atproto/upgradeRepoVersion.ts b/packages/pds/src/api/com/atproto/upgradeRepoVersion.ts new file mode 100644 index 00000000000..dd20943be3e --- /dev/null +++ b/packages/pds/src/api/com/atproto/upgradeRepoVersion.ts @@ -0,0 +1,140 @@ +import { InvalidRequestError } from '@atproto/xrpc-server' +import { TID, chunkArray, wait } from '@atproto/common' +import { Server } from '../../../lexicon' +import SqlRepoStorage from '../../../sql-repo-storage' +import AppContext from '../../../context' +import { + BlockMap, + CidSet, + DataDiff, + MST, + MemoryBlockstore, + def, + signCommit, +} from '@atproto/repo' +import { CID } from 'multiformats/cid' +import { formatSeqCommit, sequenceEvt } from '../../../sequencer' +import { httpLogger as log } from '../../../logger' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.temp.upgradeRepoVersion({ + auth: ctx.roleVerifier, + handler: async ({ input, auth }) => { + if (!auth.credentials.admin) { + throw new InvalidRequestError('must be admin') + } + const { did, force } = input.body + + await ctx.db.transaction(async (dbTxn) => { + const storage = new SqlRepoStorage(dbTxn, did) + await obtainLock(storage) + const prevCid = await storage.getRoot() + if (!prevCid) { + throw new InvalidRequestError('Could not find repo') + } + const prev = await storage.readObj(prevCid, def.versionedCommit) + const records = await dbTxn.db + .selectFrom('record') + .select(['collection', 'rkey', 'cid']) + .where('did', '=', did) + .execute() + const memoryStore = new MemoryBlockstore() + let data = await MST.create(memoryStore) + for (const record of records) { + const dataKey = record.collection + '/' + record.rkey + const cid = CID.parse(record.cid) + data = await data.add(dataKey, cid) + } + const dataCid = await data.getPointer() + if (!force && !dataCid.equals(prev.data)) { + throw new InvalidRequestError('Data cid did not match') + } + const recordCids = records.map((r) => r.cid) + const diff = await DataDiff.of(data, null) + const cidsToKeep = [...recordCids, ...diff.newMstBlocks.cids()] + const rev = TID.nextStr(prev.rev) + if (force) { + const got = await storage.getBlocks(diff.newMstBlocks.cids()) + const toAdd = diff.newMstBlocks.getMany(got.missing) + log.info( + { missing: got.missing.length }, + 'force added missing blocks', + ) + // puts any new blocks & no-ops for already existing + await storage.putMany(toAdd.blocks, rev) + } + for (const chunk of chunkArray(cidsToKeep, 500)) { + const cidStrs = chunk.map((c) => c.toString()) + await dbTxn.db + .updateTable('ipld_block') + .set({ repoRev: rev }) + .where('creator', '=', did) + .where('cid', 'in', cidStrs) + .execute() + } + await dbTxn.db + .deleteFrom('ipld_block') + .where('creator', '=', did) + .where((qb) => + qb.where('repoRev', 'is', null).orWhere('repoRev', '!=', rev), + ) + .execute() + await dbTxn.db + .updateTable('repo_blob') + .set({ repoRev: rev }) + .where('did', '=', did) + .execute() + await dbTxn.db + .updateTable('record') + .set({ repoRev: rev }) + .where('did', '=', did) + .execute() + const commit = await signCommit( + { + did, + version: 3, + rev: TID.nextStr(), + prev: prevCid, + data: dataCid, + }, + ctx.repoSigningKey, + ) + const newBlocks = new BlockMap() + const commitCid = await newBlocks.add(commit) + await storage.putMany(newBlocks, rev) + await dbTxn.db + .updateTable('repo_root') + .set({ + root: commitCid.toString(), + rev, + indexedAt: storage.getTimestamp(), + }) + .where('did', '=', did) + .execute() + + const commitData = { + cid: commitCid, + rev, + prev: prevCid, + since: null, + newBlocks, + removedCids: new CidSet(), + } + const seqEvt = await formatSeqCommit(did, commitData, []) + await sequenceEvt(dbTxn, seqEvt) + }) + }, + }) +} + +const obtainLock = async (storage: SqlRepoStorage, tries = 20) => { + const obtained = await storage.lockRepo() + if (obtained) { + return + } + if (tries < 1) { + throw new InvalidRequestError('could not obtain lock') + } + await wait(50) + return obtainLock(storage, tries - 1) +} diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index 71af4ae62dc..466bc9a5613 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -180,6 +180,7 @@ export class AppContext { repoSigningKey, blobstore, appViewAgent, + pdsHostname: cfg.service.hostname, appViewDid: cfg.bskyAppView.did, appViewCdnUrlPattern: cfg.bskyAppView.cdnUrlPattern, backgroundQueue, diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 93435056503..c0840abea2c 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -67,6 +67,7 @@ import * as ComAtprotoSyncListRepos from './types/com/atproto/sync/listRepos' import * as ComAtprotoSyncNotifyOfUpdate from './types/com/atproto/sync/notifyOfUpdate' import * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCrawl' import * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos' +import * as ComAtprotoTempUpgradeRepoVersion from './types/com/atproto/temp/upgradeRepoVersion' import * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences' import * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile' import * as AppBskyActorGetProfiles from './types/app/bsky/actor/getProfiles' @@ -163,6 +164,7 @@ export class AtprotoNS { repo: RepoNS server: ServerNS sync: SyncNS + temp: TempNS constructor(server: Server) { this._server = server @@ -173,6 +175,7 @@ export class AtprotoNS { this.repo = new RepoNS(server) this.server = new ServerNS(server) this.sync = new SyncNS(server) + this.temp = new TempNS(server) } } @@ -870,6 +873,25 @@ export class SyncNS { } } +export class TempNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + upgradeRepoVersion( + cfg: ConfigOf< + AV, + ComAtprotoTempUpgradeRepoVersion.Handler>, + ComAtprotoTempUpgradeRepoVersion.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.temp.upgradeRepoVersion' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + export class AppNS { _server: Server bsky: BskyNS diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index f3c93c5e805..c682f239a3b 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -3517,6 +3517,32 @@ export const schemaDict = { }, }, }, + ComAtprotoTempUpgradeRepoVersion: { + lexicon: 1, + id: 'com.atproto.temp.upgradeRepoVersion', + defs: { + main: { + type: 'procedure', + description: 'Upgrade a repo to v3', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + }, + force: { + type: 'boolean', + }, + }, + }, + }, + }, + }, + }, AppBskyActorDefs: { lexicon: 1, id: 'app.bsky.actor.defs', @@ -6838,6 +6864,7 @@ export const ids = { ComAtprotoSyncNotifyOfUpdate: 'com.atproto.sync.notifyOfUpdate', ComAtprotoSyncRequestCrawl: 'com.atproto.sync.requestCrawl', ComAtprotoSyncSubscribeRepos: 'com.atproto.sync.subscribeRepos', + ComAtprotoTempUpgradeRepoVersion: 'com.atproto.temp.upgradeRepoVersion', AppBskyActorDefs: 'app.bsky.actor.defs', AppBskyActorGetPreferences: 'app.bsky.actor.getPreferences', AppBskyActorGetProfile: 'app.bsky.actor.getProfile', diff --git a/packages/pds/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts b/packages/pds/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts new file mode 100644 index 00000000000..c5b77876fec --- /dev/null +++ b/packages/pds/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts @@ -0,0 +1,39 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + did: string + force?: boolean + [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/index.ts b/packages/pds/src/services/index.ts index 30dffedf061..954a5544e6e 100644 --- a/packages/pds/src/services/index.ts +++ b/packages/pds/src/services/index.ts @@ -14,6 +14,7 @@ import { LocalService } from './local' export function createServices(resources: { repoSigningKey: crypto.Keypair blobstore: BlobStore + pdsHostname: string appViewAgent?: AtpAgent appViewDid?: string appViewCdnUrlPattern?: string @@ -23,6 +24,7 @@ export function createServices(resources: { const { repoSigningKey, blobstore, + pdsHostname, appViewAgent, appViewDid, appViewCdnUrlPattern, @@ -41,6 +43,7 @@ export function createServices(resources: { ), local: LocalService.creator( repoSigningKey, + pdsHostname, appViewAgent, appViewDid, appViewCdnUrlPattern, diff --git a/packages/pds/src/services/local/index.ts b/packages/pds/src/services/local/index.ts index 867f3baf4e7..7af82ee4173 100644 --- a/packages/pds/src/services/local/index.ts +++ b/packages/pds/src/services/local/index.ts @@ -39,6 +39,7 @@ export class LocalService { constructor( public db: Database, public signingKey: Keypair, + public pdsHostname: string, public appviewAgent?: AtpAgent, public appviewDid?: string, public appviewCdnUrlPattern?: string, @@ -46,6 +47,7 @@ export class LocalService { static creator( signingKey: Keypair, + pdsHostname: string, appviewAgent?: AtpAgent, appviewDid?: string, appviewCdnUrlPattern?: string, @@ -54,6 +56,7 @@ export class LocalService { new LocalService( db, signingKey, + pdsHostname, appviewAgent, appviewDid, appviewCdnUrlPattern, @@ -62,7 +65,7 @@ export class LocalService { getImageUrl(pattern: CommonSignedUris, did: string, cid: string) { if (!this.appviewCdnUrlPattern) { - return '' + return `https://${this.pdsHostname}/xrpc/${ids.ComAtprotoSyncGetBlob}?did=${did}&cid=${cid}` } return util.format(this.appviewCdnUrlPattern, pattern, did, cid) } From 49271508cc6eb5ebde6af55522f895ce699d575d Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 14 Sep 2023 19:32:00 -0500 Subject: [PATCH 076/105] bump version --- packages/pds/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/package.json b/packages/pds/package.json index 3520c3e10c4..604d4461a8e 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/pds", - "version": "0.3.0-beta.2", + "version": "0.3.0-beta.3", "license": "MIT", "repository": { "type": "git", From dc53474040705666f09f16b12548f61d1e2cb24f Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 26 Sep 2023 12:14:02 -0500 Subject: [PATCH 077/105] merging --- .changeset/seven-schools-switch.md | 6 - .eslintrc | 5 +- LICENSE | 2 +- Makefile | 2 + README.md | 2 +- lexicons/app/bsky/actor/defs.json | 48 +- lexicons/app/bsky/actor/searchActors.json | 13 +- .../app/bsky/actor/searchActorsTypeahead.json | 11 +- lexicons/app/bsky/feed/defs.json | 24 +- lexicons/app/bsky/feed/getListFeed.json | 42 + lexicons/app/bsky/feed/post.json | 6 + lexicons/app/bsky/feed/searchPosts.json | 52 ++ lexicons/app/bsky/feed/threadgate.json | 45 + lexicons/app/bsky/graph/defs.json | 9 +- lexicons/app/bsky/richtext/facet.json | 10 +- lexicons/app/bsky/unspecced/applyLabels.json | 23 - lexicons/app/bsky/unspecced/defs.json | 20 + lexicons/app/bsky/unspecced/getPopular.json | 2 +- .../bsky/unspecced/searchActorsSkeleton.json | 56 ++ .../bsky/unspecced/searchPostsSkeleton.json | 52 ++ lexicons/com/atproto/admin/searchRepos.json | 6 +- package.json | 2 +- packages/api/CHANGELOG.md | 41 + packages/api/README.md | 12 +- packages/api/package.json | 21 +- packages/api/src/agent.ts | 1 - packages/api/src/bsky-agent.ts | 74 +- packages/api/src/client/index.ts | 137 ++- packages/api/src/client/lexicons.ts | 490 ++++++++++- .../src/client/types/app/bsky/actor/defs.ts | 50 ++ .../types/app/bsky/actor/searchActors.ts | 3 + .../app/bsky/actor/searchActorsTypeahead.ts | 3 + .../src/client/types/app/bsky/feed/defs.ts | 40 + .../applyLabels.ts => feed/getListFeed.ts} | 25 +- .../src/client/types/app/bsky/feed/post.ts | 2 + .../client/types/app/bsky/feed/searchPosts.ts | 50 ++ .../client/types/app/bsky/feed/threadgate.ts | 84 ++ .../src/client/types/app/bsky/graph/defs.ts | 7 +- .../client/types/app/bsky/richtext/facet.ts | 18 +- .../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/moderation/subjects/feed-generator.ts | 4 +- .../api/src/moderation/subjects/user-list.ts | 4 +- packages/api/src/moderation/util.ts | 6 +- packages/api/src/rich-text/detection.ts | 27 + packages/api/src/rich-text/rich-text.ts | 13 + packages/api/src/rich-text/sanitization.ts | 2 + packages/api/src/types.ts | 26 +- packages/api/tests/bsky-agent.test.ts | 538 +++++++++++- .../api/tests/rich-text-detection.test.ts | 104 +++ packages/aws/CHANGELOG.md | 8 + packages/aws/package.json | 20 +- packages/bsky/CHANGELOG.md | 49 ++ packages/bsky/README.md | 11 +- packages/bsky/package.json | 11 +- .../src/api/app/bsky/actor/searchActors.ts | 8 +- .../app/bsky/actor/searchActorsTypeahead.ts | 8 +- .../src/api/app/bsky/feed/getActorLikes.ts | 2 +- .../bsky/src/api/app/bsky/feed/getListFeed.ts | 129 +++ .../src/api/app/bsky/feed/getPostThread.ts | 79 +- .../bsky/src/api/app/bsky/feed/getPosts.ts | 2 + .../atproto/admin/reverseModerationAction.ts | 1 - .../src/api/com/atproto/admin/searchRepos.ts | 5 + packages/bsky/src/api/index.ts | 2 + packages/bsky/src/auto-moderator/abyss.ts | 32 +- packages/bsky/src/auto-moderator/index.ts | 63 +- packages/bsky/src/db/database-schema.ts | 2 + .../20230906T222220386Z-thread-gating.ts | 27 + .../20230920T213858047Z-add-tags-to-post.ts | 9 + packages/bsky/src/db/migrations/index.ts | 2 + packages/bsky/src/db/tables/post.ts | 3 + packages/bsky/src/db/tables/thread-gate.ts | 12 + packages/bsky/src/lexicon/index.ts | 61 +- packages/bsky/src/lexicon/lexicons.ts | 490 ++++++++++- .../src/lexicon/types/app/bsky/actor/defs.ts | 50 ++ .../types/app/bsky/actor/searchActors.ts | 3 + .../app/bsky/actor/searchActorsTypeahead.ts | 3 + .../src/lexicon/types/app/bsky/feed/defs.ts | 40 + .../types/app/bsky/feed/getListFeed.ts} | 25 +- .../src/lexicon/types/app/bsky/feed/post.ts | 2 + .../types/app/bsky/feed/searchPosts.ts | 54 ++ .../lexicon/types/app/bsky/feed/threadgate.ts | 84 ++ .../src/lexicon/types/app/bsky/graph/defs.ts | 7 +- .../lexicon/types/app/bsky/richtext/facet.ts | 18 +- .../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/bsky/src/services/actor/views.ts | 20 +- packages/bsky/src/services/feed/index.ts | 66 +- 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/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 | 84 +- .../src/services/indexing/plugins/repost.ts | 2 +- .../services/indexing/plugins/thread-gate.ts | 95 +++ packages/bsky/src/services/label/index.ts | 2 +- .../tests/__snapshots__/indexing.test.ts.snap | 6 + packages/bsky/tests/_util.ts | 1 + .../tests/auto-moderator/takedowns.test.ts | 3 +- packages/bsky/tests/seeds/client.ts | 53 ++ .../__snapshots__/block-lists.test.ts.snap | 10 +- .../views/__snapshots__/blocks.test.ts.snap | 9 + .../__snapshots__/list-feed.test.ts.snap | 721 ++++++++++++++++ .../__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 ++++ packages/bsky/tests/views/actor-likes.test.ts | 6 - packages/bsky/tests/views/blocks.test.ts | 45 +- 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 + packages/bsky/tests/views/posts.test.ts | 28 +- .../tests/views/suggested-follows.test.ts | 2 +- .../bsky/tests/views/threadgating.test.ts | 571 +++++++++++++ packages/common-web/README.md | 11 +- packages/common-web/package.json | 17 +- packages/common/README.md | 11 +- packages/common/package.json | 18 +- .../indexing/util.ts => common/src/dates.ts} | 0 packages/common/src/index.ts | 1 + packages/common/tests/streams.test.ts | 2 +- packages/crypto/README.md | 47 +- packages/crypto/package.json | 18 +- packages/crypto/tests/signatures.test.ts | 1 + packages/dev-env/CHANGELOG.md | 57 ++ packages/dev-env/README.md | 9 +- packages/dev-env/build.js | 2 +- packages/dev-env/package.json | 22 +- packages/dev-env/src/bin.ts | 13 +- packages/dev-env/src/mock/index.ts | 29 +- packages/dev-env/src/pds.ts | 36 + packages/dev-env/src/types.ts | 3 + 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/identity/README.md | 41 +- packages/identity/package.json | 19 +- packages/lex-cli/CHANGELOG.md | 9 + packages/lex-cli/package.json | 20 +- packages/lexicon/CHANGELOG.md | 8 + packages/lexicon/README.md | 13 +- packages/lexicon/package.json | 20 +- packages/lexicon/src/types.ts | 8 +- packages/lexicon/tests/general.test.ts | 1 - 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/pds/CHANGELOG.md | 50 ++ packages/pds/README.md | 13 +- packages/pds/package.json | 18 +- .../src/api/app/bsky/actor/getPreferences.ts | 26 + .../pds/src/api/app/bsky/actor/getProfile.ts | 38 + .../pds/src/api/app/bsky/actor/getProfiles.ts | 46 + .../src/api/app/bsky/actor/getSuggestions.ts | 19 + packages/pds/src/api/app/bsky/actor/index.ts | 20 + .../src/api/app/bsky/actor/putPreferences.ts | 28 + .../src/api/app/bsky/actor/searchActors.ts | 19 + .../app/bsky/actor/searchActorsTypeahead.ts | 20 + .../pds/src/api/app/bsky/dynamic/index.ts | 7 - .../src/api/app/bsky/feed/getActorFeeds.ts | 19 + .../bsky/{munged => feed}/getActorLikes.ts | 4 +- .../bsky/{munged => feed}/getAuthorFeed.ts | 4 +- packages/pds/src/api/app/bsky/feed/getFeed.ts | 25 + .../src/api/app/bsky/feed/getFeedGenerator.ts | 19 + .../api/app/bsky/feed/getFeedGenerators.ts | 19 + .../pds/src/api/app/bsky/feed/getLikes.ts | 19 + .../pds/src/api/app/bsky/feed/getListFeed.ts | 19 + .../bsky/{munged => feed}/getPostThread.ts | 10 +- .../pds/src/api/app/bsky/feed/getPosts.ts | 19 + .../src/api/app/bsky/feed/getRepostedBy.ts | 19 + .../api/app/bsky/feed/getSuggestedFeeds.ts | 19 + .../app/bsky/{munged => feed}/getTimeline.ts | 5 +- packages/pds/src/api/app/bsky/feed/index.ts | 31 + .../pds/src/api/app/bsky/graph/getBlocks.ts | 19 + .../src/api/app/bsky/graph/getFollowers.ts | 21 + .../pds/src/api/app/bsky/graph/getFollows.ts | 21 + .../pds/src/api/app/bsky/graph/getList.ts | 19 + .../src/api/app/bsky/graph/getListBlocks.ts | 19 + .../src/api/app/bsky/graph/getListMutes.ts | 19 + .../pds/src/api/app/bsky/graph/getLists.ts | 19 + .../pds/src/api/app/bsky/graph/getMutes.ts | 19 + .../bsky/graph/getSuggestedFollowsByActor.ts | 20 + packages/pds/src/api/app/bsky/graph/index.ts | 31 + .../pds/src/api/app/bsky/graph/muteActor.ts | 34 + .../src/api/app/bsky/graph/muteActorList.ts | 33 + .../pds/src/api/app/bsky/graph/unmuteActor.ts | 31 + .../src/api/app/bsky/graph/unmuteActorList.ts | 24 + packages/pds/src/api/app/bsky/index.ts | 18 +- packages/pds/src/api/app/bsky/munged/index.ts | 17 - .../app/bsky/notification/getUnreadCount.ts | 20 + .../src/api/app/bsky/notification/index.ts | 14 + .../bsky/notification/listNotifications.ts | 20 + .../{dynamic => notification}/registerPush.ts | 0 .../api/app/bsky/notification/updateSeen.ts | 41 + .../app/bsky/preferences/getPreferences.ts | 9 +- .../{munged => preferences}/getProfile.ts | 4 +- .../{munged => preferences}/getProfiles.ts | 4 +- .../app/bsky/preferences/getSuggestions.ts | 19 + .../pds/src/api/app/bsky/preferences/index.ts | 10 + .../api/app/bsky/preferences/searchActors.ts | 19 + .../bsky/preferences/searchActorsTypeahead.ts | 20 + .../src/api/app/bsky/unspecced/getPopular.ts | 23 + .../unspecced/getPopularFeedGenerators.ts | 21 + .../pds/src/api/app/bsky/unspecced/index.ts | 10 + .../util.ts => util/read-after-write.ts} | 0 .../{dynamic/util.ts => util/resolver.ts} | 0 .../api/com/atproto/identity/resolveHandle.ts | 5 + .../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/getRecord.ts | 26 +- .../pds/src/api/com/atproto/repo/putRecord.ts | 12 + .../api/com/atproto/server/createSession.ts | 4 +- .../atproto/server/getAccountInviteCodes.ts | 2 +- packages/pds/src/config.ts | 498 +++++++++++ packages/pds/src/context.ts | 111 +++ packages/pds/src/db/database-schema.ts | 15 + .../20230922T033938477Z-remove-appview.ts | 29 + packages/pds/src/db/migrations/index.ts | 67 ++ packages/pds/src/image/index.ts | 20 + packages/pds/src/index.ts | 146 ++++ packages/pds/src/lexicon/index.ts | 61 +- packages/pds/src/lexicon/lexicons.ts | 490 ++++++++++- .../src/lexicon/types/app/bsky/actor/defs.ts | 50 ++ .../types/app/bsky/actor/searchActors.ts | 3 + .../app/bsky/actor/searchActorsTypeahead.ts | 3 + .../src/lexicon/types/app/bsky/feed/defs.ts | 40 + .../types/app/bsky/feed/getListFeed.ts} | 25 +- .../src/lexicon/types/app/bsky/feed/post.ts | 2 + .../types/app/bsky/feed/searchPosts.ts | 54 ++ .../lexicon/types/app/bsky/feed/threadgate.ts | 84 ++ .../src/lexicon/types/app/bsky/graph/defs.ts | 7 +- .../lexicon/types/app/bsky/richtext/facet.ts | 18 +- .../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/repo/prepare.ts | 24 +- packages/pds/src/runtime-flags.ts | 41 +- packages/pds/src/services/account/index.ts | 99 +++ packages/pds/src/services/index.ts | 12 + packages/pds/src/services/record/index.ts | 8 + packages/pds/src/services/util/search.ts | 144 ++++ packages/pds/src/sql-repo-storage.ts | 25 +- packages/pds/tests/_util.ts | 35 + packages/pds/tests/account-deletion.test.ts | 14 +- packages/pds/tests/admin/moderation.test.ts | 24 + packages/pds/tests/create-post.test.ts | 45 + packages/pds/tests/handles.test.ts | 29 + .../proxied/__snapshots__/views.test.ts.snap | 792 +++++++++++++++++- packages/pds/tests/proxied/admin.test.ts | 64 +- packages/pds/tests/proxied/views.test.ts | 37 +- packages/pds/tests/seeds/client.ts | 53 ++ packages/pds/tests/server.test.ts | 35 + packages/repo/CHANGELOG.md | 9 + packages/repo/README.md | 13 +- packages/repo/package.json | 20 +- packages/repo/tests/util.test.ts | 2 +- packages/syntax/CHANGELOG.md | 7 + packages/syntax/README.md | 21 +- packages/syntax/package.json | 22 +- 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 - packages/xrpc-server/CHANGELOG.md | 8 + packages/xrpc-server/README.md | 7 +- packages/xrpc-server/package.json | 20 +- packages/xrpc-server/src/rate-limiter.ts | 35 +- packages/xrpc-server/src/server.ts | 28 +- packages/xrpc-server/src/types.ts | 12 +- packages/xrpc/CHANGELOG.md | 8 + packages/xrpc/README.md | 7 +- packages/xrpc/package.json | 20 +- pnpm-lock.yaml | 36 +- services/pds/index.js | 74 ++ 302 files changed, 10938 insertions(+), 914 deletions(-) delete mode 100644 .changeset/seven-schools-switch.md create mode 100644 lexicons/app/bsky/feed/getListFeed.json create mode 100644 lexicons/app/bsky/feed/searchPosts.json create mode 100644 lexicons/app/bsky/feed/threadgate.json delete mode 100644 lexicons/app/bsky/unspecced/applyLabels.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/CHANGELOG.md rename packages/api/src/client/types/app/bsky/{unspecced/applyLabels.ts => feed/getListFeed.ts} (52%) create mode 100644 packages/api/src/client/types/app/bsky/feed/searchPosts.ts create mode 100644 packages/api/src/client/types/app/bsky/feed/threadgate.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/aws/CHANGELOG.md create mode 100644 packages/bsky/CHANGELOG.md create mode 100644 packages/bsky/src/api/app/bsky/feed/getListFeed.ts create mode 100644 packages/bsky/src/db/migrations/20230906T222220386Z-thread-gating.ts create mode 100644 packages/bsky/src/db/migrations/20230920T213858047Z-add-tags-to-post.ts create mode 100644 packages/bsky/src/db/tables/thread-gate.ts rename packages/{pds/src/lexicon/types/app/bsky/unspecced/applyLabels.ts => bsky/src/lexicon/types/app/bsky/feed/getListFeed.ts} (61%) create mode 100644 packages/bsky/src/lexicon/types/app/bsky/feed/searchPosts.ts create mode 100644 packages/bsky/src/lexicon/types/app/bsky/feed/threadgate.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/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__/list-feed.test.ts.snap create mode 100644 packages/bsky/tests/views/__snapshots__/threadgating.test.ts.snap create mode 100644 packages/bsky/tests/views/list-feed.test.ts create mode 100644 packages/bsky/tests/views/threadgating.test.ts rename packages/{bsky/src/services/indexing/util.ts => common/src/dates.ts} (100%) create mode 100644 packages/dev-env/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 create mode 100644 packages/lex-cli/CHANGELOG.md create mode 100644 packages/lexicon/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 create mode 100644 packages/pds/CHANGELOG.md create mode 100644 packages/pds/src/api/app/bsky/actor/getPreferences.ts create mode 100644 packages/pds/src/api/app/bsky/actor/getProfile.ts create mode 100644 packages/pds/src/api/app/bsky/actor/getProfiles.ts create mode 100644 packages/pds/src/api/app/bsky/actor/getSuggestions.ts create mode 100644 packages/pds/src/api/app/bsky/actor/index.ts create mode 100644 packages/pds/src/api/app/bsky/actor/putPreferences.ts create mode 100644 packages/pds/src/api/app/bsky/actor/searchActors.ts create mode 100644 packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts delete mode 100644 packages/pds/src/api/app/bsky/dynamic/index.ts create mode 100644 packages/pds/src/api/app/bsky/feed/getActorFeeds.ts rename packages/pds/src/api/app/bsky/{munged => feed}/getActorLikes.ts (92%) rename packages/pds/src/api/app/bsky/{munged => feed}/getAuthorFeed.ts (94%) create mode 100644 packages/pds/src/api/app/bsky/feed/getFeed.ts create mode 100644 packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts create mode 100644 packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts create mode 100644 packages/pds/src/api/app/bsky/feed/getLikes.ts create mode 100644 packages/pds/src/api/app/bsky/feed/getListFeed.ts rename packages/pds/src/api/app/bsky/{munged => feed}/getPostThread.ts (96%) create mode 100644 packages/pds/src/api/app/bsky/feed/getPosts.ts create mode 100644 packages/pds/src/api/app/bsky/feed/getRepostedBy.ts create mode 100644 packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts rename packages/pds/src/api/app/bsky/{munged => feed}/getTimeline.ts (87%) create mode 100644 packages/pds/src/api/app/bsky/feed/index.ts create mode 100644 packages/pds/src/api/app/bsky/graph/getBlocks.ts create mode 100644 packages/pds/src/api/app/bsky/graph/getFollowers.ts create mode 100644 packages/pds/src/api/app/bsky/graph/getFollows.ts create mode 100644 packages/pds/src/api/app/bsky/graph/getList.ts create mode 100644 packages/pds/src/api/app/bsky/graph/getListBlocks.ts create mode 100644 packages/pds/src/api/app/bsky/graph/getListMutes.ts create mode 100644 packages/pds/src/api/app/bsky/graph/getLists.ts create mode 100644 packages/pds/src/api/app/bsky/graph/getMutes.ts create mode 100644 packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts create mode 100644 packages/pds/src/api/app/bsky/graph/index.ts create mode 100644 packages/pds/src/api/app/bsky/graph/muteActor.ts create mode 100644 packages/pds/src/api/app/bsky/graph/muteActorList.ts create mode 100644 packages/pds/src/api/app/bsky/graph/unmuteActor.ts create mode 100644 packages/pds/src/api/app/bsky/graph/unmuteActorList.ts delete mode 100644 packages/pds/src/api/app/bsky/munged/index.ts create mode 100644 packages/pds/src/api/app/bsky/notification/getUnreadCount.ts create mode 100644 packages/pds/src/api/app/bsky/notification/index.ts create mode 100644 packages/pds/src/api/app/bsky/notification/listNotifications.ts rename packages/pds/src/api/app/bsky/{dynamic => notification}/registerPush.ts (100%) create mode 100644 packages/pds/src/api/app/bsky/notification/updateSeen.ts rename packages/pds/src/api/app/bsky/{munged => preferences}/getProfile.ts (90%) rename packages/pds/src/api/app/bsky/{munged => preferences}/getProfiles.ts (90%) create mode 100644 packages/pds/src/api/app/bsky/preferences/getSuggestions.ts create mode 100644 packages/pds/src/api/app/bsky/preferences/searchActors.ts create mode 100644 packages/pds/src/api/app/bsky/preferences/searchActorsTypeahead.ts 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/api/app/bsky/{munged/util.ts => util/read-after-write.ts} (100%) rename packages/pds/src/api/app/bsky/{dynamic/util.ts => util/resolver.ts} (100%) create mode 100644 packages/pds/src/config.ts create mode 100644 packages/pds/src/db/migrations/20230922T033938477Z-remove-appview.ts rename packages/{bsky/src/lexicon/types/app/bsky/unspecced/applyLabels.ts => pds/src/lexicon/types/app/bsky/feed/getListFeed.ts} (61%) create mode 100644 packages/pds/src/lexicon/types/app/bsky/feed/searchPosts.ts create mode 100644 packages/pds/src/lexicon/types/app/bsky/feed/threadgate.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 create mode 100644 packages/pds/src/services/util/search.ts create mode 100644 packages/pds/tests/create-post.test.ts create mode 100644 packages/repo/CHANGELOG.md create mode 100644 packages/syntax/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 create mode 100644 packages/xrpc-server/CHANGELOG.md create mode 100644 packages/xrpc/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/.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/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/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 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/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/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/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/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/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/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/feed/threadgate.json b/lexicons/app/bsky/feed/threadgate.json new file mode 100644 index 00000000000..7969b6360a6 --- /dev/null +++ b/lexicons/app/bsky/feed/threadgate.json @@ -0,0 +1,45 @@ +{ + "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.", + "properties": {} + }, + "followingRule": { + "type": "object", + "description": "Allow replies from actors you follow.", + "properties": {} + }, + "listRule": { + "type": "object", + "description": "Allow replies from actors on a list.", + "required": ["list"], + "properties": { + "list": { "type": "string", "format": "at-uri" } + } + } + } +} 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/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/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/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/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/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/package.json b/package.json index 71b802b991f..4906b1827f5 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": { diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md new file mode 100644 index 00000000000..1d6d7788839 --- /dev/null +++ b/packages/api/CHANGELOG.md @@ -0,0 +1,41 @@ +# @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 + +- [#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 + +- [#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 + +- [#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 + +- 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 + +- [#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/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 diff --git a/packages/api/package.json b/packages/api/package.json index 3f4f0be06d8..13624588c0c 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,19 @@ { "name": "@atproto/api", - "version": "0.6.12", + "version": "0.6.18", + "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/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 b7dd1bc1931..37b8d9c3620 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -6,7 +6,24 @@ import { AppBskyActorDefs, ComAtprotoRepoPutRecord, } from './client' -import { BskyPreferences, BskyLabelPreference } from './types' +import { + BskyPreferences, + BskyLabelPreference, + BskyFeedViewPreference, + BskyThreadViewPreference, +} 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 { @@ -254,6 +271,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 +312,20 @@ export class BskyAgent extends AtpAgent { if (pref.birthDate) { prefs.birthDate = new Date(pref.birthDate) } + } else if ( + 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 } } } return prefs @@ -406,6 +443,41 @@ export class BskyAgent extends AtpAgent { .concat([personalDetailsPref]) }) } + + async setFeedViewPrefs(feed: string, pref: Partial) { + 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: Partial) { + 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/index.ts b/packages/api/src/client/index.ts index dfd5e51ff57..094d495ade5 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -95,6 +95,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' @@ -103,6 +104,8 @@ 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' import * as AppBskyGraphFollow from './types/app/bsky/graph/follow' @@ -127,10 +130,12 @@ 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' 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' @@ -220,6 +225,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' @@ -228,6 +234,8 @@ 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' export * as AppBskyGraphFollow from './types/app/bsky/graph/follow' @@ -252,10 +260,12 @@ 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' 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', @@ -273,6 +283,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 { @@ -1224,6 +1235,7 @@ export class FeedNS { like: LikeRecord post: PostRecord repost: RepostRecord + threadgate: ThreadgateRecord constructor(service: AtpServiceClient) { this._service = service @@ -1231,6 +1243,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( @@ -1332,6 +1345,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, @@ -1386,6 +1410,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 { @@ -1632,6 +1667,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 @@ -2174,17 +2274,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, @@ -2222,4 +2311,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 c682f239a3b..5ea6bab95e3 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', @@ -3713,6 +3717,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', ], }, }, @@ -3769,6 +3775,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: { @@ -3975,18 +4028,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', @@ -4027,12 +4086,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, }, }, }, @@ -4416,6 +4480,10 @@ export const schemaDict = { ref: 'lex:com.atproto.label.defs#label', }, }, + threadgate: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#threadgateView', + }, }, }, viewerState: { @@ -4512,6 +4580,10 @@ export const schemaDict = { ], }, }, + viewer: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#viewerThreadState', + }, }, }, notFoundPost: { @@ -4560,6 +4632,14 @@ export const schemaDict = { }, }, }, + viewerThreadState: { + type: 'object', + properties: { + canReply: { + type: 'boolean', + }, + }, + }, generatorView: { type: 'object', required: ['uri', 'cid', 'did', 'creator', 'displayName', 'indexedAt'], @@ -4645,6 +4725,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: { @@ -5186,6 +5289,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', @@ -5507,6 +5663,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', @@ -5588,6 +5754,126 @@ 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', + 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.', + properties: {}, + }, + followingRule: { + type: 'object', + description: 'Allow replies from actors you follow.', + properties: {}, + }, + 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', @@ -5713,13 +5999,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: { @@ -6579,6 +6873,7 @@ export const schemaDict = { refs: [ 'lex:app.bsky.richtext.facet#mention', 'lex:app.bsky.richtext.facet#link', + 'lex:app.bsky.richtext.facet#tag', ], }, }, @@ -6606,6 +6901,18 @@ export const schemaDict = { }, }, }, + tag: { + type: 'object', + description: 'A hashtag.', + required: ['tag'], + properties: { + tag: { + type: 'string', + maxLength: 640, + maxGraphemes: 64, + }, + }, + }, byteSlice: { type: 'object', description: @@ -6624,27 +6931,27 @@ export const schemaDict = { }, }, }, - AppBskyUnspeccedApplyLabels: { + AppBskyUnspeccedDefs: { lexicon: 1, - id: 'app.bsky.unspecced.applyLabels', + id: 'app.bsky.unspecced.defs', 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', - }, - }, - }, + skeletonSearchPost: { + type: 'object', + required: ['uri'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + }, + }, + skeletonSearchActor: { + type: 'object', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', }, }, }, @@ -6656,7 +6963,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: { @@ -6791,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) @@ -6889,6 +7323,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', @@ -6897,6 +7332,8 @@ 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', AppBskyGraphFollow: 'app.bsky.graph.follow', @@ -6923,9 +7360,12 @@ 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: '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/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/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/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/unspecced/applyLabels.ts b/packages/api/src/client/types/app/bsky/feed/getListFeed.ts similarity index 52% rename from packages/api/src/client/types/app/bsky/unspecced/applyLabels.ts rename to packages/api/src/client/types/app/bsky/feed/getListFeed.ts index c8e72746a42..511e9526c6d 100644 --- a/packages/api/src/client/types/app/bsky/unspecced/applyLabels.ts +++ b/packages/api/src/client/types/app/bsky/feed/getListFeed.ts @@ -6,28 +6,41 @@ 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' +import * as AppBskyFeedDefs from './defs' -export interface QueryParams {} +export interface QueryParams { + list: string + limit?: number + cursor?: string +} + +export type InputSchema = undefined -export interface InputSchema { - labels: ComAtprotoLabelDefs.Label[] +export interface OutputSchema { + cursor?: string + feed: AppBskyFeedDefs.FeedViewPost[] [k: string]: unknown } export interface CallOptions { headers?: Headers - qp?: QueryParams - encoding: 'application/json' } 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/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/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/feed/threadgate.ts b/packages/api/src/client/types/app/bsky/feed/threadgate.ts new file mode 100644 index 00000000000..a1afec85673 --- /dev/null +++ b/packages/api/src/client/types/app/bsky/feed/threadgate.ts @@ -0,0 +1,84 @@ +/** + * 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 { + [k: string]: unknown +} + +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 { + [k: string]: unknown +} + +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/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/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/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/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/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/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/src/types.ts b/packages/api/src/types.ts index 0310d6743b8..10d0bbd90fe 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -80,13 +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: BskyThreadViewPreference 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 96082e92f4c..53dc777e61b 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/bsky-agent.test.ts @@ -42,8 +42,12 @@ describe('agent', () => { email: 'user1@test.com', password: 'password', }) +<<<<<<< HEAD const displayName1 = await await getProfileDisplayName(agent) +======= + const displayName1 = await getProfileDisplayName(agent) +>>>>>>> main expect(displayName1).toBeFalsy() await agent.upsertProfile((existing) => { @@ -53,7 +57,11 @@ describe('agent', () => { } }) +<<<<<<< HEAD const displayName2 = await await getProfileDisplayName(agent) +======= + const displayName2 = await getProfileDisplayName(agent) +>>>>>>> main expect(displayName2).toBe('Bob') await agent.upsertProfile((existing) => { @@ -63,7 +71,11 @@ describe('agent', () => { } }) +<<<<<<< HEAD const displayName3 = await await getProfileDisplayName(agent) +======= + const displayName3 = await getProfileDisplayName(agent) +>>>>>>> main expect(displayName3).toBe('BOB') }) @@ -76,12 +88,16 @@ describe('agent', () => { password: 'password', }) +<<<<<<< HEAD const displayName1 = await await getProfileDisplayName(agent) +======= + const displayName1 = await getProfileDisplayName(agent) +>>>>>>> main expect(displayName1).toBeFalsy() 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 || '', @@ -102,7 +118,11 @@ describe('agent', () => { }) expect(ranTwice).toBe(true) +<<<<<<< HEAD const displayName2 = await await getProfileDisplayName(agent) +======= + const displayName2 = await getProfileDisplayName(agent) +>>>>>>> main expect(displayName2).toBe('Bob') }) @@ -115,10 +135,14 @@ describe('agent', () => { password: 'password', }) +<<<<<<< HEAD const displayName1 = await await getProfileDisplayName(agent) +======= + const displayName1 = await getProfileDisplayName(agent) +>>>>>>> main expect(displayName1).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', @@ -144,7 +168,7 @@ describe('agent', () => { password: 'password', }) - const p = agent.upsertProfile((existing) => { + const p = agent.upsertProfile((_existing) => { return { displayName: { string: 'Bob' }, } as unknown as AppBskyActorProfile.Record @@ -230,6 +254,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) @@ -238,6 +275,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) @@ -246,6 +296,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') @@ -256,6 +319,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' @@ -268,6 +344,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') @@ -282,6 +371,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') @@ -296,6 +398,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') @@ -310,6 +425,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') @@ -324,6 +452,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') @@ -338,6 +479,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') @@ -358,6 +512,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') @@ -372,6 +539,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' }) @@ -386,6 +566,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, + }, }) }) @@ -456,6 +805,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({ @@ -468,6 +845,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) @@ -481,6 +871,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') @@ -494,6 +897,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') @@ -507,6 +923,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' }) @@ -520,29 +949,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/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[] { 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..3cd4dddcf2e 100644 --- a/packages/aws/package.json +++ b/packages/aws/package.json @@ -1,17 +1,23 @@ { "name": "@atproto/aws", - "version": "0.1.0", - "main": "src/index.ts", - "publishConfig": { - "main": "dist/index.js", - "types": "dist/src/index.d.ts" - }, + "version": "0.1.1", "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/CHANGELOG.md b/packages/bsky/CHANGELOG.md new file mode 100644 index 00000000000..9d85cfa4a70 --- /dev/null +++ b/packages/bsky/CHANGELOG.md @@ -0,0 +1,49 @@ +# @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 + +- [#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 + +- Updated dependencies [[`56e2cf89`](https://github.com/bluesky-social/atproto/commit/56e2cf8999f6d7522529a9be8652c47545f82242)]: + - @atproto/api@0.6.16 + +## 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 + +- 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 + +- Updated dependencies [[`3877210e`](https://github.com/bluesky-social/atproto/commit/3877210e7fb3c76dfb1a11eb9ba3f18426301d9f)]: + - @atproto/api@0.6.13 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 508d9ef569f..5281805279f 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -1,10 +1,16 @@ { "name": "@atproto/bsky", - "version": "0.0.3", + "version": "0.0.9", "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", @@ -44,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/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/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, 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/app/bsky/feed/getPostThread.ts b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts index 2d10ff98006..0c26285b384 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,29 @@ 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] + // @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) { return { $type: 'app.bsky.feed.defs#notFoundPost', uri: threadData.post.postUri, @@ -135,7 +171,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 +184,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 +220,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 +305,28 @@ 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 + // @TODO re-enable invalidReplyRoot check + // if (anchor?.invalidReplyRoot || anchor?.violatesThreadGate) return false + if (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 +352,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/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/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/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/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..30befc19110 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({ @@ -245,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/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..4c417278d67 --- /dev/null +++ b/packages/bsky/src/db/migrations/20230906T222220386Z-thread-gating.ts @@ -0,0 +1,27 @@ +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') + .execute() + await db.schema + .alterTable('post') + .addColumn('violatesThreadGate', 'boolean') + .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/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 505f7c84909..9e8bfe9cf7f 100644 --- a/packages/bsky/src/db/migrations/index.ts +++ b/packages/bsky/src/db/migrations/index.ts @@ -27,3 +27,5 @@ 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' +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 f878b8dab33..6c01b76c8e0 100644 --- a/packages/bsky/src/db/tables/post.ts +++ b/packages/bsky/src/db/tables/post.ts @@ -12,6 +12,9 @@ export interface Post { replyParent: string | null replyParentCid: string | null langs: string[] | null + tags: string[] | null + invalidReplyRoot: boolean | null + violatesThreadGate: boolean | null 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/index.ts b/packages/bsky/src/lexicon/index.ts index c0840abea2c..c8c7d230e36 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -84,11 +84,13 @@ 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' 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' @@ -106,10 +108,11 @@ 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' +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', @@ -127,6 +130,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 { @@ -1123,6 +1127,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, @@ -1177,6 +1192,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 { @@ -1397,17 +1423,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, @@ -1440,6 +1455,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 c682f239a3b..5ea6bab95e3 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', @@ -3713,6 +3717,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', ], }, }, @@ -3769,6 +3775,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: { @@ -3975,18 +4028,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', @@ -4027,12 +4086,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, }, }, }, @@ -4416,6 +4480,10 @@ export const schemaDict = { ref: 'lex:com.atproto.label.defs#label', }, }, + threadgate: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#threadgateView', + }, }, }, viewerState: { @@ -4512,6 +4580,10 @@ export const schemaDict = { ], }, }, + viewer: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#viewerThreadState', + }, }, }, notFoundPost: { @@ -4560,6 +4632,14 @@ export const schemaDict = { }, }, }, + viewerThreadState: { + type: 'object', + properties: { + canReply: { + type: 'boolean', + }, + }, + }, generatorView: { type: 'object', required: ['uri', 'cid', 'did', 'creator', 'displayName', 'indexedAt'], @@ -4645,6 +4725,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: { @@ -5186,6 +5289,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', @@ -5507,6 +5663,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', @@ -5588,6 +5754,126 @@ 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', + 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.', + properties: {}, + }, + followingRule: { + type: 'object', + description: 'Allow replies from actors you follow.', + properties: {}, + }, + 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', @@ -5713,13 +5999,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: { @@ -6579,6 +6873,7 @@ export const schemaDict = { refs: [ 'lex:app.bsky.richtext.facet#mention', 'lex:app.bsky.richtext.facet#link', + 'lex:app.bsky.richtext.facet#tag', ], }, }, @@ -6606,6 +6901,18 @@ export const schemaDict = { }, }, }, + tag: { + type: 'object', + description: 'A hashtag.', + required: ['tag'], + properties: { + tag: { + type: 'string', + maxLength: 640, + maxGraphemes: 64, + }, + }, + }, byteSlice: { type: 'object', description: @@ -6624,27 +6931,27 @@ export const schemaDict = { }, }, }, - AppBskyUnspeccedApplyLabels: { + AppBskyUnspeccedDefs: { lexicon: 1, - id: 'app.bsky.unspecced.applyLabels', + id: 'app.bsky.unspecced.defs', 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', - }, - }, - }, + skeletonSearchPost: { + type: 'object', + required: ['uri'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + }, + }, + skeletonSearchActor: { + type: 'object', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', }, }, }, @@ -6656,7 +6963,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: { @@ -6791,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) @@ -6889,6 +7323,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', @@ -6897,6 +7332,8 @@ 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', AppBskyGraphFollow: 'app.bsky.graph.follow', @@ -6923,9 +7360,12 @@ 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: '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/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/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/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/pds/src/lexicon/types/app/bsky/unspecced/applyLabels.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getListFeed.ts similarity index 61% rename from packages/pds/src/lexicon/types/app/bsky/unspecced/applyLabels.ts rename to packages/bsky/src/lexicon/types/app/bsky/feed/getListFeed.ts index 1d359a9547d..e24c3f8ed22 100644 --- a/packages/pds/src/lexicon/types/app/bsky/unspecced/applyLabels.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getListFeed.ts @@ -7,26 +7,37 @@ 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' +import * as AppBskyFeedDefs from './defs' -export interface QueryParams {} +export interface QueryParams { + list: string + limit: number + cursor?: string +} + +export type InputSchema = undefined -export interface InputSchema { - labels: ComAtprotoLabelDefs.Label[] +export interface OutputSchema { + cursor?: string + feed: AppBskyFeedDefs.FeedViewPost[] [k: string]: unknown } -export interface HandlerInput { +export type HandlerInput = undefined + +export interface HandlerSuccess { encoding: 'application/json' - body: InputSchema + body: OutputSchema + headers?: { [key: string]: string } } export interface HandlerError { status: number message?: string + error?: 'UnknownList' } -export type HandlerOutput = HandlerError | void +export type HandlerOutput = HandlerError | HandlerSuccess export type HandlerReqCtx = { auth: HA params: QueryParams 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/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/feed/threadgate.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/threadgate.ts new file mode 100644 index 00000000000..6a190d6e98a --- /dev/null +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/threadgate.ts @@ -0,0 +1,84 @@ +/** + * 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 { + [k: string]: unknown +} + +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 { + [k: string]: unknown +} + +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/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/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/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/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/src/services/feed/index.ts b/packages/bsky/src/services/feed/index.ts index db32f1971bc..dab9673d9db 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,10 +141,13 @@ 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', 'post_agg.replyCount as replyCount', + 'post.tags as tags', db .selectFrom('repost') .if(!viewer, (q) => q.where(noMatch)) @@ -152,9 +166,12 @@ export class FeedService { .execute() return posts.reduce((acc, cur) => { const { recordJson, ...post } = cur + const record = jsonStringToLex(recordJson) as PostRecord const info: PostInfo = { ...post, - record: jsonStringToLex(recordJson) as Record, + invalidReplyRoot: post.invalidReplyRoot ?? false, + violatesThreadGate: post.violatesThreadGate ?? false, + record, viewer, } return Object.assign(acc, { [post.uri]: info }) @@ -216,31 +233,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 +419,10 @@ export class FeedService { uri, actorInfos, feedState.posts, + feedState.threadgates, feedState.embeds, feedState.labels, + feedState.lists, ) recordEmbedViews[uri] = this.views.getRecordEmbedView( uri, @@ -416,6 +439,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/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 7ce431fdcd8..7173c04a991 100644 --- a/packages/bsky/src/services/indexing/plugins/post.ts +++ b/packages/bsky/src/services/indexing/plugins/post.ts @@ -1,7 +1,13 @@ 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 { toSimplifiedISOSafe } from '@atproto/common' +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' @@ -14,12 +20,13 @@ 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' 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 @@ -69,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([ @@ -96,6 +106,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 +406,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/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 new file mode 100644 index 00000000000..37f3ddb062e --- /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 { 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 { 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/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/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/auto-moderator/takedowns.test.ts b/packages/bsky/tests/auto-moderator/takedowns.test.ts index a42a1d01c8a..1af6c4d6498 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)) { diff --git a/packages/bsky/tests/seeds/client.ts b/packages/bsky/tests/seeds/client.ts index ddd1acb9192..ee551214789 100644 --- a/packages/bsky/tests/seeds/client.ts +++ b/packages/bsky/tests/seeds/client.ts @@ -74,6 +74,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 +88,7 @@ export class SeedClient { this.likes = {} this.replies = {} this.reposts = {} + this.lists = {} this.dids = {} } @@ -319,6 +324,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__/block-lists.test.ts.snap b/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap index fae6e7f4fa9..364ad4b7d63 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, + }, }, } `; @@ -503,7 +512,6 @@ Object { "viewer": Object { "blockedBy": false, "blocking": "record(0)", - "following": "record(4)", "muted": false, }, }, 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__/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/__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/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/bsky/tests/views/blocks.test.ts b/packages/bsky/tests/views/blocks.test.ts index 127aac4fc6b..0109b93f82a 100644 --- a/packages/bsky/tests/views/blocks.test.ts +++ b/packages/bsky/tests/views/blocks.test.ts @@ -163,22 +163,63 @@ 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 }, { 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/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/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/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/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/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..5b88803035f 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", @@ -22,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/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/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/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/dev-env/CHANGELOG.md b/packages/dev-env/CHANGELOG.md new file mode 100644 index 00000000000..67523be96e0 --- /dev/null +++ b/packages/dev-env/CHANGELOG.md @@ -0,0 +1,57 @@ +# @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 + +- 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 + +- 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 + +- 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 + +- 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 + +- 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/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/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 adc1a4e21c8..6ae8cc5d151 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -1,24 +1,28 @@ { "name": "@atproto/dev-env", - "version": "0.2.3", + "version": "0.2.9", + "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", "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.ts b/packages/dev-env/src/bin.ts index 0aa81afb668..4e31daa5170 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,10 +12,18 @@ const run = async () => { [ created by Bluesky ]`) - const network = await TestNetworkNoAppView.create({ + const network = await TestNetwork.create({ pds: { port: 2583, +<<<<<<< HEAD hostname: 'localhost', +======= + publicUrl: 'http://localhost:2583', + dbPostgresSchema: 'pds', + }, + bsky: { + dbPostgresSchema: 'bsky', +>>>>>>> main }, plc: { port: 2582 }, }) @@ -27,6 +35,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 3b6d739c499..6e33b9123a1 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,6 +186,7 @@ export async function generateMockSetup(env: TestNetworkNoAppView) { }, ) +<<<<<<< HEAD // @TODO get from appview instead // const ctx = env.pds.ctx // if (ctx) { @@ -211,6 +212,30 @@ export async function generateMockSetup(env: TestNetworkNoAppView) { // ]) // .execute() // } +======= + const ctx = env.bsky.ctx + if (ctx) { + 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(), + }, + ]) + } +>>>>>>> main // a set of replies for (let i = 0; i < 100; i++) { diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index 46a73e8813b..2a1a759241c 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -39,6 +39,7 @@ export class TestPds { moderatorPassword: MOD_PASSWORD, triagePassword: TRIAGE_PASSWORD, jwtSecret: 'jwt-secret', +<<<<<<< HEAD serviceHandleDomains: ['.test'], sequencerLeaderLockId: uniqueLockId(), bskyAppViewCdnUrlPattern: 'http://cdn.appview.com/%s/%s/%s', @@ -65,6 +66,41 @@ export class TestPds { if (migrationDb !== server.ctx.db) { await migrationDb.close() } +======= + availableUserDomains: ['.test'], + rateLimitsEnabled: false, + appUrlPasswordReset: 'app://forgot-password', + emailNoReplyAddress: 'noreply@blueskyweb.xyz', + publicUrl: 'https://pds.public.url', + dbPostgresUrl: cfg.dbPostgresUrl, + maxSubscriptionBuffer: 200, + repoBackfillLimitMs: 1000 * 60 * 60, // 1hr + sequencerLeaderLockId: uniqueLockId(), + dbTxLockNonce: await randomStr(32, 'base32'), + bskyAppViewEndpoint: cfg.bskyAppViewEndpoint ?? 'http://fake_address', + bskyAppViewDid: cfg.bskyAppViewDid ?? 'did:example:fake', + bskyAppViewCdnUrlPattern: 'http://cdn.appview.com/%s/%s/%s', + ...cfg, + }) + + const blobstore = new pds.MemoryBlobStore() + const db = config.dbPostgresUrl + ? pds.Database.postgres({ + url: config.dbPostgresUrl, + schema: config.dbPostgresSchema, + txLockNonce: config.dbTxLockNonce, + }) + : pds.Database.memory() + await db.migrateToLatestOrThrow() + + const server = pds.PDS.create({ + db, + blobstore, + repoSigningKey, + plcRotationKey, + config, + }) +>>>>>>> main await server.start() diff --git a/packages/dev-env/src/types.ts b/packages/dev-env/src/types.ts index 9430f919ed4..1ddb83937a6 100644 --- a/packages/dev-env/src/types.ts +++ b/packages/dev-env/src/types.ts @@ -10,7 +10,10 @@ export type PlcConfig = { export type PdsConfig = Partial & { didPlcUrl: string migration?: string +<<<<<<< HEAD enableLabelsCache?: boolean +======= +>>>>>>> main } export type BskyConfig = Partial & { 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 1642c1046d0..00000000000 --- a/packages/identifier/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "@atproto/identifier", - "version": "0.2.0", - "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/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/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..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.0", + "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/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/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 4b5f7470671..ebce21d48af 100644 --- a/packages/lexicon/package.json +++ b/packages/lexicon/package.json @@ -1,6 +1,18 @@ { "name": "@atproto/lexicon", - "version": "0.2.0", + "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/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/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/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 af156718f49..00000000000 --- a/packages/nsid/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "@atproto/nsid", - "version": "0.1.0", - "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/pds/CHANGELOG.md b/packages/pds/CHANGELOG.md new file mode 100644 index 00000000000..2680326d054 --- /dev/null +++ b/packages/pds/CHANGELOG.md @@ -0,0 +1,50 @@ +# @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 + +- [#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 + +- Updated dependencies [[`56e2cf89`](https://github.com/bluesky-social/atproto/commit/56e2cf8999f6d7522529a9be8652c47545f82242)]: + - @atproto/api@0.6.16 + +## 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 + +- 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 + +- Updated dependencies [[`3877210e`](https://github.com/bluesky-social/atproto/commit/3877210e7fb3c76dfb1a11eb9ba3f18426301d9f)]: + - @atproto/api@0.6.13 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 604d4461a8e..6a0daa031a4 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -1,10 +1,20 @@ { "name": "@atproto/pds", +<<<<<<< HEAD "version": "0.3.0-beta.3", +======= + "version": "0.1.18", +>>>>>>> main "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", @@ -29,10 +39,10 @@ "@atproto/aws": "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", @@ -49,10 +59,8 @@ "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", - "lru-cache": "^10.0.1", "multiformats": "^9.9.0", "nodemailer": "^6.8.0", "nodemailer-html-to-text": "^3.2.0", @@ -66,9 +74,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/app/bsky/actor/getPreferences.ts b/packages/pds/src/api/app/bsky/actor/getPreferences.ts new file mode 100644 index 00000000000..1bca50f0bd1 --- /dev/null +++ b/packages/pds/src/api/app/bsky/actor/getPreferences.ts @@ -0,0 +1,26 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { AuthScope } from '../../../../auth' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.actor.getPreferences({ + auth: ctx.accessVerifier, + handler: async ({ auth }) => { + const requester = auth.credentials.did + const { services, db } = ctx + 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 }, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/actor/getProfile.ts b/packages/pds/src/api/app/bsky/actor/getProfile.ts new file mode 100644 index 00000000000..c4e38e4a101 --- /dev/null +++ b/packages/pds/src/api/app/bsky/actor/getProfile.ts @@ -0,0 +1,38 @@ +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' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.actor.getProfile({ + auth: ctx.accessOrRoleVerifier, + handler: async ({ req, auth, params }) => { + const requester = + auth.credentials.type === 'access' ? auth.credentials.did : null + 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, + } + }, + }) +} + +const getProfileMunge = async ( + ctx: AppContext, + original: OutputSchema, + local: LocalRecords, +): Promise => { + if (!local.profile) return original + return ctx.services + .local(ctx.db) + .updateProfileDetailed(original, local.profile.record) +} \ No newline at end of file diff --git a/packages/pds/src/api/app/bsky/actor/getProfiles.ts b/packages/pds/src/api/app/bsky/actor/getProfiles.ts new file mode 100644 index 00000000000..2b1bbf35052 --- /dev/null +++ b/packages/pds/src/api/app/bsky/actor/getProfiles.ts @@ -0,0 +1,46 @@ +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) { + server.app.bsky.actor.getProfiles({ + auth: ctx.accessVerifier, + handler: async ({ auth, params }) => { + const requester = auth.credentials.did + 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 getProfilesMunge = async ( + ctx: AppContext, + original: OutputSchema, + local: LocalRecords, + requester: string, +): Promise => { + const localProf = local.profile + if (!localProf) return original + const profiles = original.profiles.map((prof) => { + if (prof.did !== requester) return prof + return ctx.services + .local(ctx.db) + .updateProfileDetailed(prof, localProf.record) + }) + return { + ...original, + profiles, + } +} \ No newline at end of file diff --git a/packages/pds/src/api/app/bsky/actor/getSuggestions.ts b/packages/pds/src/api/app/bsky/actor/getSuggestions.ts new file mode 100644 index 00000000000..e6c72e5c830 --- /dev/null +++ b/packages/pds/src/api/app/bsky/actor/getSuggestions.ts @@ -0,0 +1,19 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.actor.getSuggestions({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appViewAgent.api.app.bsky.actor.getSuggestions( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/actor/index.ts b/packages/pds/src/api/app/bsky/actor/index.ts new file mode 100644 index 00000000000..041744899f6 --- /dev/null +++ b/packages/pds/src/api/app/bsky/actor/index.ts @@ -0,0 +1,20 @@ +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/api/app/bsky/actor/putPreferences.ts b/packages/pds/src/api/app/bsky/actor/putPreferences.ts new file mode 100644 index 00000000000..589298b44c0 --- /dev/null +++ b/packages/pds/src/api/app/bsky/actor/putPreferences.ts @@ -0,0 +1,28 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { UserPreference } from '../../../../services/account' +import { InvalidRequestError } from '@atproto/xrpc-server' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.actor.putPreferences({ + auth: ctx.accessVerifierCheckTakedown, + handler: async ({ auth, input }) => { + const { preferences } = input.body + const requester = auth.credentials.did + const { services, db } = ctx + const checkedPreferences: UserPreference[] = [] + for (const pref of preferences) { + if (typeof pref.$type === 'string') { + checkedPreferences.push(pref as UserPreference) + } else { + throw new InvalidRequestError('Preference is missing a $type') + } + } + await db.transaction(async (tx) => { + await services + .account(tx) + .putPreferences(requester, checkedPreferences, 'app.bsky') + }) + }, + }) +} \ No newline at end of file diff --git a/packages/pds/src/api/app/bsky/actor/searchActors.ts b/packages/pds/src/api/app/bsky/actor/searchActors.ts new file mode 100644 index 00000000000..3f1bd2355d6 --- /dev/null +++ b/packages/pds/src/api/app/bsky/actor/searchActors.ts @@ -0,0 +1,19 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.actor.searchActors({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appViewAgent.api.app.bsky.actor.searchActors( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts b/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts new file mode 100644 index 00000000000..a637aea69c7 --- /dev/null +++ b/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts @@ -0,0 +1,20 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.actor.searchActorsTypeahead({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = + await ctx.appViewAgent.api.app.bsky.actor.searchActorsTypeahead( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/dynamic/index.ts b/packages/pds/src/api/app/bsky/dynamic/index.ts deleted file mode 100644 index 2c14eb6d850..00000000000 --- a/packages/pds/src/api/app/bsky/dynamic/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' -import registerPush from './registerPush' - -export default function (server: Server, ctx: AppContext) { - registerPush(server, ctx) -} diff --git a/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts b/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts new file mode 100644 index 00000000000..ec77754b4b2 --- /dev/null +++ b/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts @@ -0,0 +1,19 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.feed.getActorFeeds({ + auth: ctx.accessVerifier, + handler: async ({ auth, params }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.feed.getActorFeeds( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/munged/getActorLikes.ts b/packages/pds/src/api/app/bsky/feed/getActorLikes.ts similarity index 92% rename from packages/pds/src/api/app/bsky/munged/getActorLikes.ts rename to packages/pds/src/api/app/bsky/feed/getActorLikes.ts index 5a01cf5dbb0..53557c6ae4c 100644 --- a/packages/pds/src/api/app/bsky/munged/getActorLikes.ts +++ b/packages/pds/src/api/app/bsky/feed/getActorLikes.ts @@ -1,7 +1,7 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { OutputSchema } from '../../../../lexicon/types/app/bsky/feed/getAuthorFeed' -import { handleReadAfterWrite } from './util' +import { handleReadAfterWrite } from '../util/read-after-write' import { authPassthru } from '../../../../api/com/atproto/admin/util' import { LocalRecords } from '../../../../services/local' @@ -12,7 +12,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - const res = await ctx.appViewAgent.api.app.bsky.feed.getActorLikes( + const res = await ctx.appviewAgent.api.app.bsky.feed.getActorLikes( params, requester ? await ctx.serviceAuthHeaders(requester) : authPassthru(req), ) diff --git a/packages/pds/src/api/app/bsky/munged/getAuthorFeed.ts b/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts similarity index 94% rename from packages/pds/src/api/app/bsky/munged/getAuthorFeed.ts rename to packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts index 12d095027cb..7237a2df755 100644 --- a/packages/pds/src/api/app/bsky/munged/getAuthorFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts @@ -1,7 +1,7 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { OutputSchema } from '../../../../lexicon/types/app/bsky/feed/getAuthorFeed' -import { handleReadAfterWrite } from './util' +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' @@ -12,7 +12,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ req, params, auth }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - const res = await ctx.appViewAgent.api.app.bsky.feed.getAuthorFeed( + const res = await ctx.appviewAgent.api.app.bsky.feed.getAuthorFeed( params, requester ? await ctx.serviceAuthHeaders(requester) : authPassthru(req), ) diff --git a/packages/pds/src/api/app/bsky/feed/getFeed.ts b/packages/pds/src/api/app/bsky/feed/getFeed.ts new file mode 100644 index 00000000000..6ed14b0546c --- /dev/null +++ b/packages/pds/src/api/app/bsky/feed/getFeed.ts @@ -0,0 +1,25 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.feed.getFeed({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + + 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, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts b/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts new file mode 100644 index 00000000000..b9451ca16c3 --- /dev/null +++ b/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts @@ -0,0 +1,19 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.feed.getFeedGenerator({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts b/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts new file mode 100644 index 00000000000..1d085830004 --- /dev/null +++ b/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts @@ -0,0 +1,19 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.feed.getFeedGenerators({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerators( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/feed/getLikes.ts b/packages/pds/src/api/app/bsky/feed/getLikes.ts new file mode 100644 index 00000000000..75197acbcc8 --- /dev/null +++ b/packages/pds/src/api/app/bsky/feed/getLikes.ts @@ -0,0 +1,19 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.feed.getLikes({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.feed.getLikes( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/feed/getListFeed.ts b/packages/pds/src/api/app/bsky/feed/getListFeed.ts new file mode 100644 index 00000000000..7344b2476ba --- /dev/null +++ b/packages/pds/src/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/api/app/bsky/munged/getPostThread.ts b/packages/pds/src/api/app/bsky/feed/getPostThread.ts similarity index 96% rename from packages/pds/src/api/app/bsky/munged/getPostThread.ts rename to packages/pds/src/api/app/bsky/feed/getPostThread.ts index 9ed130e29b0..e8dd062da14 100644 --- a/packages/pds/src/api/app/bsky/munged/getPostThread.ts +++ b/packages/pds/src/api/app/bsky/feed/getPostThread.ts @@ -17,7 +17,11 @@ import { LocalService, RecordDescript, } from '../../../../services/local' -import { getLocalLag, getRepoRev, handleReadAfterWrite } from './util' +import { + getLocalLag, + getRepoRev, + handleReadAfterWrite, +} from '../util/read-after-write' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getPostThread({ @@ -25,7 +29,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth }) => { const requester = auth.credentials.did try { - const res = await ctx.appViewAgent.api.app.bsky.feed.getPostThread( + const res = await ctx.appviewAgent.api.app.bsky.feed.getPostThread( params, await ctx.serviceAuthHeaders(requester), ) @@ -181,7 +185,7 @@ const readAfterWriteNotFound = async ( const highestParent = getHighestParent(thread) if (highestParent) { try { - const parentsRes = await ctx.appViewAgent.api.app.bsky.feed.getPostThread( + const parentsRes = await ctx.appviewAgent.api.app.bsky.feed.getPostThread( { uri: highestParent, parentHeight: params.parentHeight, depth: 0 }, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/feed/getPosts.ts b/packages/pds/src/api/app/bsky/feed/getPosts.ts new file mode 100644 index 00000000000..05173c48ef9 --- /dev/null +++ b/packages/pds/src/api/app/bsky/feed/getPosts.ts @@ -0,0 +1,19 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.feed.getPosts({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.feed.getPosts( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts b/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts new file mode 100644 index 00000000000..44a5b15191d --- /dev/null +++ b/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts @@ -0,0 +1,19 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.feed.getRepostedBy({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.feed.getRepostedBy( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts b/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts new file mode 100644 index 00000000000..9c8d338104b --- /dev/null +++ b/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts @@ -0,0 +1,19 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.feed.getSuggestedFeeds({ + auth: ctx.accessVerifier, + handler: async ({ auth, params }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.feed.getSuggestedFeeds( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/munged/getTimeline.ts b/packages/pds/src/api/app/bsky/feed/getTimeline.ts similarity index 87% rename from packages/pds/src/api/app/bsky/munged/getTimeline.ts rename to packages/pds/src/api/app/bsky/feed/getTimeline.ts index ac26fca47d4..7d4e52ce918 100644 --- a/packages/pds/src/api/app/bsky/munged/getTimeline.ts +++ b/packages/pds/src/api/app/bsky/feed/getTimeline.ts @@ -1,7 +1,7 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { OutputSchema } from '../../../../lexicon/types/app/bsky/feed/getTimeline' -import { handleReadAfterWrite } from './util' +import { handleReadAfterWrite } from '../util/read-after-write' import { LocalRecords } from '../../../../services/local' export default function (server: Server, ctx: AppContext) { @@ -9,8 +9,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - - const res = await ctx.appViewAgent.api.app.bsky.feed.getTimeline( + const res = await ctx.appviewAgent.api.app.bsky.feed.getTimeline( params, await ctx.serviceAuthHeaders(requester), ) 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/api/app/bsky/graph/getBlocks.ts b/packages/pds/src/api/app/bsky/graph/getBlocks.ts new file mode 100644 index 00000000000..284dafd3034 --- /dev/null +++ b/packages/pds/src/api/app/bsky/graph/getBlocks.ts @@ -0,0 +1,19 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.graph.getBlocks({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.graph.getBlocks( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/graph/getFollowers.ts b/packages/pds/src/api/app/bsky/graph/getFollowers.ts new file mode 100644 index 00000000000..da0541d7e75 --- /dev/null +++ b/packages/pds/src/api/app/bsky/graph/getFollowers.ts @@ -0,0 +1,21 @@ +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({ + auth: ctx.accessOrRoleVerifier, + handler: async ({ req, params, auth }) => { + const requester = + auth.credentials.type === 'access' ? auth.credentials.did : null + 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, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/graph/getFollows.ts b/packages/pds/src/api/app/bsky/graph/getFollows.ts new file mode 100644 index 00000000000..f49c812f9ca --- /dev/null +++ b/packages/pds/src/api/app/bsky/graph/getFollows.ts @@ -0,0 +1,21 @@ +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({ + auth: ctx.accessOrRoleVerifier, + handler: async ({ req, params, auth }) => { + const requester = + auth.credentials.type === 'access' ? auth.credentials.did : null + 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, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/graph/getList.ts b/packages/pds/src/api/app/bsky/graph/getList.ts new file mode 100644 index 00000000000..5fd3c93df75 --- /dev/null +++ b/packages/pds/src/api/app/bsky/graph/getList.ts @@ -0,0 +1,19 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.graph.getList({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.graph.getList( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/graph/getListBlocks.ts b/packages/pds/src/api/app/bsky/graph/getListBlocks.ts new file mode 100644 index 00000000000..04fd55a324e --- /dev/null +++ b/packages/pds/src/api/app/bsky/graph/getListBlocks.ts @@ -0,0 +1,19 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.graph.getListBlocks({ + auth: ctx.accessVerifier, + handler: async ({ auth, params }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.graph.getListBlocks( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/graph/getListMutes.ts b/packages/pds/src/api/app/bsky/graph/getListMutes.ts new file mode 100644 index 00000000000..e0a624a3864 --- /dev/null +++ b/packages/pds/src/api/app/bsky/graph/getListMutes.ts @@ -0,0 +1,19 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.graph.getListMutes({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.graph.getListMutes( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/graph/getLists.ts b/packages/pds/src/api/app/bsky/graph/getLists.ts new file mode 100644 index 00000000000..e43a8d2b1d6 --- /dev/null +++ b/packages/pds/src/api/app/bsky/graph/getLists.ts @@ -0,0 +1,19 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.graph.getLists({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.graph.getLists( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/graph/getMutes.ts b/packages/pds/src/api/app/bsky/graph/getMutes.ts new file mode 100644 index 00000000000..9aa6b74445c --- /dev/null +++ b/packages/pds/src/api/app/bsky/graph/getMutes.ts @@ -0,0 +1,19 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.graph.getMutes({ + auth: ctx.accessVerifier, + handler: async ({ auth, params }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.graph.getMutes( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts b/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts new file mode 100644 index 00000000000..1db1c7f498f --- /dev/null +++ b/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts @@ -0,0 +1,20 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.graph.getSuggestedFollowsByActor({ + auth: ctx.accessVerifier, + handler: async ({ auth, params }) => { + const requester = auth.credentials.did + const res = + await ctx.appviewAgent.api.app.bsky.graph.getSuggestedFollowsByActor( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} 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/api/app/bsky/graph/muteActor.ts b/packages/pds/src/api/app/bsky/graph/muteActor.ts new file mode 100644 index 00000000000..44d4747f5e9 --- /dev/null +++ b/packages/pds/src/api/app/bsky/graph/muteActor.ts @@ -0,0 +1,34 @@ +import { InvalidRequestError } from '@atproto/xrpc-server' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.graph.muteActor({ + auth: ctx.accessVerifier, + handler: async ({ auth, input }) => { + const { actor } = input.body + const requester = auth.credentials.did + const { db, services } = ctx + + const subject = await services.account(db).getAccount(actor) + if (!subject) { + throw new InvalidRequestError(`Actor not found: ${actor}`) + } + if (subject.did === requester) { + throw new InvalidRequestError('Cannot mute oneself') + } + + if (ctx.canProxyWrite()) { + await ctx.appviewAgent.api.app.bsky.graph.muteActor(input.body, { + ...(await ctx.serviceAuthHeaders(requester)), + encoding: 'application/json', + }) + } + + await services.account(db).mute({ + did: subject.did, + mutedByDid: requester, + }) + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/graph/muteActorList.ts b/packages/pds/src/api/app/bsky/graph/muteActorList.ts new file mode 100644 index 00000000000..e554f7fce8b --- /dev/null +++ b/packages/pds/src/api/app/bsky/graph/muteActorList.ts @@ -0,0 +1,33 @@ +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' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.graph.muteActorList({ + auth: ctx.accessVerifier, + handler: async ({ auth, input }) => { + const { list } = input.body + const requester = auth.credentials.did + + const listUri = new AtUri(list) + const collId = lex.ids.AppBskyGraphList + if (listUri.collection !== collId) { + throw new InvalidRequestError(`Invalid collection: expected: ${collId}`) + } + + if (ctx.canProxyWrite()) { + await ctx.appviewAgent.api.app.bsky.graph.muteActorList(input.body, { + ...(await ctx.serviceAuthHeaders(requester)), + encoding: 'application/json', + }) + } + + await ctx.services.account(ctx.db).muteActorList({ + list, + mutedByDid: requester, + }) + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/graph/unmuteActor.ts b/packages/pds/src/api/app/bsky/graph/unmuteActor.ts new file mode 100644 index 00000000000..84819cc1e15 --- /dev/null +++ b/packages/pds/src/api/app/bsky/graph/unmuteActor.ts @@ -0,0 +1,31 @@ +import { Server } from '../../../../lexicon' +import { InvalidRequestError } from '@atproto/xrpc-server' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.graph.unmuteActor({ + auth: ctx.accessVerifier, + handler: async ({ auth, input }) => { + const { actor } = input.body + const requester = auth.credentials.did + const { db, services } = ctx + + const subject = await services.account(db).getAccount(actor) + if (!subject) { + throw new InvalidRequestError(`Actor not found: ${actor}`) + } + + if (ctx.canProxyWrite()) { + await ctx.appviewAgent.api.app.bsky.graph.unmuteActor(input.body, { + ...(await ctx.serviceAuthHeaders(requester)), + encoding: 'application/json', + }) + } + + await services.account(db).unmute({ + did: subject.did, + mutedByDid: requester, + }) + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/graph/unmuteActorList.ts b/packages/pds/src/api/app/bsky/graph/unmuteActorList.ts new file mode 100644 index 00000000000..ce3c1a4b254 --- /dev/null +++ b/packages/pds/src/api/app/bsky/graph/unmuteActorList.ts @@ -0,0 +1,24 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.graph.unmuteActorList({ + auth: ctx.accessVerifier, + handler: async ({ auth, input }) => { + const { list } = input.body + const requester = auth.credentials.did + + if (ctx.canProxyWrite()) { + await ctx.appviewAgent.api.app.bsky.graph.unmuteActorList(input.body, { + ...(await ctx.serviceAuthHeaders(requester)), + encoding: 'application/json', + }) + } + + await ctx.services.account(ctx.db).unmuteActorList({ + list, + mutedByDid: requester, + }) + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/index.ts b/packages/pds/src/api/app/bsky/index.ts index abef2b50a95..8f38a52683f 100644 --- a/packages/pds/src/api/app/bsky/index.ts +++ b/packages/pds/src/api/app/bsky/index.ts @@ -1,13 +1,15 @@ import { Server } from '../../../lexicon' import AppContext from '../../../context' -import simple from './simple' -import dynamic from './dynamic' -import preferences from './preferences' -import munged from './munged' +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) { - simple(server, ctx) - dynamic(server, ctx) - munged(server, ctx) - preferences(server, ctx) + actor(server, ctx) + feed(server, ctx) + graph(server, ctx) + notification(server, ctx) + unspecced(server, ctx) } diff --git a/packages/pds/src/api/app/bsky/munged/index.ts b/packages/pds/src/api/app/bsky/munged/index.ts deleted file mode 100644 index 4a41ceaf293..00000000000 --- a/packages/pds/src/api/app/bsky/munged/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' -import getActorLikes from './getActorLikes' -import getAuthorFeed from './getAuthorFeed' -import getPostThread from './getPostThread' -import getProfile from './getProfile' -import getProfiles from './getProfiles' -import getTimeline from './getTimeline' - -export default function (server: Server, ctx: AppContext) { - getActorLikes(server, ctx) - getAuthorFeed(server, ctx) - getPostThread(server, ctx) - getProfile(server, ctx) - getProfiles(server, ctx) - getTimeline(server, ctx) -} diff --git a/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts b/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts new file mode 100644 index 00000000000..e8100b183c5 --- /dev/null +++ b/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts @@ -0,0 +1,20 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.notification.getUnreadCount({ + auth: ctx.accessVerifier, + handler: async ({ auth, params }) => { + const requester = auth.credentials.did + const res = + await ctx.appviewAgent.api.app.bsky.notification.getUnreadCount( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} 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/api/app/bsky/notification/listNotifications.ts b/packages/pds/src/api/app/bsky/notification/listNotifications.ts new file mode 100644 index 00000000000..2f667172a57 --- /dev/null +++ b/packages/pds/src/api/app/bsky/notification/listNotifications.ts @@ -0,0 +1,20 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.notification.listNotifications({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = + await ctx.appviewAgent.api.app.bsky.notification.listNotifications( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/dynamic/registerPush.ts b/packages/pds/src/api/app/bsky/notification/registerPush.ts similarity index 100% rename from packages/pds/src/api/app/bsky/dynamic/registerPush.ts rename to packages/pds/src/api/app/bsky/notification/registerPush.ts diff --git a/packages/pds/src/api/app/bsky/notification/updateSeen.ts b/packages/pds/src/api/app/bsky/notification/updateSeen.ts new file mode 100644 index 00000000000..2c115a61f67 --- /dev/null +++ b/packages/pds/src/api/app/bsky/notification/updateSeen.ts @@ -0,0 +1,41 @@ +import { InvalidRequestError } from '@atproto/xrpc-server' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.notification.updateSeen({ + auth: ctx.accessVerifier, + handler: async ({ input, auth }) => { + const { seenAt } = input.body + const requester = auth.credentials.did + + let parsed: string + try { + parsed = new Date(seenAt).toISOString() + } catch (_err) { + throw new InvalidRequestError('Invalid date') + } + + const user = await ctx.services.account(ctx.db).getAccount(requester) + if (!user) { + throw new InvalidRequestError(`Could not find user: ${requester}`) + } + + if (ctx.canProxyWrite()) { + await ctx.appviewAgent.api.app.bsky.notification.updateSeen( + input.body, + { + ...(await ctx.serviceAuthHeaders(requester)), + encoding: 'application/json', + }, + ) + } + + await ctx.db.db + .updateTable('user_state') + .set({ lastSeenNotifs: parsed }) + .where('did', '=', user.did) + .executeTakeFirst() + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/preferences/getPreferences.ts b/packages/pds/src/api/app/bsky/preferences/getPreferences.ts index 782ef34869a..1bca50f0bd1 100644 --- a/packages/pds/src/api/app/bsky/preferences/getPreferences.ts +++ b/packages/pds/src/api/app/bsky/preferences/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 }, diff --git a/packages/pds/src/api/app/bsky/munged/getProfile.ts b/packages/pds/src/api/app/bsky/preferences/getProfile.ts similarity index 90% rename from packages/pds/src/api/app/bsky/munged/getProfile.ts rename to packages/pds/src/api/app/bsky/preferences/getProfile.ts index 64fe1200050..52858515827 100644 --- a/packages/pds/src/api/app/bsky/munged/getProfile.ts +++ b/packages/pds/src/api/app/bsky/preferences/getProfile.ts @@ -2,7 +2,7 @@ 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' +import { handleReadAfterWrite } from '../util/read-after-write' import { LocalRecords } from '../../../../services/local' export default function (server: Server, ctx: AppContext) { @@ -11,7 +11,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ req, auth, params }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - const res = await ctx.appViewAgent.api.app.bsky.actor.getProfile( + const res = await ctx.appviewAgent.api.app.bsky.actor.getProfile( params, requester ? await ctx.serviceAuthHeaders(requester) : authPassthru(req), ) diff --git a/packages/pds/src/api/app/bsky/munged/getProfiles.ts b/packages/pds/src/api/app/bsky/preferences/getProfiles.ts similarity index 90% rename from packages/pds/src/api/app/bsky/munged/getProfiles.ts rename to packages/pds/src/api/app/bsky/preferences/getProfiles.ts index dc54e16fa5d..46af4b08a0c 100644 --- a/packages/pds/src/api/app/bsky/munged/getProfiles.ts +++ b/packages/pds/src/api/app/bsky/preferences/getProfiles.ts @@ -2,14 +2,14 @@ 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' +import { handleReadAfterWrite } from '../util/read-after-write' export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.getProfiles({ auth: ctx.accessVerifier, handler: async ({ auth, params }) => { const requester = auth.credentials.did - const res = await ctx.appViewAgent.api.app.bsky.actor.getProfiles( + const res = await ctx.appviewAgent.api.app.bsky.actor.getProfiles( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/preferences/getSuggestions.ts b/packages/pds/src/api/app/bsky/preferences/getSuggestions.ts new file mode 100644 index 00000000000..c3ceb16cf14 --- /dev/null +++ b/packages/pds/src/api/app/bsky/preferences/getSuggestions.ts @@ -0,0 +1,19 @@ +import AppContext from '../../../../context' +import { Server } from '../../../../lexicon' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.actor.getSuggestions({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.actor.getSuggestions( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/preferences/index.ts b/packages/pds/src/api/app/bsky/preferences/index.ts index a2561170571..db69616f7e8 100644 --- a/packages/pds/src/api/app/bsky/preferences/index.ts +++ b/packages/pds/src/api/app/bsky/preferences/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/api/app/bsky/preferences/searchActors.ts b/packages/pds/src/api/app/bsky/preferences/searchActors.ts new file mode 100644 index 00000000000..921f4363bfc --- /dev/null +++ b/packages/pds/src/api/app/bsky/preferences/searchActors.ts @@ -0,0 +1,19 @@ +import AppContext from '../../../../context' +import { Server } from '../../../../lexicon' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.actor.searchActors({ + auth: ctx.accessVerifier, + handler: async ({ auth, params }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.actor.searchActors( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/api/app/bsky/preferences/searchActorsTypeahead.ts b/packages/pds/src/api/app/bsky/preferences/searchActorsTypeahead.ts new file mode 100644 index 00000000000..d8ef8f72dda --- /dev/null +++ b/packages/pds/src/api/app/bsky/preferences/searchActorsTypeahead.ts @@ -0,0 +1,20 @@ +import AppContext from '../../../../context' +import { Server } from '../../../../lexicon' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.actor.searchActorsTypeahead({ + auth: ctx.accessVerifier, + handler: async ({ params, auth }) => { + const requester = auth.credentials.did + const res = + await ctx.appviewAgent.api.app.bsky.actor.searchActorsTypeahead( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} 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/api/app/bsky/munged/util.ts b/packages/pds/src/api/app/bsky/util/read-after-write.ts similarity index 100% rename from packages/pds/src/api/app/bsky/munged/util.ts rename to packages/pds/src/api/app/bsky/util/read-after-write.ts diff --git a/packages/pds/src/api/app/bsky/dynamic/util.ts b/packages/pds/src/api/app/bsky/util/resolver.ts similarity index 100% rename from packages/pds/src/api/app/bsky/dynamic/util.ts rename to packages/pds/src/api/app/bsky/util/resolver.ts diff --git a/packages/pds/src/api/com/atproto/identity/resolveHandle.ts b/packages/pds/src/api/com/atproto/identity/resolveHandle.ts index a23f7131eb0..76b93e16b08 100644 --- a/packages/pds/src/api/com/atproto/identity/resolveHandle.ts +++ b/packages/pds/src/api/com/atproto/identity/resolveHandle.ts @@ -33,9 +33,14 @@ export default function (server: Server, ctx: AppContext) { } // this is not someone on our server, but we help with resolving anyway +<<<<<<< HEAD if (!did) { did = await tryResolveFromAppview(ctx.appViewAgent, handle) +======= + if (!did) { + did = await tryResolveFromAppview(ctx.appviewAgent, handle) +>>>>>>> main } if (!did) { 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/getRecord.ts b/packages/pds/src/api/com/atproto/repo/getRecord.ts index cd5a06a48b2..b07f2ad2a95 100644 --- a/packages/pds/src/api/com/atproto/repo/getRecord.ts +++ b/packages/pds/src/api/com/atproto/repo/getRecord.ts @@ -1,6 +1,7 @@ import { AtUri } from '@atproto/syntax' 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 }) => { @@ -13,19 +14,26 @@ 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}`) } +<<<<<<< HEAD } const res = await ctx.appViewAgent.api.com.atproto.repo.getRecord(params) +======= + return { + encoding: 'application/json', + body: { + uri: record.uri, + cid: record.cid, + value: record.value, + }, + } + } + + const res = await ctx.appviewAgent.api.com.atproto.repo.getRecord(params) +>>>>>>> main return { encoding: 'application/json', body: res.data, 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/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 }) => { diff --git a/packages/pds/src/api/com/atproto/server/getAccountInviteCodes.ts b/packages/pds/src/api/com/atproto/server/getAccountInviteCodes.ts index 46f8632c976..275398f7609 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 diff --git a/packages/pds/src/config.ts b/packages/pds/src/config.ts new file mode 100644 index 00000000000..f8d7b04d8fa --- /dev/null +++ b/packages/pds/src/config.ts @@ -0,0 +1,498 @@ +import { parseIntWithFallback, DAY, HOUR } from '@atproto/common' + +export interface ServerConfigValues { + debugMode?: boolean + version: string + + publicUrl?: string + scheme: string + port?: number + hostname: string + + dbPostgresUrl?: string + dbPostgresSchema?: string + + blobstoreLocation?: string + blobstoreTmp?: string + + jwtSecret: string + + didPlcUrl: string + didCacheStaleTTL: number + didCacheMaxTTL: number + + serverDid: string + recoveryKey: string + adminPassword: string + moderatorPassword?: string + triagePassword?: string + + inviteRequired: boolean + userInviteInterval: number | null + userInviteEpoch: number + privacyPolicyUrl?: string + termsOfServiceUrl?: string + + databaseLocation?: string + + availableUserDomains: string[] + handleResolveNameservers?: string[] + + rateLimitsEnabled: boolean + rateLimitBypassKey?: string + rateLimitBypassIps?: string[] + redisScratchAddress?: string + redisScratchPassword?: string + + appUrlPasswordReset: string + emailSmtpUrl?: string + emailNoReplyAddress: string + moderationEmailAddress?: string + moderationEmailSmtpUrl?: string + + maxSubscriptionBuffer: number + repoBackfillLimitMs: number + sequencerLeaderLockId?: number + sequencerLeaderEnabled?: boolean + + // this is really only used in test environments + dbTxLockNonce?: string + + bskyAppViewEndpoint: string + bskyAppViewDid: string + bskyAppViewModeration?: boolean + bskyAppViewCdnUrlPattern?: string + + crawlersToNotify?: string[] +} + +export class ServerConfig { + constructor(private cfg: ServerConfigValues) { + const invalidDomain = cfg.availableUserDomains.find( + (domain) => domain.length < 1 || !domain.startsWith('.'), + ) + if (invalidDomain) { + throw new Error(`Invalid domain: ${invalidDomain}`) + } + } + + static readEnv(overrides?: Partial) { + const debugMode = process.env.DEBUG_MODE === '1' + const version = process.env.PDS_VERSION || '0.0.0' + + const publicUrl = process.env.PUBLIC_URL || undefined + const hostname = process.env.HOSTNAME || 'localhost' + let scheme + if ('TLS' in process.env) { + scheme = process.env.TLS === '1' ? 'https' : 'http' + } else { + scheme = hostname === 'localhost' ? 'http' : 'https' + } + const port = parseIntWithFallback(process.env.PORT, 2583) + + const jwtSecret = process.env.JWT_SECRET || 'jwt_secret' + + const didPlcUrl = process.env.DID_PLC_URL || 'http://localhost:2582' + const didCacheStaleTTL = parseIntWithFallback( + process.env.DID_CACHE_STALE_TTL, + HOUR, + ) + const didCacheMaxTTL = parseIntWithFallback( + process.env.DID_CACHE_MAX_TTL, + DAY, + ) + + const serverDid = overrides?.serverDid || process.env.SERVER_DID + if (typeof serverDid !== 'string') { + throw new Error('No value provided for process.env.SERVER_DID') + } + + const recoveryKey = overrides?.recoveryKey || process.env.RECOVERY_KEY + if (typeof recoveryKey !== 'string') { + throw new Error('No value provided for process.env.RECOVERY_KEY') + } + + const adminPassword = process.env.ADMIN_PASSWORD || 'admin' + const moderatorPassword = process.env.MODERATOR_PASSWORD || undefined + const triagePassword = process.env.TRIAGE_PASSWORD || undefined + + const inviteRequired = process.env.INVITE_REQUIRED === 'true' ? true : false + const userInviteInterval = parseIntWithFallback( + process.env.USER_INVITE_INTERVAL, + null, + ) + const userInviteEpoch = parseIntWithFallback( + process.env.USER_INVITE_EPOCH, + 0, + ) + + const privacyPolicyUrl = process.env.PRIVACY_POLICY_URL + const termsOfServiceUrl = process.env.TERMS_OF_SERVICE_URL + + const databaseLocation = process.env.DATABASE_LOC + + const blobstoreLocation = process.env.BLOBSTORE_LOC + const blobstoreTmp = process.env.BLOBSTORE_TMP + + const availableUserDomains = process.env.AVAILABLE_USER_DOMAINS + ? process.env.AVAILABLE_USER_DOMAINS.split(',') + : [] + + const handleResolveNameservers = process.env.HANDLE_RESOLVE_NAMESERVERS + ? process.env.HANDLE_RESOLVE_NAMESERVERS.split(',') + : [] + + 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, + ) + const redisScratchPassword = nonemptyString( + process.env.REDIS_SCRATCH_PASSWORD, + ) + + const appUrlPasswordReset = + process.env.APP_URL_PASSWORD_RESET || 'app://password-reset' + + const emailSmtpUrl = process.env.EMAIL_SMTP_URL || undefined + + const emailNoReplyAddress = + process.env.EMAIL_NO_REPLY_ADDRESS || 'noreply@blueskyweb.xyz' + + const moderationEmailAddress = + process.env.MODERATION_EMAIL_ADDRESS || undefined + const moderationEmailSmtpUrl = + process.env.MODERATION_EMAIL_SMTP_URL || undefined + + const dbPostgresUrl = process.env.DB_POSTGRES_URL + const dbPostgresSchema = process.env.DB_POSTGRES_SCHEMA + + const maxSubscriptionBuffer = parseIntWithFallback( + process.env.MAX_SUBSCRIPTION_BUFFER, + 500, + ) + + const repoBackfillLimitMs = parseIntWithFallback( + process.env.REPO_BACKFILL_LIMIT_MS, + DAY, + ) + + const sequencerLeaderLockId = parseIntWithFallback( + process.env.SEQUENCER_LEADER_LOCK_ID, + undefined, + ) + + // by default each instance is a potential sequencer leader, but may be configured off + const sequencerLeaderEnabled = process.env.SEQUENCER_LEADER_ENABLED + ? process.env.SEQUENCER_LEADER_ENABLED !== '0' && + process.env.SEQUENCER_LEADER_ENABLED !== 'false' + : undefined + + const dbTxLockNonce = nonemptyString(process.env.DB_TX_LOCK_NONCE) + + 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 bskyAppViewCdnUrlPattern = nonemptyString( + process.env.BSKY_APP_VIEW_CDN_URL_PATTERN, + ) + + const crawlersEnv = process.env.CRAWLERS_TO_NOTIFY + const crawlersToNotify = + crawlersEnv && crawlersEnv.length > 0 ? crawlersEnv.split(',') : [] + + return new ServerConfig({ + debugMode, + version, + publicUrl, + scheme, + hostname, + port, + dbPostgresUrl, + dbPostgresSchema, + blobstoreLocation, + blobstoreTmp, + jwtSecret, + recoveryKey, + didPlcUrl, + didCacheStaleTTL, + didCacheMaxTTL, + serverDid, + adminPassword, + moderatorPassword, + triagePassword, + inviteRequired, + userInviteInterval, + userInviteEpoch, + privacyPolicyUrl, + termsOfServiceUrl, + databaseLocation, + availableUserDomains, + handleResolveNameservers, + rateLimitsEnabled, + rateLimitBypassKey, + rateLimitBypassIps, + redisScratchAddress, + redisScratchPassword, + appUrlPasswordReset, + emailSmtpUrl, + emailNoReplyAddress, + moderationEmailAddress, + moderationEmailSmtpUrl, + maxSubscriptionBuffer, + repoBackfillLimitMs, + sequencerLeaderLockId, + sequencerLeaderEnabled, + dbTxLockNonce, + bskyAppViewEndpoint, + bskyAppViewDid, + bskyAppViewModeration, + bskyAppViewCdnUrlPattern, + crawlersToNotify, + ...overrides, + }) + } + + get debugMode() { + return !!this.cfg.debugMode + } + + get version() { + return this.cfg.version + } + + get scheme() { + return this.cfg.scheme + } + + get port() { + return this.cfg.port + } + + get hostname() { + return this.cfg.hostname + } + + get internalUrl() { + return `${this.scheme}://${this.hostname}:${this.port}` + } + + get origin() { + const u = new URL(this.internalUrl) + return u.origin + } + + get publicUrl() { + return this.cfg.publicUrl || this.internalUrl + } + + get publicHostname() { + const u = new URL(this.publicUrl) + return u.hostname + } + + get dbPostgresUrl() { + return this.cfg.dbPostgresUrl + } + + get dbPostgresSchema() { + return this.cfg.dbPostgresSchema + } + + get blobstoreLocation() { + return this.cfg.blobstoreLocation + } + + get blobstoreTmp() { + return this.cfg.blobstoreTmp + } + + get jwtSecret() { + return this.cfg.jwtSecret + } + + get didPlcUrl() { + return this.cfg.didPlcUrl + } + + get didCacheStaleTTL() { + return this.cfg.didCacheStaleTTL + } + + get didCacheMaxTTL() { + return this.cfg.didCacheMaxTTL + } + + get serverDid() { + return this.cfg.serverDid + } + + get recoveryKey() { + return this.cfg.recoveryKey + } + + get adminPassword() { + return this.cfg.adminPassword + } + + get moderatorPassword() { + return this.cfg.moderatorPassword + } + + get triagePassword() { + return this.cfg.triagePassword + } + + get inviteRequired() { + return this.cfg.inviteRequired + } + + get userInviteInterval() { + return this.cfg.userInviteInterval + } + + get userInviteEpoch() { + return this.cfg.userInviteEpoch + } + + get privacyPolicyUrl() { + if ( + this.cfg.privacyPolicyUrl && + this.cfg.privacyPolicyUrl.startsWith('/') + ) { + return this.publicUrl + this.cfg.privacyPolicyUrl + } + return this.cfg.privacyPolicyUrl + } + + get termsOfServiceUrl() { + if ( + this.cfg.termsOfServiceUrl && + this.cfg.termsOfServiceUrl.startsWith('/') + ) { + return this.publicUrl + this.cfg.termsOfServiceUrl + } + return this.cfg.termsOfServiceUrl + } + + get databaseLocation() { + return this.cfg.databaseLocation + } + + get useMemoryDatabase() { + return !this.databaseLocation + } + + get availableUserDomains() { + return this.cfg.availableUserDomains + } + + get handleResolveNameservers() { + return this.cfg.handleResolveNameservers + } + + get rateLimitsEnabled() { + return this.cfg.rateLimitsEnabled + } + + get rateLimitBypassKey() { + return this.cfg.rateLimitBypassKey + } + + get rateLimitBypassIps() { + return this.cfg.rateLimitBypassIps + } + + get redisScratchAddress() { + return this.cfg.redisScratchAddress + } + + get redisScratchPassword() { + return this.cfg.redisScratchPassword + } + + get appUrlPasswordReset() { + return this.cfg.appUrlPasswordReset + } + + get emailSmtpUrl() { + return this.cfg.emailSmtpUrl + } + + get emailNoReplyAddress() { + return this.cfg.emailNoReplyAddress + } + + get moderationEmailAddress() { + return this.cfg.moderationEmailAddress + } + + get moderationEmailSmtpUrl() { + return this.cfg.moderationEmailSmtpUrl + } + + get maxSubscriptionBuffer() { + return this.cfg.maxSubscriptionBuffer + } + + get repoBackfillLimitMs() { + return this.cfg.repoBackfillLimitMs + } + + get sequencerLeaderLockId() { + return this.cfg.sequencerLeaderLockId + } + + get sequencerLeaderEnabled() { + return this.cfg.sequencerLeaderEnabled !== false + } + + get dbTxLockNonce() { + return this.cfg.dbTxLockNonce + } + + get bskyAppViewEndpoint() { + return this.cfg.bskyAppViewEndpoint + } + + get bskyAppViewDid() { + return this.cfg.bskyAppViewDid + } + + get bskyAppViewModeration() { + return this.cfg.bskyAppViewModeration + } + + get bskyAppViewCdnUrlPattern() { + return this.cfg.bskyAppViewCdnUrlPattern + } + + get crawlersToNotify() { + return this.cfg.crawlersToNotify + } +} + +const nonemptyString = (str: string | undefined): string | undefined => { + if (str === undefined || str.length === 0) return undefined + return str +} diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index 466bc9a5613..c5930851a94 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -1,4 +1,7 @@ +<<<<<<< HEAD import * as nodemailer from 'nodemailer' +======= +>>>>>>> main import { Redis } from 'ioredis' import * as plc from '@did-plc/lib' import * as crypto from '@atproto/crypto' @@ -13,13 +16,20 @@ import { ServerAuth } from './auth' import { ServerMailer } from './mailer' import { ModerationMailer } from './mailer/moderation' import { BlobStore } from '@atproto/repo' +<<<<<<< HEAD import { Services, createServices } from './services' +======= +import { Services } from './services' +>>>>>>> main import { Sequencer, SequencerLeader } from './sequencer' import { BackgroundQueue } from './background' import DidSqlCache from './did-cache' import { Crawlers } from './crawlers' +<<<<<<< HEAD import { DiskBlobStore } from './storage' import { getRedisClient } from './redis' +======= +>>>>>>> main import { RuntimeFlags } from './runtime-flags' export type AppContextOptions = { @@ -45,6 +55,7 @@ export type AppContextOptions = { } export class AppContext { +<<<<<<< HEAD public db: Database public blobstore: BlobStore public mailer: ServerMailer @@ -135,6 +146,34 @@ export class AppContext { timeout: cfg.identity.resolverTimeout, }) const plcClient = new plc.Client(cfg.identity.plcUrl) +======= + constructor( + private opts: { + db: Database + blobstore: BlobStore + redisScratch?: Redis + repoSigningKey: crypto.Keypair + plcRotationKey: crypto.Keypair + idResolver: IdResolver + didCache: DidSqlCache + auth: auth.ServerAuth + cfg: ServerConfig + mailer: ServerMailer + moderationMailer: ModerationMailer + services: Services + sequencer: Sequencer + sequencerLeader: SequencerLeader | null + runtimeFlags: RuntimeFlags + backgroundQueue: BackgroundQueue + appviewAgent: AtpAgent + crawlers: Crawlers + }, + ) {} + + get db(): Database { + return this.opts.db + } +>>>>>>> main const sequencer = new Sequencer(db) const sequencerLeader = cfg.subscription.sequencerLeaderEnabled @@ -239,6 +278,61 @@ export class AppContext { return auth.optionalAccessOrRoleVerifier(this.auth) } +<<<<<<< HEAD +======= + get cfg(): ServerConfig { + return this.opts.cfg + } + + get mailer(): ServerMailer { + return this.opts.mailer + } + + get moderationMailer(): ModerationMailer { + return this.opts.moderationMailer + } + + get services(): Services { + return this.opts.services + } + + get sequencer(): Sequencer { + return this.opts.sequencer + } + + get sequencerLeader(): SequencerLeader | null { + return this.opts.sequencerLeader + } + + get runtimeFlags(): RuntimeFlags { + return this.opts.runtimeFlags + } + + get backgroundQueue(): BackgroundQueue { + return this.opts.backgroundQueue + } + + get crawlers(): Crawlers { + return this.opts.crawlers + } + + get plcClient(): plc.Client { + return new plc.Client(this.cfg.didPlcUrl) + } + + get idResolver(): IdResolver { + return this.opts.idResolver + } + + get didCache(): DidSqlCache { + return this.opts.didCache + } + + get appviewAgent(): AtpAgent { + return this.opts.appviewAgent + } + +>>>>>>> main async serviceAuthHeaders(did: string, audience?: string) { const aud = audience ?? this.cfg.bskyAppView.did if (!aud) { @@ -250,6 +344,23 @@ export class AppContext { keypair: this.repoSigningKey, }) } +<<<<<<< HEAD +======= + + shouldProxyModeration(): boolean { + return ( + this.cfg.bskyAppViewEndpoint !== undefined && + this.cfg.bskyAppViewModeration === true + ) + } + + canProxyWrite(): boolean { + return ( + this.cfg.bskyAppViewEndpoint !== undefined && + this.cfg.bskyAppViewDid !== undefined + ) + } +>>>>>>> main } export default AppContext diff --git a/packages/pds/src/db/database-schema.ts b/packages/pds/src/db/database-schema.ts index 655408dad8c..26f645cd746 100644 --- a/packages/pds/src/db/database-schema.ts +++ b/packages/pds/src/db/database-schema.ts @@ -14,12 +14,22 @@ import * as blob from './tables/blob' import * as repoBlob from './tables/repo-blob' import * as deleteAccountToken from './tables/delete-account-token' import * as moderation from './tables/moderation' +<<<<<<< HEAD +======= +import * as mute from './tables/mute' +import * as listMute from './tables/list-mute' +>>>>>>> main import * as repoSeq from './tables/repo-seq' import * as appMigration from './tables/app-migration' import * as runtimeFlag from './tables/runtime-flag' +<<<<<<< HEAD export type DatabaseSchemaType = appMigration.PartialDB & runtimeFlag.PartialDB & +======= +export type DatabaseSchemaType = runtimeFlag.PartialDB & + appMigration.PartialDB & +>>>>>>> main userAccount.PartialDB & userPref.PartialDB & didHandle.PartialDB & @@ -35,6 +45,11 @@ export type DatabaseSchemaType = appMigration.PartialDB & repoBlob.PartialDB & deleteAccountToken.PartialDB & moderation.PartialDB & +<<<<<<< HEAD +======= + mute.PartialDB & + listMute.PartialDB & +>>>>>>> main 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 8ebd59d6ef3..63f9d67e4ab 100644 --- a/packages/pds/src/db/migrations/index.ts +++ b/packages/pds/src/db/migrations/index.ts @@ -2,5 +2,72 @@ // It's important that every migration is exported from here with the proper name. We'd simplify // this with kysely's FileMigrationProvider, but it doesn't play nicely with the build process. +<<<<<<< HEAD export * as _20230613T164932261Z from './20230613T164932261Z-init' export * as _20230914T014727199Z from './20230914T014727199Z-repo-v3' +======= +export * as _20221021T162202001Z from './20221021T162202001Z-init' +export * as _20221116T234458063Z from './20221116T234458063Z-duplicate-records' +export * as _20221202T212459280Z from './20221202T212459280Z-blobs' +export * as _20221209T210026294Z from './20221209T210026294Z-banners' +export * as _20221212T195416407Z from './20221212T195416407Z-post-media' +export * as _20221215T220356370Z from './20221215T220356370Z-password-reset-otp' +export * as _20221226T213635517Z from './20221226T213635517Z-mute-init' +export * as _20221230T215012029Z from './20221230T215012029Z-moderation-init' +export * as _20230127T215753149Z from './20230127T215753149Z-indexed-at-on-record' +export * as _20230127T224743452Z from './20230127T224743452Z-repo-sync-data-pt1' +export * as _20230201T200606704Z from './20230201T200606704Z-repo-sync-data-pt2' +export * as _20230202T170426672Z from './20230202T170426672Z-user-partitioned-cids' +export * as _20230202T170435937Z from './20230202T170435937Z-delete-account-token' +export * as _20230202T172831900Z from './20230202T172831900Z-moderation-subject-blob' +export * as _20230202T213952826Z from './20230202T213952826Z-repo-seq' +export * as _20230208T081544325Z from './20230208T081544325Z-post-hydrate-indices' +export * as _20230208T222001557Z from './20230208T222001557Z-user-table-did-pkey' +export * as _20230210T210132396Z from './20230210T210132396Z-post-hierarchy' +export * as _20230214T172233550Z from './20230214T172233550Z-embed-records' +export * as _20230301T222603402Z from './20230301T222603402Z-repo-ops' +export * as _20230304T193548198Z from './20230304T193548198Z-pagination-indices' +export * as _20230308T234640077Z from './20230308T234640077Z-record-indexes' +export * as _20230309T012947663Z from './20230309T012947663Z-app-migration' +export * as _20230310T205728933Z from './20230310T205728933Z-subscription-init' +export * as _20230313T232322844Z from './20230313T232322844Z-blob-creator' +export * as _20230314T023842127Z from './20230314T023842127Z-refresh-grace-period' +export * as _20230323T162732466Z from './20230323T162732466Z-remove-scenes' +export * as _20230328T214311000Z from './20230328T214311000Z-remove-declarations-assertions-confirmations' +export * as _20230328T214311001Z from './20230328T214311001Z-votes-to-likes' +export * as _20230328T214311002Z from './20230328T214311002Z-remove-post-entities' +export * as _20230328T214311003Z from './20230328T214311003Z-backlinks' +export * as _20230328T214311004Z from './20230328T214311004Z-profile-display-name-empty' +export * as _20230328T214311005Z from './20230328T214311005Z-rework-seq' +export * as _20230406T185855842Z from './20230406T185855842Z-feed-item-init' +export * as _20230411T175730759Z from './20230411T175730759Z-drop-message-queue' +export * as _20230411T180247652Z from './20230411T180247652Z-labels' +export * as _20230412T231807162Z from './20230412T231807162Z-moderation-action-labels' +export * as _20230416T221236745Z from './20230416T221236745Z-app-specific-passwords' +export * as _20230420T143821201Z from './20230420T143821201Z-post-profile-aggs' +export * as _20230427T194652255Z from './20230427T194652255Z-notif-record-index' +export * as _20230428T195614638Z from './20230428T195614638Z-actor-block-init' +export * as _20230508T193807762Z from './20230508T193807762Z-acct-deletion-indexes' +export * as _20230508T232711152Z from './20230508T232711152Z-disable-account-invites' +export * as _20230509T192324175Z from './20230509T192324175Z-seq-invalidated' +export * as _20230511T154721392Z from './20230511T154721392Z-mute-lists' +export * as _20230511T171739449Z from './20230511T171739449Z-actor-preferences' +export * as _20230511T200212974Z from './20230511T200212974Z-feed-generators' +export * as _20230523T183902064Z from './20230523T183902064Z-algo-whats-hot-view' +export * as _20230529T222706121Z from './20230529T222706121Z-suggested-follows' +export * as _20230530T213530067Z from './20230530T213530067Z-rebase-indices' +export * as _20230605T235529700Z from './20230605T235529700Z-outgoing-repo-seq' +export * as _20230703T044601833Z from './20230703T044601833Z-feed-and-label-indices' +export * as _20230718T170914772Z from './20230718T170914772Z-sequencer-leader-sequence' +export * as _20230727T172043676Z from './20230727T172043676Z-user-account-cursor-idx' +export * as _20230801T141349990Z from './20230801T141349990Z-invite-note' +export * as _20230801T195109532Z from './20230801T195109532Z-remove-moderation-fkeys' +export * as _20230807T035309811Z from './20230807T035309811Z-feed-item-delete-invite-for-user-idx' +export * as _20230808T172813122Z from './20230808T172813122Z-repo-rev' +export * as _20230810T203412859Z from './20230810T203412859Z-action-duration' +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' +>>>>>>> main diff --git a/packages/pds/src/image/index.ts b/packages/pds/src/image/index.ts index 4f70321f0ed..5a825ac84bd 100644 --- a/packages/pds/src/image/index.ts +++ b/packages/pds/src/image/index.ts @@ -46,13 +46,33 @@ export async function getInfo(stream: Readable): Promise { return maybeInfo } +<<<<<<< HEAD export type Dimensions = { height: number; width: number } +======= +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 +} +>>>>>>> main export type ImageInfo = Dimensions & { size: number mime: `image/${string}` | 'unknown' } +<<<<<<< HEAD +======= +export type Dimensions = { height: number; width: number } + +>>>>>>> main export const formatsToMimes: { [s in keyof sharp.FormatEnum]?: `image/${string}` } = { diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index 101b4944a46..b80ef4f6df5 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -15,6 +15,10 @@ import { RateLimiterOpts, Options as XrpcServerOptions, } from '@atproto/xrpc-server' +<<<<<<< HEAD +======= +import { DAY, HOUR, MINUTE } from '@atproto/common' +>>>>>>> main import API from './api' import * as basicRoutes from './basic-routes' import * as wellKnown from './well-known' @@ -22,15 +26,33 @@ import * as error from './error' import { dbLogger, loggerMiddleware, seqLogger } from './logger' import { ServerConfig, ServerSecrets } from './config' import { createServer } from './lexicon' +<<<<<<< HEAD import { createHttpTerminator, HttpTerminator } from 'http-terminator' import AppContext, { AppContextOptions } from './context' import compression from './util/compression' export * from './config' +======= +import { createServices } from './services' +import { createHttpTerminator, HttpTerminator } from 'http-terminator' +import AppContext from './context' +import { Sequencer, SequencerLeader } from './sequencer' +import { BackgroundQueue } from './background' +import DidSqlCache from './did-cache' +import { Crawlers } from './crawlers' +import { getRedisClient } from './redis' +import { RuntimeFlags } from './runtime-flags' + +export type { ServerConfigValues } from './config' +export { ServerConfig } from './config' +>>>>>>> main export { Database } from './db' export { DiskBlobStore, MemoryBlobStore } from './storage' export { AppContext } from './context' +<<<<<<< HEAD export { httpLogger } from './logger' +======= +>>>>>>> main export class PDS { public ctx: AppContext @@ -45,18 +67,119 @@ export class PDS { this.app = opts.app } +<<<<<<< HEAD static async create( cfg: ServerConfig, secrets: ServerSecrets, overrides?: Partial, ): Promise { +======= + static create(opts: { + db: Database + blobstore: BlobStore + repoSigningKey: crypto.Keypair + plcRotationKey: crypto.Keypair + config: ServerConfig + }): PDS { + const { db, blobstore, repoSigningKey, plcRotationKey, config } = opts + const auth = new ServerAuth({ + jwtSecret: config.jwtSecret, + adminPass: config.adminPassword, + moderatorPass: config.moderatorPassword, + triagePass: config.triagePassword, + }) + + const didCache = new DidSqlCache( + db, + config.didCacheStaleTTL, + config.didCacheMaxTTL, + ) + const idResolver = new IdResolver({ + plcUrl: config.didPlcUrl, + didCache, + backupNameservers: config.handleResolveNameservers, + }) + + const sequencer = new Sequencer(db) + const sequencerLeader = config.sequencerLeaderEnabled + ? new SequencerLeader(db, config.sequencerLeaderLockId) + : null + + const serverMailTransport = + config.emailSmtpUrl !== undefined + ? createTransport(config.emailSmtpUrl) + : createTransport({ jsonTransport: true }) + + const moderationMailTransport = + config.moderationEmailSmtpUrl !== undefined + ? createTransport(config.moderationEmailSmtpUrl) + : createTransport({ jsonTransport: true }) + + const mailer = new ServerMailer(serverMailTransport, config) + const moderationMailer = new ModerationMailer( + moderationMailTransport, + config, + ) + +>>>>>>> main const app = express() app.set('trust proxy', true) app.use(cors()) app.use(loggerMiddleware) app.use(compression()) +<<<<<<< HEAD const ctx = await AppContext.fromConfig(cfg, secrets, overrides) +======= + const backgroundQueue = new BackgroundQueue(db) + const crawlers = new Crawlers( + config.hostname, + config.crawlersToNotify ?? [], + ) + + const appviewAgent = new AtpAgent({ service: config.bskyAppViewEndpoint }) + + const services = createServices({ + repoSigningKey, + blobstore, + appviewAgent, + appviewDid: config.bskyAppViewDid, + appviewCdnUrlPattern: config.bskyAppViewCdnUrlPattern, + backgroundQueue, + crawlers, + }) + + const runtimeFlags = new RuntimeFlags(db) + + let redisScratch: Redis | undefined = undefined + if (config.redisScratchAddress) { + redisScratch = getRedisClient( + config.redisScratchAddress, + config.redisScratchPassword, + ) + } + + const ctx = new AppContext({ + db, + blobstore, + redisScratch, + repoSigningKey, + plcRotationKey, + idResolver, + didCache, + cfg: config, + auth, + sequencer, + sequencerLeader, + runtimeFlags, + services, + mailer, + moderationMailer, + backgroundQueue, + appviewAgent, + crawlers, + }) +>>>>>>> main const xrpcOpts: XrpcServerOptions = { validateResponse: false, @@ -73,14 +196,25 @@ export class PDS { throw new Error('Redis not set up for ratelimiting mode: `redis`') } rlCreator = (opts: RateLimiterOpts) => +<<<<<<< HEAD RateLimiter.redis(ctx.redisScratch, { // bypassSecret: cfg.rateLimits., +======= + RateLimiter.redis(redisScratch, { + bypassSecret: config.rateLimitBypassKey, + bypassIps: config.rateLimitBypassIps, +>>>>>>> main ...opts, }) } else { rlCreator = (opts: RateLimiterOpts) => RateLimiter.memory({ +<<<<<<< HEAD // bypassSecret: config.rateLimitBypassKey, +======= + bypassSecret: config.rateLimitBypassKey, + bypassIps: config.rateLimitBypassIps, +>>>>>>> main ...opts, }) } @@ -93,6 +227,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 + }, + ], } } diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index c0840abea2c..c8c7d230e36 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -84,11 +84,13 @@ 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' 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' @@ -106,10 +108,11 @@ 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' +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', @@ -127,6 +130,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 { @@ -1123,6 +1127,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, @@ -1177,6 +1192,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 { @@ -1397,17 +1423,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, @@ -1440,6 +1455,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 c682f239a3b..5ea6bab95e3 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', @@ -3713,6 +3717,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', ], }, }, @@ -3769,6 +3775,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: { @@ -3975,18 +4028,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', @@ -4027,12 +4086,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, }, }, }, @@ -4416,6 +4480,10 @@ export const schemaDict = { ref: 'lex:com.atproto.label.defs#label', }, }, + threadgate: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#threadgateView', + }, }, }, viewerState: { @@ -4512,6 +4580,10 @@ export const schemaDict = { ], }, }, + viewer: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#viewerThreadState', + }, }, }, notFoundPost: { @@ -4560,6 +4632,14 @@ export const schemaDict = { }, }, }, + viewerThreadState: { + type: 'object', + properties: { + canReply: { + type: 'boolean', + }, + }, + }, generatorView: { type: 'object', required: ['uri', 'cid', 'did', 'creator', 'displayName', 'indexedAt'], @@ -4645,6 +4725,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: { @@ -5186,6 +5289,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', @@ -5507,6 +5663,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', @@ -5588,6 +5754,126 @@ 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', + 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.', + properties: {}, + }, + followingRule: { + type: 'object', + description: 'Allow replies from actors you follow.', + properties: {}, + }, + 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', @@ -5713,13 +5999,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: { @@ -6579,6 +6873,7 @@ export const schemaDict = { refs: [ 'lex:app.bsky.richtext.facet#mention', 'lex:app.bsky.richtext.facet#link', + 'lex:app.bsky.richtext.facet#tag', ], }, }, @@ -6606,6 +6901,18 @@ export const schemaDict = { }, }, }, + tag: { + type: 'object', + description: 'A hashtag.', + required: ['tag'], + properties: { + tag: { + type: 'string', + maxLength: 640, + maxGraphemes: 64, + }, + }, + }, byteSlice: { type: 'object', description: @@ -6624,27 +6931,27 @@ export const schemaDict = { }, }, }, - AppBskyUnspeccedApplyLabels: { + AppBskyUnspeccedDefs: { lexicon: 1, - id: 'app.bsky.unspecced.applyLabels', + id: 'app.bsky.unspecced.defs', 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', - }, - }, - }, + skeletonSearchPost: { + type: 'object', + required: ['uri'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + }, + }, + skeletonSearchActor: { + type: 'object', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', }, }, }, @@ -6656,7 +6963,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: { @@ -6791,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) @@ -6889,6 +7323,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', @@ -6897,6 +7332,8 @@ 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', AppBskyGraphFollow: 'app.bsky.graph.follow', @@ -6923,9 +7360,12 @@ 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: '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/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) +} 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/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/bsky/src/lexicon/types/app/bsky/unspecced/applyLabels.ts b/packages/pds/src/lexicon/types/app/bsky/feed/getListFeed.ts similarity index 61% rename from packages/bsky/src/lexicon/types/app/bsky/unspecced/applyLabels.ts rename to packages/pds/src/lexicon/types/app/bsky/feed/getListFeed.ts index 1d359a9547d..e24c3f8ed22 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/unspecced/applyLabels.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/getListFeed.ts @@ -7,26 +7,37 @@ 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' +import * as AppBskyFeedDefs from './defs' -export interface QueryParams {} +export interface QueryParams { + list: string + limit: number + cursor?: string +} + +export type InputSchema = undefined -export interface InputSchema { - labels: ComAtprotoLabelDefs.Label[] +export interface OutputSchema { + cursor?: string + feed: AppBskyFeedDefs.FeedViewPost[] [k: string]: unknown } -export interface HandlerInput { +export type HandlerInput = undefined + +export interface HandlerSuccess { encoding: 'application/json' - body: InputSchema + body: OutputSchema + headers?: { [key: string]: string } } export interface HandlerError { status: number message?: string + error?: 'UnknownList' } -export type HandlerOutput = HandlerError | void +export type HandlerOutput = HandlerError | HandlerSuccess export type HandlerReqCtx = { auth: HA params: QueryParams 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/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/feed/threadgate.ts b/packages/pds/src/lexicon/types/app/bsky/feed/threadgate.ts new file mode 100644 index 00000000000..6a190d6e98a --- /dev/null +++ b/packages/pds/src/lexicon/types/app/bsky/feed/threadgate.ts @@ -0,0 +1,84 @@ +/** + * 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 { + [k: string]: unknown +} + +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 { + [k: string]: unknown +} + +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/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/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/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 diff --git a/packages/pds/src/repo/prepare.ts b/packages/pds/src/repo/prepare.ts index 60fbe2d81cd..2147ef552b6 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, @@ -293,8 +299,16 @@ 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') } } + +// 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 +} 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/src/services/account/index.ts b/packages/pds/src/services/account/index.ts index 91c0b09f8c9..da6da9c5214 100644 --- a/packages/pds/src/services/account/index.ts +++ b/packages/pds/src/services/account/index.ts @@ -1,6 +1,9 @@ import { sql } from 'kysely' +<<<<<<< HEAD import { randomStr } from '@atproto/crypto' import { InvalidRequestError } from '@atproto/xrpc-server' +======= +>>>>>>> main import { dbLogger as log } from '../../logger' import Database from '../../db' import * as scrypt from '../../db/scrypt' @@ -8,9 +11,18 @@ import { UserAccountEntry } from '../../db/tables/user-account' import { DidHandle } from '../../db/tables/did-handle' import { RepoRoot } from '../../db/tables/repo-root' import { countAll, notSoftDeletedClause } from '../../db/util' +<<<<<<< HEAD import { paginate, TimeCidKeyset } from '../../db/pagination' import * as sequencer from '../../sequencer' import { AppPassword } from '../../lexicon/types/com/atproto/server/createAppPassword' +======= +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' +>>>>>>> main export class AccountService { constructor(public db: Database) {} @@ -273,6 +285,79 @@ export class AccountService { .execute() } +<<<<<<< HEAD +======= + async mute(info: { did: string; mutedByDid: string; createdAt?: Date }) { + const { did, mutedByDid, createdAt = new Date() } = info + await this.db.db + .insertInto('mute') + .values({ + did, + mutedByDid, + createdAt: createdAt.toISOString(), + }) + .onConflict((oc) => oc.doNothing()) + .execute() + } + + async unmute(info: { did: string; mutedByDid: string }) { + const { did, mutedByDid } = info + await this.db.db + .deleteFrom('mute') + .where('did', '=', did) + .where('mutedByDid', '=', mutedByDid) + .execute() + } + + async getMute(mutedBy: string, did: string): Promise { + const mutes = await this.getMutes(mutedBy, [did]) + return mutes[did] ?? false + } + + async getMutes( + mutedBy: string, + dids: string[], + ): Promise> { + if (dids.length === 0) return {} + const res = await this.db.db + .selectFrom('mute') + .where('mutedByDid', '=', mutedBy) + .where('did', 'in', dids) + .selectAll() + .execute() + return res.reduce((acc, cur) => { + acc[cur.did] = true + return acc + }, {} as Record) + } + + async muteActorList(info: { + list: string + mutedByDid: string + createdAt?: Date + }) { + const { list, mutedByDid, createdAt = new Date() } = info + await this.db.db + .insertInto('list_mute') + .values({ + listUri: list, + mutedByDid, + createdAt: createdAt.toISOString(), + }) + .onConflict((oc) => oc.doNothing()) + .execute() + } + + async unmuteActorList(info: { list: string; mutedByDid: string }) { + const { list, mutedByDid } = info + await this.db.db + .deleteFrom('list_mute') + .where('listUri', '=', list) + .where('mutedByDid', '=', mutedByDid) + .execute() + } + +>>>>>>> main async search(opts: { term: string limit: number @@ -282,6 +367,7 @@ export class AccountService { const { term, limit, cursor, includeSoftDeleted } = opts const { ref } = this.db.db.dynamic +<<<<<<< HEAD const builder = this.db.db .selectFrom('did_handle') .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') @@ -312,6 +398,19 @@ export class AccountService { cursor, keyset, }).execute() +======= + const builder = + this.db.dialect === 'pg' + ? getUserSearchQueryPg(this.db, opts) + .selectAll('did_handle') + .selectAll('repo_root') + : getUserSearchQuerySqlite(this.db, opts) + .selectAll('did_handle') + .selectAll('repo_root') + .select(sql`0`.as('distance')) + + return await builder.execute() +>>>>>>> main } async list(opts: { diff --git a/packages/pds/src/services/index.ts b/packages/pds/src/services/index.ts index 954a5544e6e..2f7b1fed63b 100644 --- a/packages/pds/src/services/index.ts +++ b/packages/pds/src/services/index.ts @@ -14,20 +14,32 @@ import { LocalService } from './local' export function createServices(resources: { repoSigningKey: crypto.Keypair blobstore: BlobStore +<<<<<<< HEAD pdsHostname: string appViewAgent?: AtpAgent appViewDid?: string appViewCdnUrlPattern?: string +======= + appviewAgent?: AtpAgent + appviewDid?: string + appviewCdnUrlPattern?: string +>>>>>>> main backgroundQueue: BackgroundQueue crawlers: Crawlers }): Services { const { repoSigningKey, blobstore, +<<<<<<< HEAD pdsHostname, appViewAgent, appViewDid, appViewCdnUrlPattern, +======= + appviewAgent, + appviewDid, + appviewCdnUrlPattern, +>>>>>>> main backgroundQueue, crawlers, } = resources diff --git a/packages/pds/src/services/record/index.ts b/packages/pds/src/services/record/index.ts index fe8a142f82e..748230bdcf4 100644 --- a/packages/pds/src/services/record/index.ts +++ b/packages/pds/src/services/record/index.ts @@ -73,11 +73,19 @@ export class RecordService { await this.db.db .deleteFrom('backlink') .where('uri', '=', uri.toString()) +<<<<<<< HEAD .execute() await this.db.db .deleteFrom('record') .where('uri', '=', uri.toString()) .execute() +======= + const backlinkQuery = this.db.db + .deleteFrom('backlink') + .where('uri', '=', uri.toString()) + await Promise.all([deleteQuery.execute(), backlinkQuery.execute()]) + +>>>>>>> main log.info({ uri }, 'deleted indexed record') } diff --git a/packages/pds/src/services/util/search.ts b/packages/pds/src/services/util/search.ts new file mode 100644 index 00000000000..26b3e81bf06 --- /dev/null +++ b/packages/pds/src/services/util/search.ts @@ -0,0 +1,144 @@ +import { sql } from 'kysely' +import { InvalidRequestError } from '@atproto/xrpc-server' +import Database from '../../db' +import { notSoftDeletedClause, DbRef } from '../../db/util' +import { GenericKeyset, paginate } from '../../db/pagination' + +// @TODO utilized in both pds and app-view +export const getUserSearchQueryPg = ( + db: Database, + opts: { + term: string + limit: number + cursor?: string + includeSoftDeleted?: boolean + invitedBy?: string + }, +) => { + const { ref } = db.db.dynamic + const { term, limit, cursor, includeSoftDeleted } = opts + // Matching user accounts based on handle + const distanceAccount = distance(term, ref('handle')) + const accountsQb = getMatchingAccountsQb(db, { term, includeSoftDeleted }) + return paginate(accountsQb, { + limit, + cursor, + direction: 'asc', + keyset: new SearchKeyset(distanceAccount, ref('handle')), + }) +} + +// Matching user accounts based on handle +const getMatchingAccountsQb = ( + db: Database, + opts: { term: string; includeSoftDeleted?: boolean }, +) => { + const { ref } = db.db.dynamic + const { term, includeSoftDeleted } = opts + const distanceAccount = distance(term, ref('handle')) + return db.db + .selectFrom('did_handle') + .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') + .if(!includeSoftDeleted, (qb) => + qb.where(notSoftDeletedClause(ref('repo_root'))), + ) + .where(similar(term, ref('handle'))) // Coarse filter engaging trigram index + .select(['did_handle.did as did', distanceAccount.as('distance')]) +} + +export const getUserSearchQuerySqlite = ( + db: Database, + opts: { + term: string + limit: number + cursor?: string + includeSoftDeleted?: boolean + }, +) => { + const { ref } = db.db.dynamic + const { term, limit, cursor, includeSoftDeleted } = opts + + // Take the first three words in the search term. We're going to build a dynamic query + // based on the number of words, so to keep things predictable just ignore words 4 and + // beyond. We also remove the special wildcard characters supported by the LIKE operator, + // since that's where these values are heading. + const safeWords = term + .replace(/[%_]/g, '') + .split(/\s+/) + .filter(Boolean) + .slice(0, 3) + + if (!safeWords.length) { + // Return no results. This could happen with weird input like ' % _ '. + 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, + // so that we can reliably match word prefixes using LIKE operator. + const textForMatch = sql`lower(' ' || ${ref( + 'did_handle.handle', + )} || ' ' || coalesce(${ref('profile.displayName')}, ''))` + + const keyset = new SearchKeyset(sql``, sql``) + const unpackedCursor = keyset.unpackCursor(cursor) + + return db.db + .selectFrom('did_handle') + .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') + .if(!includeSoftDeleted, (qb) => + qb.where(notSoftDeletedClause(ref('repo_root'))), + ) + .where((q) => { + safeWords.forEach((word) => { + // Match word prefixes against contents of handle and displayName + q = q.where(textForMatch, 'like', `% ${word.toLowerCase()}%`) + }) + return q + }) + .if(!!unpackedCursor, (qb) => + unpackedCursor ? qb.where('handle', '>', unpackedCursor.secondary) : qb, + ) + .orderBy('handle') + .limit(limit) +} + +// Remove leading @ in case a handle is input that way +export const cleanTerm = (term: string) => term.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})` + +// 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})` + +type Result = { distance: number; handle: string } +type LabeledResult = { primary: number; secondary: string } +export class SearchKeyset extends GenericKeyset { + labelResult(result: Result) { + return { + primary: result.distance, + secondary: result.handle, + } + } + labeledResultToCursor(labeled: LabeledResult) { + return { + primary: labeled.primary.toString().replace('0.', '.'), + secondary: labeled.secondary, + } + } + cursorToLabeledResult(cursor: { primary: string; secondary: string }) { + const distance = parseFloat(cursor.primary) + if (isNaN(distance)) { + throw new InvalidRequestError('Malformed cursor') + } + return { + primary: distance, + secondary: cursor.secondary, + } + } +} 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() diff --git a/packages/pds/tests/_util.ts b/packages/pds/tests/_util.ts index 69ea84d4826..5b6da75ee2c 100644 --- a/packages/pds/tests/_util.ts +++ b/packages/pds/tests/_util.ts @@ -13,7 +13,10 @@ import { PDS, Database } from '../src' import { FeedViewPost } from '../src/lexicon/types/app/bsky/feed/defs' import AppContext from '../src/context' import { lexToJson } from '@atproto/lexicon' +<<<<<<< HEAD import { ServerEnvironment, envToCfg, envToSecrets } from '../src/config' +======= +>>>>>>> main const ADMIN_PASSWORD = 'admin-pass' const MODERATOR_PASSWORD = 'moderator-pass' @@ -81,9 +84,26 @@ export const runTestServer = async ( adminPassword: ADMIN_PASSWORD, moderatorPassword: MODERATOR_PASSWORD, jwtSecret: 'jwt-secret', +<<<<<<< HEAD inviteRequired: false, inviteEpoch: Date.now(), triagePassword: TRIAGE_PASSWORD, +======= + availableUserDomains: ['.test'], + rateLimitsEnabled: false, + appUrlPasswordReset: 'app://forgot-password', + emailNoReplyAddress: 'noreply@blueskyweb.xyz', + publicUrl: 'https://pds.public.url', + dbPostgresUrl: process.env.DB_POSTGRES_URL, + blobstoreLocation: `${blobstoreLoc}/blobs`, + blobstoreTmp: `${blobstoreLoc}/tmp`, + maxSubscriptionBuffer: 200, + repoBackfillLimitMs: HOUR, + sequencerLeaderLockId: uniqueLockId(), + bskyAppViewEndpoint: 'http://fake_address.invalid', + bskyAppViewDid: 'did:example:fake', + dbTxLockNonce: await randomStr(32, 'base32'), +>>>>>>> main ...params, } @@ -110,6 +130,21 @@ export const runTestServer = async ( await migrationDb.close() } +<<<<<<< HEAD +======= + const blobstore = + cfg.blobstoreLocation !== undefined + ? await DiskBlobStore.create(cfg.blobstoreLocation, cfg.blobstoreTmp) + : new MemoryBlobStore() + + const pds = PDS.create({ + db, + blobstore, + repoSigningKey, + plcRotationKey, + config: cfg, + }) +>>>>>>> main 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 2abcdf5f9b8..d4bb6e45133 100644 --- a/packages/pds/tests/account-deletion.test.ts +++ b/packages/pds/tests/account-deletion.test.ts @@ -16,6 +16,10 @@ import { Blob } from '../src/db/tables/blob' 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' +<<<<<<< HEAD +======= +import { UserState } from '../src/db/tables/user-state' +>>>>>>> main describe('account deletion', () => { let server: util.TestServerInfo @@ -163,9 +167,7 @@ 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), ) @@ -226,10 +228,18 @@ type DbContents = { } const getDbContents = async (db: Database): Promise => { +<<<<<<< HEAD const [roots, users, 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(), +======= + 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(), +>>>>>>> main db.db .selectFrom('ipld_block') .orderBy('creator') diff --git a/packages/pds/tests/admin/moderation.test.ts b/packages/pds/tests/admin/moderation.test.ts index 4b5dac66352..393af913385 100644 --- a/packages/pds/tests/admin/moderation.test.ts +++ b/packages/pds/tests/admin/moderation.test.ts @@ -918,12 +918,30 @@ describe('moderation', () => { 'Must be a full moderator to perform an account takedown', ) }) +<<<<<<< HEAD:packages/pds/tests/admin/moderation.test.ts +======= + + async function reverse(actionId: number) { + await agent.api.com.atproto.admin.reverseModerationAction( + { + id: actionId, + createdBy: 'did:example:admin', + reason: 'Y', + }, + { + encoding: 'application/json', + headers: { authorization: adminAuth() }, + }, + ) + } +>>>>>>> main:packages/pds/tests/moderation.test.ts }) describe('blob takedown', () => { let post: { ref: RecordRef; images: ImageRef[] } let blob: ImageRef let actionId: number + beforeAll(async () => { post = sc.posts[sc.dids.carol][0] blob = post.images[1] @@ -963,6 +981,7 @@ describe('moderation', () => { await expect(referenceBlob).rejects.toThrow('Could not find blob:') }) +<<<<<<< HEAD:packages/pds/tests/admin/moderation.test.ts it('prevents image blob from being served, even when cached.', async () => { const attempt = agent.api.com.atproto.sync.getBlob({ did: sc.dids.carol, @@ -971,6 +990,8 @@ describe('moderation', () => { await expect(attempt).rejects.toThrow('Blob not found') }) +======= +>>>>>>> main:packages/pds/tests/moderation.test.ts it('restores blob when action is reversed.', async () => { await agent.api.com.atproto.admin.reverseModerationAction( { @@ -987,6 +1008,7 @@ 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() +<<<<<<< HEAD:packages/pds/tests/admin/moderation.test.ts // Can fetch through image server const res = await agent.api.com.atproto.sync.getBlob({ @@ -995,6 +1017,8 @@ describe('moderation', () => { }) expect(res.data.byteLength).toBeGreaterThan(9000) +======= +>>>>>>> main:packages/pds/tests/moderation.test.ts }) }) }) 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']) + }) +}) diff --git a/packages/pds/tests/handles.test.ts b/packages/pds/tests/handles.test.ts index 1461b1b4f10..dec695d9633 100644 --- a/packages/pds/tests/handles.test.ts +++ b/packages/pds/tests/handles.test.ts @@ -50,13 +50,21 @@ describe('handles', () => { await close() }) +<<<<<<< HEAD const getDbHandle = async (did: string): Promise => { +======= + const getHandleFromDb = async (did: string): Promise => { +>>>>>>> main const res = await ctx.db.db .selectFrom('did_handle') .selectAll() .where('did', '=', did) .executeTakeFirst() +<<<<<<< HEAD return res?.handle ?? null +======= + return res?.handle +>>>>>>> main } it('resolves handles', async () => { @@ -174,8 +182,13 @@ describe('handles', () => { }, { headers: sc.getHeaders(alice), encoding: 'application/json' }, ) +<<<<<<< HEAD const handle = await getDbHandle(alice) expect(handle).toBe('alice.external') +======= + const dbHandle = await getHandleFromDb(alice) + expect(dbHandle).toBe('alice.external') +>>>>>>> main const data = await idResolver.did.resolveAtprotoData(alice) expect(data.handle).toBe('alice.external') @@ -202,8 +215,13 @@ describe('handles', () => { 'External handle did not resolve to DID', ) +<<<<<<< HEAD const handle = await getDbHandle(alice) expect(handle).toBe('alice.external') +======= + const dbHandle = await getHandleFromDb(alice) + expect(dbHandle).toBe('alice.external') +>>>>>>> main }) it('allows admin overrules of service domains', async () => { @@ -217,8 +235,14 @@ describe('handles', () => { encoding: 'application/json', }, ) +<<<<<<< HEAD const handle = await getDbHandle(bob) expect(handle).toBe('bob-alt.test') +======= + + const dbHandle = await getHandleFromDb(bob) + expect(dbHandle).toBe('bob-alt.test') +>>>>>>> main }) it('allows admin override of reserved domains', async () => { @@ -233,8 +257,13 @@ describe('handles', () => { }, ) +<<<<<<< HEAD const handle = await getDbHandle(bob) expect(handle).toBe('dril.test') +======= + const dbHandle = await getHandleFromDb(bob) + expect(dbHandle).toBe('dril.test') +>>>>>>> main }) it('requires admin auth', async () => { diff --git a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap index 63c047c6e7f..2a2e2bbf886 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 [ @@ -2745,8 +3527,6 @@ Object { "viewer": Object { "blockedBy": false, "blocking": "record(0)", - "followedBy": "record(2)", - "following": "record(1)", "muted": false, }, }, @@ -2763,7 +3543,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 { @@ -2771,15 +3551,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, }, }, diff --git a/packages/pds/tests/proxied/admin.test.ts b/packages/pds/tests/proxied/admin.test.ts index 8b3d0bb2dcf..614a28ad0c2 100644 --- a/packages/pds/tests/proxied/admin.test.ts +++ b/packages/pds/tests/proxied/admin.test.ts @@ -245,6 +245,10 @@ describe('proxies admin requests', () => { }) it('takesdown and labels repos, and reverts.', async () => { +<<<<<<< HEAD +======= + const { db, services } = network.bsky.ctx +>>>>>>> main // takedown repo const { data: action } = await agent.api.com.atproto.admin.takeModerationAction( @@ -265,22 +269,22 @@ 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) }, }, ) - await expect(tryGetProfilePds).rejects.toThrow( - 'Account has been taken down', - ) await expect(tryGetProfileAppview).rejects.toThrow( 'Account has been taken down', ) +<<<<<<< HEAD +======= + const labelsA = await services + .label(db.getPrimary()) + .getLabels(sc.dids.alice, { includeNeg: false, skipCache: true }) + expect(labelsA.map((l) => l.val)).toEqual(['dogs']) +>>>>>>> main // reverse action await agent.api.com.atproto.admin.reverseModerationAction( { id: action.id, createdBy: 'did:example:admin', reason: 'X' }, @@ -290,25 +294,29 @@ 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) }, }, ) - expect(profilePds).toEqual( - expect.objectContaining({ did: sc.dids.alice, handle: 'alice.test' }), - ) expect(profileAppview).toEqual( expect.objectContaining({ did: sc.dids.alice, handle: 'alice.test' }), ) +<<<<<<< HEAD + }) + + it('takesdown and labels records, and reverts.', async () => { +======= + 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.bsky.ctx +>>>>>>> main const post = sc.posts[sc.dids.alice][0] // takedown post const { data: action } = @@ -331,11 +339,25 @@ describe('proxies admin requests', () => { }, ) // check thread and labels +<<<<<<< HEAD const tryGetPost = agent.api.app.bsky.feed.getPostThread( { uri: post.ref.uriStr, depth: 0 }, { headers: sc.getHeaders(sc.dids.carol) }, ) await expect(tryGetPost).rejects.toThrow(NotFoundError) +======= + 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(tryGetPostAppview).rejects.toThrow(NotFoundError) + const labelsA = await services + .label(db.getPrimary()) + .getLabels(post.ref.uriStr, { includeNeg: false, skipCache: true }) + expect(labelsA.map((l) => l.val)).toEqual(['dogs']) +>>>>>>> main // reverse action await agent.api.com.atproto.admin.reverseModerationAction( { id: action.id, createdBy: 'did:example:admin', reason: 'X' }, @@ -345,22 +367,22 @@ 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) }, }, ) - 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 }), ) +<<<<<<< HEAD +======= + const labelsB = await services + .label(db.getPrimary()) + .getLabels(post.ref.uriStr, { includeNeg: false, skipCache: true }) + expect(labelsB.map((l) => l.val)).toEqual(['cats']) +>>>>>>> main }) it('does not persist actions and reports on pds.', async () => { diff --git a/packages/pds/tests/proxied/views.test.ts b/packages/pds/tests/proxied/views.test.ts index 43ea29e5d23..38b486a1d05 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() }) beforeAll(async () => { @@ -193,6 +196,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 0f6680f6a52..767ecdc18f1 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 = {} } @@ -367,6 +372,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/pds/tests/server.test.ts b/packages/pds/tests/server.test.ts index c4ca73c72c7..33381f55631 100644 --- a/packages/pds/tests/server.test.ts +++ b/packages/pds/tests/server.test.ts @@ -1,12 +1,21 @@ import { AddressInfo } from 'net' import express from 'express' import axios, { AxiosError } from 'axios' +<<<<<<< HEAD import AtpAgent from '@atproto/api' +======= +import AtpAgent, { AtUri } from '@atproto/api' +import { CloseFn, runTestServer, TestServerInfo } from './_util' +>>>>>>> main import { handler as errorHandler } from '../src/error' import { SeedClient } from './seeds/client' import basicSeed from './seeds/basic' import { Database } from '../src' +<<<<<<< HEAD import { TestNetwork } from '@atproto/dev-env' +======= +import { randomStr } from '@atproto/crypto' +>>>>>>> main describe('server', () => { let network: TestNetwork @@ -87,8 +96,30 @@ 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( +<<<<<<< HEAD `${network.pds.url}/xrpc/app.bsky.feed.getTimeline`, +======= + `${server.url}/xrpc/com.atproto.repo.getRecord?repo=${uri.host}&collection=${uri.collection}&rkey=${uri.rkey}`, +>>>>>>> main { decompress: false, headers: { ...sc.getHeaders(alice), 'accept-encoding': 'gzip' }, @@ -122,7 +153,11 @@ describe('server', () => { it('healthcheck fails when database is unavailable.', async () => { // destroy to release lock & allow db to close +<<<<<<< HEAD await network.pds.ctx.sequencerLeader?.destroy() +======= + await server.ctx.sequencerLeader?.destroy() +>>>>>>> main await db.close() let error: AxiosError 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/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 9c37695f1d8..bcb9d5eb452 100644 --- a/packages/repo/package.json +++ b/packages/repo/package.json @@ -1,17 +1,23 @@ { "name": "@atproto/repo", - "version": "0.3.0", - "main": "src/index.ts", - "publishConfig": { - "main": "dist/index.js", - "types": "dist/index.d.ts" - }, + "version": "0.3.1", "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/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 } } 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/README.md b/packages/syntax/README.md index 5fd557fa323..0658b64d59c 100644 --- a/packages/syntax/README.md +++ b/packages/syntax/README.md @@ -1,11 +1,18 @@ -# 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 * as identifier from '@atproto/syntax' +import { isValidHandle, ensureValidHandle, isValidDid } from '@atproto/syntax' isValidHandle('alice.test') // returns true ensureValidHandle('alice.test') // returns void @@ -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 60fabdfe8d2..54488519d50 100644 --- a/packages/syntax/package.json +++ b/packages/syntax/package.json @@ -1,6 +1,20 @@ { "name": "@atproto/syntax", - "version": "0.1.0", + "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/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 1060dda6890..00000000000 --- a/packages/uri/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "@atproto/uri", - "version": "0.1.0", - "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/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/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 e51db0f0a0f..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.0", + "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-server/src/rate-limiter.ts b/packages/xrpc-server/src/rate-limiter.ts index 82719101674..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 } @@ -61,13 +64,16 @@ 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 + 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) @@ -82,7 +88,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 +125,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 +154,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..4e0a84ce4b7 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 @@ -401,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] @@ -416,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, @@ -475,14 +480,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..dc75627a95f 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 @@ -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 { @@ -224,7 +230,7 @@ export class ForbiddenError extends XRPCError { export class RateLimitExceededError extends XRPCError { constructor( - status: RateLimiterStatus, + public status: RateLimiterStatus, errorMessage?: string, customErrorName?: string, ) { 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/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 4fd825acd06..68ee51b35b8 100644 --- a/packages/xrpc/package.json +++ b/packages/xrpc/package.json @@ -1,6 +1,18 @@ { "name": "@atproto/xrpc", - "version": "0.3.0", + "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" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5456bfda698..698a8bc86b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -216,9 +216,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 @@ -295,6 +292,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 @@ -390,12 +390,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': @@ -469,12 +463,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': @@ -549,18 +537,12 @@ 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 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 @@ -677,12 +659,6 @@ importers: specifier: workspace:^ version: link:../common-web - packages/uri: - dependencies: - '@atproto/syntax': - specifier: workspace:^ - version: link:../syntax - packages/xrpc: dependencies: '@atproto/lexicon': @@ -9004,11 +8980,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: @@ -10837,6 +10808,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): diff --git a/services/pds/index.js b/services/pds/index.js index a99b7ae980e..f69f0b6f6f7 100644 --- a/services/pds/index.js +++ b/services/pds/index.js @@ -12,6 +12,7 @@ require('dd-trace') // Only works with commonjs // Tracer code above must come before anything else const path = require('path') +<<<<<<< HEAD const { PDS, Database, @@ -19,6 +20,14 @@ const { envToSecrets, readEnv, httpLogger, +======= +const { KmsKeypair, S3BlobStore } = require('@atproto/aws') +const { + Database, + ServerConfig, + PDS, + ViewMaintainer, +>>>>>>> main PeriodicModerationActionReversal, } = require('@atproto/pds') const pkg = require('@atproto/pds/package.json') @@ -40,6 +49,27 @@ const main = async () => { } else { await pds.ctx.db.migrateToLatestOrThrow() } +<<<<<<< HEAD +======= + const cfg = ServerConfig.readEnv({ + port: env.port, + recoveryKey, + emailSmtpUrl: smtpUrl({ + host: env.smtpHost, + username: env.smtpUsername, + password: env.smtpPassword, + }), + }) + const pds = PDS.create({ + db, + blobstore: s3Blobstore, + repoSigningKey, + plcRotationKey, + config: cfg, + }) + const viewMaintainer = new ViewMaintainer(migrateDb) + const viewMaintainerRunning = viewMaintainer.run() +>>>>>>> main // If the PDS is configured to proxy moderation, this will be running on appview instead of pds. // Also don't run this on the sequencer leader, which may not be configured regarding moderation proxying at all. @@ -66,6 +96,50 @@ const main = async () => { }) } +<<<<<<< HEAD +======= +const pgUrl = ({ + username = 'postgres', + password = 'postgres', + host = 'localhost', + port = '5432', + database = 'postgres', + sslmode, +}) => { + const enc = encodeURIComponent + return `postgresql://${username}:${enc( + password, + )}@${host}:${port}/${database}${sslmode ? `?sslmode=${enc(sslmode)}` : ''}` +} + +const smtpUrl = ({ username, password, host }) => { + const enc = encodeURIComponent + return `smtps://${username}:${enc(password)}@${host}` +} + +const maybeParseInt = (str) => { + const parsed = parseInt(str) + return isNaN(parsed) ? undefined : parsed +} + +const getEnv = () => ({ + port: parseInt(process.env.PORT), + plcRotationKeyId: process.env.PLC_ROTATION_KEY_ID, + repoSigningKey: process.env.REPO_SIGNING_KEY, + recoveryKeyId: process.env.RECOVERY_KEY_ID, + dbCreds: JSON.parse(process.env.DB_CREDS_JSON), + dbMigrateCreds: JSON.parse(process.env.DB_MIGRATE_CREDS_JSON), + dbSchema: process.env.DB_SCHEMA || undefined, + dbPoolSize: maybeParseInt(process.env.DB_POOL_SIZE), + dbPoolMaxUses: maybeParseInt(process.env.DB_POOL_MAX_USES), + dbPoolIdleTimeoutMs: maybeParseInt(process.env.DB_POOL_IDLE_TIMEOUT_MS), + smtpHost: process.env.SMTP_HOST, + smtpUsername: process.env.SMTP_USERNAME, + smtpPassword: process.env.SMTP_PASSWORD, + s3Bucket: process.env.S3_BUCKET_NAME, +}) + +>>>>>>> main const maintainXrpcResource = (span, req) => { // Show actual xrpc method as resource rather than the route pattern if (span && req.originalUrl?.startsWith('/xrpc/')) { From 8972adc0d2349554e1b3e742dcc38ea3d60895e7 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 26 Sep 2023 18:43:01 -0500 Subject: [PATCH 078/105] merge pds --- packages/pds/package.json | 4 - .../pds/src/api/app/bsky/actor/getProfile.ts | 4 +- .../pds/src/api/app/bsky/actor/getProfiles.ts | 4 +- .../src/api/app/bsky/feed/getActorFeeds.ts | 2 +- .../src/api/app/bsky/feed/getActorLikes.ts | 2 +- .../src/api/app/bsky/feed/getAuthorFeed.ts | 2 +- packages/pds/src/api/app/bsky/feed/getFeed.ts | 4 +- .../src/api/app/bsky/feed/getFeedGenerator.ts | 2 +- .../api/app/bsky/feed/getFeedGenerators.ts | 2 +- .../pds/src/api/app/bsky/feed/getLikes.ts | 2 +- .../pds/src/api/app/bsky/feed/getListFeed.ts | 2 +- .../src/api/app/bsky/feed/getPostThread.ts | 4 +- .../pds/src/api/app/bsky/feed/getPosts.ts | 2 +- .../src/api/app/bsky/feed/getRepostedBy.ts | 2 +- .../api/app/bsky/feed/getSuggestedFeeds.ts | 2 +- .../pds/src/api/app/bsky/feed/getTimeline.ts | 2 +- .../pds/src/api/app/bsky/graph/getBlocks.ts | 2 +- .../src/api/app/bsky/graph/getFollowers.ts | 2 +- .../pds/src/api/app/bsky/graph/getFollows.ts | 2 +- .../pds/src/api/app/bsky/graph/getList.ts | 2 +- .../src/api/app/bsky/graph/getListBlocks.ts | 2 +- .../src/api/app/bsky/graph/getListMutes.ts | 2 +- .../pds/src/api/app/bsky/graph/getLists.ts | 2 +- .../pds/src/api/app/bsky/graph/getMutes.ts | 2 +- .../bsky/graph/getSuggestedFollowsByActor.ts | 2 +- .../pds/src/api/app/bsky/graph/muteActor.ts | 25 +- .../src/api/app/bsky/graph/muteActorList.ts | 23 +- .../pds/src/api/app/bsky/graph/unmuteActor.ts | 21 +- .../src/api/app/bsky/graph/unmuteActorList.ts | 14 +- .../app/bsky/notification/getUnreadCount.ts | 2 +- .../bsky/notification/listNotifications.ts | 2 +- .../api/app/bsky/notification/registerPush.ts | 2 +- .../api/app/bsky/notification/updateSeen.ts | 33 +- .../api/app/bsky/preferences/getProfile.ts | 2 +- .../api/app/bsky/preferences/getProfiles.ts | 2 +- .../app/bsky/preferences/getSuggestions.ts | 2 +- .../api/app/bsky/preferences/searchActors.ts | 2 +- .../bsky/preferences/searchActorsTypeahead.ts | 2 +- .../src/api/app/bsky/unspecced/getPopular.ts | 2 +- .../unspecced/getPopularFeedGenerators.ts | 2 +- .../api/com/atproto/identity/resolveHandle.ts | 6 - .../pds/src/api/com/atproto/repo/getRecord.ts | 14 - packages/pds/src/config.ts | 498 ------------------ packages/pds/src/config/config.ts | 4 + packages/pds/src/config/env.ts | 2 + packages/pds/src/context.ts | 111 ---- packages/pds/src/db/database-schema.ts | 15 - packages/pds/src/db/migrations/index.ts | 67 --- packages/pds/src/image/index.ts | 20 - packages/pds/src/index.ts | 142 +---- packages/pds/src/services/account/index.ts | 120 ----- packages/pds/src/services/index.ts | 12 - packages/pds/src/services/local/index.ts | 14 +- packages/pds/src/services/record/index.ts | 10 +- packages/pds/tests/_util.ts | 35 -- 55 files changed, 74 insertions(+), 1192 deletions(-) delete mode 100644 packages/pds/src/config.ts diff --git a/packages/pds/package.json b/packages/pds/package.json index 6a0daa031a4..6d86a8860e9 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -1,10 +1,6 @@ { "name": "@atproto/pds", -<<<<<<< HEAD "version": "0.3.0-beta.3", -======= - "version": "0.1.18", ->>>>>>> main "license": "MIT", "description": "Reference implementation of atproto Personal Data Server (PDS)", "keywords": [ diff --git a/packages/pds/src/api/app/bsky/actor/getProfile.ts b/packages/pds/src/api/app/bsky/actor/getProfile.ts index c4e38e4a101..c200e1dd75f 100644 --- a/packages/pds/src/api/app/bsky/actor/getProfile.ts +++ b/packages/pds/src/api/app/bsky/actor/getProfile.ts @@ -11,7 +11,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ req, auth, params }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - const res = await ctx.appviewAgent.api.app.bsky.actor.getProfile( + const res = await ctx.appViewAgent.api.app.bsky.actor.getProfile( params, requester ? await ctx.serviceAuthHeaders(requester) : authPassthru(req), ) @@ -35,4 +35,4 @@ const getProfileMunge = async ( return ctx.services .local(ctx.db) .updateProfileDetailed(original, local.profile.record) -} \ No newline at end of file +} diff --git a/packages/pds/src/api/app/bsky/actor/getProfiles.ts b/packages/pds/src/api/app/bsky/actor/getProfiles.ts index 2b1bbf35052..ebec9e36938 100644 --- a/packages/pds/src/api/app/bsky/actor/getProfiles.ts +++ b/packages/pds/src/api/app/bsky/actor/getProfiles.ts @@ -9,7 +9,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ auth, params }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.actor.getProfiles( + const res = await ctx.appViewAgent.api.app.bsky.actor.getProfiles( params, await ctx.serviceAuthHeaders(requester), ) @@ -43,4 +43,4 @@ const getProfilesMunge = async ( ...original, profiles, } -} \ No newline at end of file +} diff --git a/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts b/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts index ec77754b4b2..da99617178f 100644 --- a/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts +++ b/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ auth, params }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.feed.getActorFeeds( + const res = await ctx.appViewAgent.api.app.bsky.feed.getActorFeeds( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/feed/getActorLikes.ts b/packages/pds/src/api/app/bsky/feed/getActorLikes.ts index 53557c6ae4c..9c0c38c5a20 100644 --- a/packages/pds/src/api/app/bsky/feed/getActorLikes.ts +++ b/packages/pds/src/api/app/bsky/feed/getActorLikes.ts @@ -12,7 +12,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - const res = await ctx.appviewAgent.api.app.bsky.feed.getActorLikes( + const res = await ctx.appViewAgent.api.app.bsky.feed.getActorLikes( params, requester ? await ctx.serviceAuthHeaders(requester) : authPassthru(req), ) diff --git a/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts index 7237a2df755..6563812fb9a 100644 --- a/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts @@ -12,7 +12,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ req, params, auth }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - const res = await ctx.appviewAgent.api.app.bsky.feed.getAuthorFeed( + const res = await ctx.appViewAgent.api.app.bsky.feed.getAuthorFeed( params, requester ? await ctx.serviceAuthHeaders(requester) : authPassthru(req), ) diff --git a/packages/pds/src/api/app/bsky/feed/getFeed.ts b/packages/pds/src/api/app/bsky/feed/getFeed.ts index 6ed14b0546c..051b0c7bcdf 100644 --- a/packages/pds/src/api/app/bsky/feed/getFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getFeed.ts @@ -8,11 +8,11 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did const { data: feed } = - await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( + 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( + const res = await ctx.appViewAgent.api.app.bsky.feed.getFeed( params, await ctx.serviceAuthHeaders(requester, feed.view.did), ) diff --git a/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts b/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts index b9451ca16c3..28c404b58e8 100644 --- a/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts +++ b/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( + const res = await ctx.appViewAgent.api.app.bsky.feed.getFeedGenerator( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts b/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts index 1d085830004..12cf9e91c0a 100644 --- a/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts +++ b/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerators( + const res = await ctx.appViewAgent.api.app.bsky.feed.getFeedGenerators( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/feed/getLikes.ts b/packages/pds/src/api/app/bsky/feed/getLikes.ts index 75197acbcc8..771cc511cd4 100644 --- a/packages/pds/src/api/app/bsky/feed/getLikes.ts +++ b/packages/pds/src/api/app/bsky/feed/getLikes.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.feed.getLikes( + const res = await ctx.appViewAgent.api.app.bsky.feed.getLikes( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/feed/getListFeed.ts b/packages/pds/src/api/app/bsky/feed/getListFeed.ts index 7344b2476ba..34b8630a933 100644 --- a/packages/pds/src/api/app/bsky/feed/getListFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getListFeed.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ auth, params }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.feed.getListFeed( + const res = await ctx.appViewAgent.api.app.bsky.feed.getListFeed( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/feed/getPostThread.ts b/packages/pds/src/api/app/bsky/feed/getPostThread.ts index e8dd062da14..3b719648f44 100644 --- a/packages/pds/src/api/app/bsky/feed/getPostThread.ts +++ b/packages/pds/src/api/app/bsky/feed/getPostThread.ts @@ -29,7 +29,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth }) => { const requester = auth.credentials.did try { - const res = await ctx.appviewAgent.api.app.bsky.feed.getPostThread( + const res = await ctx.appViewAgent.api.app.bsky.feed.getPostThread( params, await ctx.serviceAuthHeaders(requester), ) @@ -185,7 +185,7 @@ const readAfterWriteNotFound = async ( const highestParent = getHighestParent(thread) if (highestParent) { try { - const parentsRes = await ctx.appviewAgent.api.app.bsky.feed.getPostThread( + const parentsRes = await ctx.appViewAgent.api.app.bsky.feed.getPostThread( { uri: highestParent, parentHeight: params.parentHeight, depth: 0 }, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/feed/getPosts.ts b/packages/pds/src/api/app/bsky/feed/getPosts.ts index 05173c48ef9..1b755450f63 100644 --- a/packages/pds/src/api/app/bsky/feed/getPosts.ts +++ b/packages/pds/src/api/app/bsky/feed/getPosts.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.feed.getPosts( + const res = await ctx.appViewAgent.api.app.bsky.feed.getPosts( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts b/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts index 44a5b15191d..30e72b434e6 100644 --- a/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts +++ b/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.feed.getRepostedBy( + const res = await ctx.appViewAgent.api.app.bsky.feed.getRepostedBy( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts b/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts index 9c8d338104b..733405b3b42 100644 --- a/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts +++ b/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ auth, params }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.feed.getSuggestedFeeds( + const res = await ctx.appViewAgent.api.app.bsky.feed.getSuggestedFeeds( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/feed/getTimeline.ts b/packages/pds/src/api/app/bsky/feed/getTimeline.ts index 7d4e52ce918..2c3e2ed44d6 100644 --- a/packages/pds/src/api/app/bsky/feed/getTimeline.ts +++ b/packages/pds/src/api/app/bsky/feed/getTimeline.ts @@ -9,7 +9,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.feed.getTimeline( + const res = await ctx.appViewAgent.api.app.bsky.feed.getTimeline( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/graph/getBlocks.ts b/packages/pds/src/api/app/bsky/graph/getBlocks.ts index 284dafd3034..f66eb64b945 100644 --- a/packages/pds/src/api/app/bsky/graph/getBlocks.ts +++ b/packages/pds/src/api/app/bsky/graph/getBlocks.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.graph.getBlocks( + const res = await ctx.appViewAgent.api.app.bsky.graph.getBlocks( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/graph/getFollowers.ts b/packages/pds/src/api/app/bsky/graph/getFollowers.ts index da0541d7e75..389f92d4e14 100644 --- a/packages/pds/src/api/app/bsky/graph/getFollowers.ts +++ b/packages/pds/src/api/app/bsky/graph/getFollowers.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ req, params, auth }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - const res = await ctx.appviewAgent.api.app.bsky.graph.getFollowers( + const res = await ctx.appViewAgent.api.app.bsky.graph.getFollowers( params, requester ? await ctx.serviceAuthHeaders(requester) : authPassthru(req), ) diff --git a/packages/pds/src/api/app/bsky/graph/getFollows.ts b/packages/pds/src/api/app/bsky/graph/getFollows.ts index f49c812f9ca..343fd81d414 100644 --- a/packages/pds/src/api/app/bsky/graph/getFollows.ts +++ b/packages/pds/src/api/app/bsky/graph/getFollows.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ req, params, auth }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - const res = await ctx.appviewAgent.api.app.bsky.graph.getFollows( + const res = await ctx.appViewAgent.api.app.bsky.graph.getFollows( params, requester ? await ctx.serviceAuthHeaders(requester) : authPassthru(req), ) diff --git a/packages/pds/src/api/app/bsky/graph/getList.ts b/packages/pds/src/api/app/bsky/graph/getList.ts index 5fd3c93df75..061d6759c2c 100644 --- a/packages/pds/src/api/app/bsky/graph/getList.ts +++ b/packages/pds/src/api/app/bsky/graph/getList.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.graph.getList( + const res = await ctx.appViewAgent.api.app.bsky.graph.getList( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/graph/getListBlocks.ts b/packages/pds/src/api/app/bsky/graph/getListBlocks.ts index 04fd55a324e..83975782fa4 100644 --- a/packages/pds/src/api/app/bsky/graph/getListBlocks.ts +++ b/packages/pds/src/api/app/bsky/graph/getListBlocks.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ auth, params }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.graph.getListBlocks( + const res = await ctx.appViewAgent.api.app.bsky.graph.getListBlocks( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/graph/getListMutes.ts b/packages/pds/src/api/app/bsky/graph/getListMutes.ts index e0a624a3864..05f6ce1ab09 100644 --- a/packages/pds/src/api/app/bsky/graph/getListMutes.ts +++ b/packages/pds/src/api/app/bsky/graph/getListMutes.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.graph.getListMutes( + const res = await ctx.appViewAgent.api.app.bsky.graph.getListMutes( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/graph/getLists.ts b/packages/pds/src/api/app/bsky/graph/getLists.ts index e43a8d2b1d6..6c8f6452ea4 100644 --- a/packages/pds/src/api/app/bsky/graph/getLists.ts +++ b/packages/pds/src/api/app/bsky/graph/getLists.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.graph.getLists( + const res = await ctx.appViewAgent.api.app.bsky.graph.getLists( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/graph/getMutes.ts b/packages/pds/src/api/app/bsky/graph/getMutes.ts index 9aa6b74445c..12ff1a032a0 100644 --- a/packages/pds/src/api/app/bsky/graph/getMutes.ts +++ b/packages/pds/src/api/app/bsky/graph/getMutes.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ auth, params }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.graph.getMutes( + const res = await ctx.appViewAgent.api.app.bsky.graph.getMutes( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts b/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts index 1db1c7f498f..53125cbc517 100644 --- a/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts +++ b/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts @@ -7,7 +7,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ auth, params }) => { const requester = auth.credentials.did const res = - await ctx.appviewAgent.api.app.bsky.graph.getSuggestedFollowsByActor( + await ctx.appViewAgent.api.app.bsky.graph.getSuggestedFollowsByActor( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/graph/muteActor.ts b/packages/pds/src/api/app/bsky/graph/muteActor.ts index 44d4747f5e9..289783ac376 100644 --- a/packages/pds/src/api/app/bsky/graph/muteActor.ts +++ b/packages/pds/src/api/app/bsky/graph/muteActor.ts @@ -1,4 +1,3 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' @@ -6,28 +5,10 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.muteActor({ auth: ctx.accessVerifier, handler: async ({ auth, input }) => { - const { actor } = input.body const requester = auth.credentials.did - const { db, services } = ctx - - const subject = await services.account(db).getAccount(actor) - if (!subject) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } - if (subject.did === requester) { - throw new InvalidRequestError('Cannot mute oneself') - } - - if (ctx.canProxyWrite()) { - await ctx.appviewAgent.api.app.bsky.graph.muteActor(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), - encoding: 'application/json', - }) - } - - await services.account(db).mute({ - did: subject.did, - mutedByDid: requester, + await ctx.appViewAgent.api.app.bsky.graph.muteActor(input.body, { + ...(await ctx.serviceAuthHeaders(requester)), + encoding: 'application/json', }) }, }) diff --git a/packages/pds/src/api/app/bsky/graph/muteActorList.ts b/packages/pds/src/api/app/bsky/graph/muteActorList.ts index e554f7fce8b..441571a26b9 100644 --- a/packages/pds/src/api/app/bsky/graph/muteActorList.ts +++ b/packages/pds/src/api/app/bsky/graph/muteActorList.ts @@ -1,32 +1,15 @@ 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' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.muteActorList({ auth: ctx.accessVerifier, handler: async ({ auth, input }) => { - const { list } = input.body const requester = auth.credentials.did - const listUri = new AtUri(list) - const collId = lex.ids.AppBskyGraphList - if (listUri.collection !== collId) { - throw new InvalidRequestError(`Invalid collection: expected: ${collId}`) - } - - if (ctx.canProxyWrite()) { - await ctx.appviewAgent.api.app.bsky.graph.muteActorList(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), - encoding: 'application/json', - }) - } - - await ctx.services.account(ctx.db).muteActorList({ - list, - mutedByDid: requester, + await ctx.appViewAgent.api.app.bsky.graph.muteActorList(input.body, { + ...(await ctx.serviceAuthHeaders(requester)), + encoding: 'application/json', }) }, }) diff --git a/packages/pds/src/api/app/bsky/graph/unmuteActor.ts b/packages/pds/src/api/app/bsky/graph/unmuteActor.ts index 84819cc1e15..586b12565d6 100644 --- a/packages/pds/src/api/app/bsky/graph/unmuteActor.ts +++ b/packages/pds/src/api/app/bsky/graph/unmuteActor.ts @@ -1,30 +1,15 @@ import { Server } from '../../../../lexicon' -import { InvalidRequestError } from '@atproto/xrpc-server' import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.unmuteActor({ auth: ctx.accessVerifier, handler: async ({ auth, input }) => { - const { actor } = input.body const requester = auth.credentials.did - const { db, services } = ctx - const subject = await services.account(db).getAccount(actor) - if (!subject) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } - - if (ctx.canProxyWrite()) { - await ctx.appviewAgent.api.app.bsky.graph.unmuteActor(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), - encoding: 'application/json', - }) - } - - await services.account(db).unmute({ - did: subject.did, - mutedByDid: requester, + await ctx.appViewAgent.api.app.bsky.graph.unmuteActor(input.body, { + ...(await ctx.serviceAuthHeaders(requester)), + encoding: 'application/json', }) }, }) diff --git a/packages/pds/src/api/app/bsky/graph/unmuteActorList.ts b/packages/pds/src/api/app/bsky/graph/unmuteActorList.ts index ce3c1a4b254..e8ba9f8c4d4 100644 --- a/packages/pds/src/api/app/bsky/graph/unmuteActorList.ts +++ b/packages/pds/src/api/app/bsky/graph/unmuteActorList.ts @@ -5,19 +5,11 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.unmuteActorList({ auth: ctx.accessVerifier, handler: async ({ auth, input }) => { - const { list } = input.body const requester = auth.credentials.did - if (ctx.canProxyWrite()) { - await ctx.appviewAgent.api.app.bsky.graph.unmuteActorList(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), - encoding: 'application/json', - }) - } - - await ctx.services.account(ctx.db).unmuteActorList({ - list, - mutedByDid: requester, + await ctx.appViewAgent.api.app.bsky.graph.unmuteActorList(input.body, { + ...(await ctx.serviceAuthHeaders(requester)), + encoding: 'application/json', }) }, }) diff --git a/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts b/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts index e8100b183c5..c8b723403d5 100644 --- a/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts +++ b/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts @@ -7,7 +7,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ auth, params }) => { const requester = auth.credentials.did const res = - await ctx.appviewAgent.api.app.bsky.notification.getUnreadCount( + await ctx.appViewAgent.api.app.bsky.notification.getUnreadCount( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/notification/listNotifications.ts b/packages/pds/src/api/app/bsky/notification/listNotifications.ts index 2f667172a57..48e75304af5 100644 --- a/packages/pds/src/api/app/bsky/notification/listNotifications.ts +++ b/packages/pds/src/api/app/bsky/notification/listNotifications.ts @@ -7,7 +7,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth }) => { const requester = auth.credentials.did const res = - await ctx.appviewAgent.api.app.bsky.notification.listNotifications( + await ctx.appViewAgent.api.app.bsky.notification.listNotifications( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/notification/registerPush.ts b/packages/pds/src/api/app/bsky/notification/registerPush.ts index 647e0573a2a..d5db39f1ac7 100644 --- a/packages/pds/src/api/app/bsky/notification/registerPush.ts +++ b/packages/pds/src/api/app/bsky/notification/registerPush.ts @@ -3,7 +3,7 @@ import AppContext from '../../../../context' import { getNotif } from '@atproto/identity' import { InvalidRequestError } from '@atproto/xrpc-server' import { AtpAgent } from '@atproto/api' -import { getDidDoc } from './util' +import { getDidDoc } from '../util/resolver' export default function (server: Server, ctx: AppContext) { server.app.bsky.notification.registerPush({ diff --git a/packages/pds/src/api/app/bsky/notification/updateSeen.ts b/packages/pds/src/api/app/bsky/notification/updateSeen.ts index 2c115a61f67..44fe4bc13cc 100644 --- a/packages/pds/src/api/app/bsky/notification/updateSeen.ts +++ b/packages/pds/src/api/app/bsky/notification/updateSeen.ts @@ -1,4 +1,3 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' @@ -6,36 +5,12 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.notification.updateSeen({ auth: ctx.accessVerifier, handler: async ({ input, auth }) => { - const { seenAt } = input.body const requester = auth.credentials.did - let parsed: string - try { - parsed = new Date(seenAt).toISOString() - } catch (_err) { - throw new InvalidRequestError('Invalid date') - } - - const user = await ctx.services.account(ctx.db).getAccount(requester) - if (!user) { - throw new InvalidRequestError(`Could not find user: ${requester}`) - } - - if (ctx.canProxyWrite()) { - await ctx.appviewAgent.api.app.bsky.notification.updateSeen( - input.body, - { - ...(await ctx.serviceAuthHeaders(requester)), - encoding: 'application/json', - }, - ) - } - - await ctx.db.db - .updateTable('user_state') - .set({ lastSeenNotifs: parsed }) - .where('did', '=', user.did) - .executeTakeFirst() + await ctx.appViewAgent.api.app.bsky.notification.updateSeen(input.body, { + ...(await ctx.serviceAuthHeaders(requester)), + encoding: 'application/json', + }) }, }) } diff --git a/packages/pds/src/api/app/bsky/preferences/getProfile.ts b/packages/pds/src/api/app/bsky/preferences/getProfile.ts index 52858515827..c200e1dd75f 100644 --- a/packages/pds/src/api/app/bsky/preferences/getProfile.ts +++ b/packages/pds/src/api/app/bsky/preferences/getProfile.ts @@ -11,7 +11,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ req, auth, params }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - const res = await ctx.appviewAgent.api.app.bsky.actor.getProfile( + const res = await ctx.appViewAgent.api.app.bsky.actor.getProfile( params, requester ? await ctx.serviceAuthHeaders(requester) : authPassthru(req), ) diff --git a/packages/pds/src/api/app/bsky/preferences/getProfiles.ts b/packages/pds/src/api/app/bsky/preferences/getProfiles.ts index 46af4b08a0c..ebec9e36938 100644 --- a/packages/pds/src/api/app/bsky/preferences/getProfiles.ts +++ b/packages/pds/src/api/app/bsky/preferences/getProfiles.ts @@ -9,7 +9,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ auth, params }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.actor.getProfiles( + const res = await ctx.appViewAgent.api.app.bsky.actor.getProfiles( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/preferences/getSuggestions.ts b/packages/pds/src/api/app/bsky/preferences/getSuggestions.ts index c3ceb16cf14..03c6fa6e609 100644 --- a/packages/pds/src/api/app/bsky/preferences/getSuggestions.ts +++ b/packages/pds/src/api/app/bsky/preferences/getSuggestions.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ params, auth }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.actor.getSuggestions( + const res = await ctx.appViewAgent.api.app.bsky.actor.getSuggestions( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/preferences/searchActors.ts b/packages/pds/src/api/app/bsky/preferences/searchActors.ts index 921f4363bfc..0ae4d9b211c 100644 --- a/packages/pds/src/api/app/bsky/preferences/searchActors.ts +++ b/packages/pds/src/api/app/bsky/preferences/searchActors.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ auth, params }) => { const requester = auth.credentials.did - const res = await ctx.appviewAgent.api.app.bsky.actor.searchActors( + const res = await ctx.appViewAgent.api.app.bsky.actor.searchActors( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/preferences/searchActorsTypeahead.ts b/packages/pds/src/api/app/bsky/preferences/searchActorsTypeahead.ts index d8ef8f72dda..3ece0083ec2 100644 --- a/packages/pds/src/api/app/bsky/preferences/searchActorsTypeahead.ts +++ b/packages/pds/src/api/app/bsky/preferences/searchActorsTypeahead.ts @@ -7,7 +7,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth }) => { const requester = auth.credentials.did const res = - await ctx.appviewAgent.api.app.bsky.actor.searchActorsTypeahead( + await ctx.appViewAgent.api.app.bsky.actor.searchActorsTypeahead( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/app/bsky/unspecced/getPopular.ts b/packages/pds/src/api/app/bsky/unspecced/getPopular.ts index c4b4736cdc0..f890ea7baed 100644 --- a/packages/pds/src/api/app/bsky/unspecced/getPopular.ts +++ b/packages/pds/src/api/app/bsky/unspecced/getPopular.ts @@ -10,7 +10,7 @@ export default function (server: Server, ctx: AppContext) { 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( + 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), ) diff --git a/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts index 3f41f18560c..abc556cdb70 100644 --- a/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ auth, params }) => { const requester = auth.credentials.did const res = - await ctx.appviewAgent.api.app.bsky.unspecced.getPopularFeedGenerators( + await ctx.appViewAgent.api.app.bsky.unspecced.getPopularFeedGenerators( params, await ctx.serviceAuthHeaders(requester), ) diff --git a/packages/pds/src/api/com/atproto/identity/resolveHandle.ts b/packages/pds/src/api/com/atproto/identity/resolveHandle.ts index 76b93e16b08..2a29956c1d9 100644 --- a/packages/pds/src/api/com/atproto/identity/resolveHandle.ts +++ b/packages/pds/src/api/com/atproto/identity/resolveHandle.ts @@ -33,14 +33,8 @@ export default function (server: Server, ctx: AppContext) { } // this is not someone on our server, but we help with resolving anyway -<<<<<<< HEAD - if (!did) { did = await tryResolveFromAppview(ctx.appViewAgent, handle) -======= - if (!did) { - did = await tryResolveFromAppview(ctx.appviewAgent, handle) ->>>>>>> main } if (!did) { diff --git a/packages/pds/src/api/com/atproto/repo/getRecord.ts b/packages/pds/src/api/com/atproto/repo/getRecord.ts index b07f2ad2a95..5362b821b9f 100644 --- a/packages/pds/src/api/com/atproto/repo/getRecord.ts +++ b/packages/pds/src/api/com/atproto/repo/getRecord.ts @@ -17,23 +17,9 @@ export default function (server: Server, ctx: AppContext) { if (!record) { throw new InvalidRequestError(`Could not locate record: ${uri}`) } -<<<<<<< HEAD } const res = await ctx.appViewAgent.api.com.atproto.repo.getRecord(params) -======= - return { - encoding: 'application/json', - body: { - uri: record.uri, - cid: record.cid, - value: record.value, - }, - } - } - - const res = await ctx.appviewAgent.api.com.atproto.repo.getRecord(params) ->>>>>>> main return { encoding: 'application/json', body: res.data, diff --git a/packages/pds/src/config.ts b/packages/pds/src/config.ts deleted file mode 100644 index f8d7b04d8fa..00000000000 --- a/packages/pds/src/config.ts +++ /dev/null @@ -1,498 +0,0 @@ -import { parseIntWithFallback, DAY, HOUR } from '@atproto/common' - -export interface ServerConfigValues { - debugMode?: boolean - version: string - - publicUrl?: string - scheme: string - port?: number - hostname: string - - dbPostgresUrl?: string - dbPostgresSchema?: string - - blobstoreLocation?: string - blobstoreTmp?: string - - jwtSecret: string - - didPlcUrl: string - didCacheStaleTTL: number - didCacheMaxTTL: number - - serverDid: string - recoveryKey: string - adminPassword: string - moderatorPassword?: string - triagePassword?: string - - inviteRequired: boolean - userInviteInterval: number | null - userInviteEpoch: number - privacyPolicyUrl?: string - termsOfServiceUrl?: string - - databaseLocation?: string - - availableUserDomains: string[] - handleResolveNameservers?: string[] - - rateLimitsEnabled: boolean - rateLimitBypassKey?: string - rateLimitBypassIps?: string[] - redisScratchAddress?: string - redisScratchPassword?: string - - appUrlPasswordReset: string - emailSmtpUrl?: string - emailNoReplyAddress: string - moderationEmailAddress?: string - moderationEmailSmtpUrl?: string - - maxSubscriptionBuffer: number - repoBackfillLimitMs: number - sequencerLeaderLockId?: number - sequencerLeaderEnabled?: boolean - - // this is really only used in test environments - dbTxLockNonce?: string - - bskyAppViewEndpoint: string - bskyAppViewDid: string - bskyAppViewModeration?: boolean - bskyAppViewCdnUrlPattern?: string - - crawlersToNotify?: string[] -} - -export class ServerConfig { - constructor(private cfg: ServerConfigValues) { - const invalidDomain = cfg.availableUserDomains.find( - (domain) => domain.length < 1 || !domain.startsWith('.'), - ) - if (invalidDomain) { - throw new Error(`Invalid domain: ${invalidDomain}`) - } - } - - static readEnv(overrides?: Partial) { - const debugMode = process.env.DEBUG_MODE === '1' - const version = process.env.PDS_VERSION || '0.0.0' - - const publicUrl = process.env.PUBLIC_URL || undefined - const hostname = process.env.HOSTNAME || 'localhost' - let scheme - if ('TLS' in process.env) { - scheme = process.env.TLS === '1' ? 'https' : 'http' - } else { - scheme = hostname === 'localhost' ? 'http' : 'https' - } - const port = parseIntWithFallback(process.env.PORT, 2583) - - const jwtSecret = process.env.JWT_SECRET || 'jwt_secret' - - const didPlcUrl = process.env.DID_PLC_URL || 'http://localhost:2582' - const didCacheStaleTTL = parseIntWithFallback( - process.env.DID_CACHE_STALE_TTL, - HOUR, - ) - const didCacheMaxTTL = parseIntWithFallback( - process.env.DID_CACHE_MAX_TTL, - DAY, - ) - - const serverDid = overrides?.serverDid || process.env.SERVER_DID - if (typeof serverDid !== 'string') { - throw new Error('No value provided for process.env.SERVER_DID') - } - - const recoveryKey = overrides?.recoveryKey || process.env.RECOVERY_KEY - if (typeof recoveryKey !== 'string') { - throw new Error('No value provided for process.env.RECOVERY_KEY') - } - - const adminPassword = process.env.ADMIN_PASSWORD || 'admin' - const moderatorPassword = process.env.MODERATOR_PASSWORD || undefined - const triagePassword = process.env.TRIAGE_PASSWORD || undefined - - const inviteRequired = process.env.INVITE_REQUIRED === 'true' ? true : false - const userInviteInterval = parseIntWithFallback( - process.env.USER_INVITE_INTERVAL, - null, - ) - const userInviteEpoch = parseIntWithFallback( - process.env.USER_INVITE_EPOCH, - 0, - ) - - const privacyPolicyUrl = process.env.PRIVACY_POLICY_URL - const termsOfServiceUrl = process.env.TERMS_OF_SERVICE_URL - - const databaseLocation = process.env.DATABASE_LOC - - const blobstoreLocation = process.env.BLOBSTORE_LOC - const blobstoreTmp = process.env.BLOBSTORE_TMP - - const availableUserDomains = process.env.AVAILABLE_USER_DOMAINS - ? process.env.AVAILABLE_USER_DOMAINS.split(',') - : [] - - const handleResolveNameservers = process.env.HANDLE_RESOLVE_NAMESERVERS - ? process.env.HANDLE_RESOLVE_NAMESERVERS.split(',') - : [] - - 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, - ) - const redisScratchPassword = nonemptyString( - process.env.REDIS_SCRATCH_PASSWORD, - ) - - const appUrlPasswordReset = - process.env.APP_URL_PASSWORD_RESET || 'app://password-reset' - - const emailSmtpUrl = process.env.EMAIL_SMTP_URL || undefined - - const emailNoReplyAddress = - process.env.EMAIL_NO_REPLY_ADDRESS || 'noreply@blueskyweb.xyz' - - const moderationEmailAddress = - process.env.MODERATION_EMAIL_ADDRESS || undefined - const moderationEmailSmtpUrl = - process.env.MODERATION_EMAIL_SMTP_URL || undefined - - const dbPostgresUrl = process.env.DB_POSTGRES_URL - const dbPostgresSchema = process.env.DB_POSTGRES_SCHEMA - - const maxSubscriptionBuffer = parseIntWithFallback( - process.env.MAX_SUBSCRIPTION_BUFFER, - 500, - ) - - const repoBackfillLimitMs = parseIntWithFallback( - process.env.REPO_BACKFILL_LIMIT_MS, - DAY, - ) - - const sequencerLeaderLockId = parseIntWithFallback( - process.env.SEQUENCER_LEADER_LOCK_ID, - undefined, - ) - - // by default each instance is a potential sequencer leader, but may be configured off - const sequencerLeaderEnabled = process.env.SEQUENCER_LEADER_ENABLED - ? process.env.SEQUENCER_LEADER_ENABLED !== '0' && - process.env.SEQUENCER_LEADER_ENABLED !== 'false' - : undefined - - const dbTxLockNonce = nonemptyString(process.env.DB_TX_LOCK_NONCE) - - 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 bskyAppViewCdnUrlPattern = nonemptyString( - process.env.BSKY_APP_VIEW_CDN_URL_PATTERN, - ) - - const crawlersEnv = process.env.CRAWLERS_TO_NOTIFY - const crawlersToNotify = - crawlersEnv && crawlersEnv.length > 0 ? crawlersEnv.split(',') : [] - - return new ServerConfig({ - debugMode, - version, - publicUrl, - scheme, - hostname, - port, - dbPostgresUrl, - dbPostgresSchema, - blobstoreLocation, - blobstoreTmp, - jwtSecret, - recoveryKey, - didPlcUrl, - didCacheStaleTTL, - didCacheMaxTTL, - serverDid, - adminPassword, - moderatorPassword, - triagePassword, - inviteRequired, - userInviteInterval, - userInviteEpoch, - privacyPolicyUrl, - termsOfServiceUrl, - databaseLocation, - availableUserDomains, - handleResolveNameservers, - rateLimitsEnabled, - rateLimitBypassKey, - rateLimitBypassIps, - redisScratchAddress, - redisScratchPassword, - appUrlPasswordReset, - emailSmtpUrl, - emailNoReplyAddress, - moderationEmailAddress, - moderationEmailSmtpUrl, - maxSubscriptionBuffer, - repoBackfillLimitMs, - sequencerLeaderLockId, - sequencerLeaderEnabled, - dbTxLockNonce, - bskyAppViewEndpoint, - bskyAppViewDid, - bskyAppViewModeration, - bskyAppViewCdnUrlPattern, - crawlersToNotify, - ...overrides, - }) - } - - get debugMode() { - return !!this.cfg.debugMode - } - - get version() { - return this.cfg.version - } - - get scheme() { - return this.cfg.scheme - } - - get port() { - return this.cfg.port - } - - get hostname() { - return this.cfg.hostname - } - - get internalUrl() { - return `${this.scheme}://${this.hostname}:${this.port}` - } - - get origin() { - const u = new URL(this.internalUrl) - return u.origin - } - - get publicUrl() { - return this.cfg.publicUrl || this.internalUrl - } - - get publicHostname() { - const u = new URL(this.publicUrl) - return u.hostname - } - - get dbPostgresUrl() { - return this.cfg.dbPostgresUrl - } - - get dbPostgresSchema() { - return this.cfg.dbPostgresSchema - } - - get blobstoreLocation() { - return this.cfg.blobstoreLocation - } - - get blobstoreTmp() { - return this.cfg.blobstoreTmp - } - - get jwtSecret() { - return this.cfg.jwtSecret - } - - get didPlcUrl() { - return this.cfg.didPlcUrl - } - - get didCacheStaleTTL() { - return this.cfg.didCacheStaleTTL - } - - get didCacheMaxTTL() { - return this.cfg.didCacheMaxTTL - } - - get serverDid() { - return this.cfg.serverDid - } - - get recoveryKey() { - return this.cfg.recoveryKey - } - - get adminPassword() { - return this.cfg.adminPassword - } - - get moderatorPassword() { - return this.cfg.moderatorPassword - } - - get triagePassword() { - return this.cfg.triagePassword - } - - get inviteRequired() { - return this.cfg.inviteRequired - } - - get userInviteInterval() { - return this.cfg.userInviteInterval - } - - get userInviteEpoch() { - return this.cfg.userInviteEpoch - } - - get privacyPolicyUrl() { - if ( - this.cfg.privacyPolicyUrl && - this.cfg.privacyPolicyUrl.startsWith('/') - ) { - return this.publicUrl + this.cfg.privacyPolicyUrl - } - return this.cfg.privacyPolicyUrl - } - - get termsOfServiceUrl() { - if ( - this.cfg.termsOfServiceUrl && - this.cfg.termsOfServiceUrl.startsWith('/') - ) { - return this.publicUrl + this.cfg.termsOfServiceUrl - } - return this.cfg.termsOfServiceUrl - } - - get databaseLocation() { - return this.cfg.databaseLocation - } - - get useMemoryDatabase() { - return !this.databaseLocation - } - - get availableUserDomains() { - return this.cfg.availableUserDomains - } - - get handleResolveNameservers() { - return this.cfg.handleResolveNameservers - } - - get rateLimitsEnabled() { - return this.cfg.rateLimitsEnabled - } - - get rateLimitBypassKey() { - return this.cfg.rateLimitBypassKey - } - - get rateLimitBypassIps() { - return this.cfg.rateLimitBypassIps - } - - get redisScratchAddress() { - return this.cfg.redisScratchAddress - } - - get redisScratchPassword() { - return this.cfg.redisScratchPassword - } - - get appUrlPasswordReset() { - return this.cfg.appUrlPasswordReset - } - - get emailSmtpUrl() { - return this.cfg.emailSmtpUrl - } - - get emailNoReplyAddress() { - return this.cfg.emailNoReplyAddress - } - - get moderationEmailAddress() { - return this.cfg.moderationEmailAddress - } - - get moderationEmailSmtpUrl() { - return this.cfg.moderationEmailSmtpUrl - } - - get maxSubscriptionBuffer() { - return this.cfg.maxSubscriptionBuffer - } - - get repoBackfillLimitMs() { - return this.cfg.repoBackfillLimitMs - } - - get sequencerLeaderLockId() { - return this.cfg.sequencerLeaderLockId - } - - get sequencerLeaderEnabled() { - return this.cfg.sequencerLeaderEnabled !== false - } - - get dbTxLockNonce() { - return this.cfg.dbTxLockNonce - } - - get bskyAppViewEndpoint() { - return this.cfg.bskyAppViewEndpoint - } - - get bskyAppViewDid() { - return this.cfg.bskyAppViewDid - } - - get bskyAppViewModeration() { - return this.cfg.bskyAppViewModeration - } - - get bskyAppViewCdnUrlPattern() { - return this.cfg.bskyAppViewCdnUrlPattern - } - - get crawlersToNotify() { - return this.cfg.crawlersToNotify - } -} - -const nonemptyString = (str: string | undefined): string | undefined => { - if (str === undefined || str.length === 0) return undefined - return str -} diff --git a/packages/pds/src/config/config.ts b/packages/pds/src/config/config.ts index 512f1de41a9..de66d9f8028 100644 --- a/packages/pds/src/config/config.ts +++ b/packages/pds/src/config/config.ts @@ -156,6 +156,9 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { enabled: true, mode: redisCfg !== null ? 'redis' : 'memory', bypassKey: env.rateLimitBypassKey, + bypassIps: env.rateLimitBypassIps?.map((ipOrCidr) => + ipOrCidr.split('/')[0]?.trim(), + ), } : { enabled: false } @@ -273,6 +276,7 @@ export type RateLimitsConfig = enabled: true mode: 'memory' | 'redis' bypassKey?: string + bypassIps?: string[] } | { enabled: false } diff --git a/packages/pds/src/config/env.ts b/packages/pds/src/config/env.ts index f8470369a8b..efb2fee43d0 100644 --- a/packages/pds/src/config/env.ts +++ b/packages/pds/src/config/env.ts @@ -68,6 +68,7 @@ export const readEnv = (): ServerEnvironment => { // rate limits rateLimitsEnabled: envBool(process.env.PDS_RATE_LIMITS_ENABLED), rateLimitBypassKey: envStr(process.env.PDS_RATE_LIMIT_BYPASS_KEY), + rateLimitBypassIps: envList(process.env.PDS_RATE_LIMIT_BYPASS_IPS), // redis redisScratchAddress: envStr(process.env.PDS_REDIS_SCRATCH_ADDRESS), @@ -155,6 +156,7 @@ export type ServerEnvironment = { // rate limits rateLimitsEnabled?: boolean rateLimitBypassKey?: string + rateLimitBypassIps?: string[] // redis redisScratchAddress?: string diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index c5930851a94..466bc9a5613 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -1,7 +1,4 @@ -<<<<<<< HEAD import * as nodemailer from 'nodemailer' -======= ->>>>>>> main import { Redis } from 'ioredis' import * as plc from '@did-plc/lib' import * as crypto from '@atproto/crypto' @@ -16,20 +13,13 @@ import { ServerAuth } from './auth' import { ServerMailer } from './mailer' import { ModerationMailer } from './mailer/moderation' import { BlobStore } from '@atproto/repo' -<<<<<<< HEAD import { Services, createServices } from './services' -======= -import { Services } from './services' ->>>>>>> main import { Sequencer, SequencerLeader } from './sequencer' import { BackgroundQueue } from './background' import DidSqlCache from './did-cache' import { Crawlers } from './crawlers' -<<<<<<< HEAD import { DiskBlobStore } from './storage' import { getRedisClient } from './redis' -======= ->>>>>>> main import { RuntimeFlags } from './runtime-flags' export type AppContextOptions = { @@ -55,7 +45,6 @@ export type AppContextOptions = { } export class AppContext { -<<<<<<< HEAD public db: Database public blobstore: BlobStore public mailer: ServerMailer @@ -146,34 +135,6 @@ export class AppContext { timeout: cfg.identity.resolverTimeout, }) const plcClient = new plc.Client(cfg.identity.plcUrl) -======= - constructor( - private opts: { - db: Database - blobstore: BlobStore - redisScratch?: Redis - repoSigningKey: crypto.Keypair - plcRotationKey: crypto.Keypair - idResolver: IdResolver - didCache: DidSqlCache - auth: auth.ServerAuth - cfg: ServerConfig - mailer: ServerMailer - moderationMailer: ModerationMailer - services: Services - sequencer: Sequencer - sequencerLeader: SequencerLeader | null - runtimeFlags: RuntimeFlags - backgroundQueue: BackgroundQueue - appviewAgent: AtpAgent - crawlers: Crawlers - }, - ) {} - - get db(): Database { - return this.opts.db - } ->>>>>>> main const sequencer = new Sequencer(db) const sequencerLeader = cfg.subscription.sequencerLeaderEnabled @@ -278,61 +239,6 @@ export class AppContext { return auth.optionalAccessOrRoleVerifier(this.auth) } -<<<<<<< HEAD -======= - get cfg(): ServerConfig { - return this.opts.cfg - } - - get mailer(): ServerMailer { - return this.opts.mailer - } - - get moderationMailer(): ModerationMailer { - return this.opts.moderationMailer - } - - get services(): Services { - return this.opts.services - } - - get sequencer(): Sequencer { - return this.opts.sequencer - } - - get sequencerLeader(): SequencerLeader | null { - return this.opts.sequencerLeader - } - - get runtimeFlags(): RuntimeFlags { - return this.opts.runtimeFlags - } - - get backgroundQueue(): BackgroundQueue { - return this.opts.backgroundQueue - } - - get crawlers(): Crawlers { - return this.opts.crawlers - } - - get plcClient(): plc.Client { - return new plc.Client(this.cfg.didPlcUrl) - } - - get idResolver(): IdResolver { - return this.opts.idResolver - } - - get didCache(): DidSqlCache { - return this.opts.didCache - } - - get appviewAgent(): AtpAgent { - return this.opts.appviewAgent - } - ->>>>>>> main async serviceAuthHeaders(did: string, audience?: string) { const aud = audience ?? this.cfg.bskyAppView.did if (!aud) { @@ -344,23 +250,6 @@ export class AppContext { keypair: this.repoSigningKey, }) } -<<<<<<< HEAD -======= - - shouldProxyModeration(): boolean { - return ( - this.cfg.bskyAppViewEndpoint !== undefined && - this.cfg.bskyAppViewModeration === true - ) - } - - canProxyWrite(): boolean { - return ( - this.cfg.bskyAppViewEndpoint !== undefined && - this.cfg.bskyAppViewDid !== undefined - ) - } ->>>>>>> main } export default AppContext diff --git a/packages/pds/src/db/database-schema.ts b/packages/pds/src/db/database-schema.ts index 26f645cd746..655408dad8c 100644 --- a/packages/pds/src/db/database-schema.ts +++ b/packages/pds/src/db/database-schema.ts @@ -14,22 +14,12 @@ import * as blob from './tables/blob' import * as repoBlob from './tables/repo-blob' import * as deleteAccountToken from './tables/delete-account-token' import * as moderation from './tables/moderation' -<<<<<<< HEAD -======= -import * as mute from './tables/mute' -import * as listMute from './tables/list-mute' ->>>>>>> main import * as repoSeq from './tables/repo-seq' import * as appMigration from './tables/app-migration' import * as runtimeFlag from './tables/runtime-flag' -<<<<<<< HEAD export type DatabaseSchemaType = appMigration.PartialDB & runtimeFlag.PartialDB & -======= -export type DatabaseSchemaType = runtimeFlag.PartialDB & - appMigration.PartialDB & ->>>>>>> main userAccount.PartialDB & userPref.PartialDB & didHandle.PartialDB & @@ -45,11 +35,6 @@ export type DatabaseSchemaType = runtimeFlag.PartialDB & repoBlob.PartialDB & deleteAccountToken.PartialDB & moderation.PartialDB & -<<<<<<< HEAD -======= - mute.PartialDB & - listMute.PartialDB & ->>>>>>> main repoSeq.PartialDB export type DatabaseSchema = Kysely diff --git a/packages/pds/src/db/migrations/index.ts b/packages/pds/src/db/migrations/index.ts index 63f9d67e4ab..8ebd59d6ef3 100644 --- a/packages/pds/src/db/migrations/index.ts +++ b/packages/pds/src/db/migrations/index.ts @@ -2,72 +2,5 @@ // It's important that every migration is exported from here with the proper name. We'd simplify // this with kysely's FileMigrationProvider, but it doesn't play nicely with the build process. -<<<<<<< HEAD export * as _20230613T164932261Z from './20230613T164932261Z-init' export * as _20230914T014727199Z from './20230914T014727199Z-repo-v3' -======= -export * as _20221021T162202001Z from './20221021T162202001Z-init' -export * as _20221116T234458063Z from './20221116T234458063Z-duplicate-records' -export * as _20221202T212459280Z from './20221202T212459280Z-blobs' -export * as _20221209T210026294Z from './20221209T210026294Z-banners' -export * as _20221212T195416407Z from './20221212T195416407Z-post-media' -export * as _20221215T220356370Z from './20221215T220356370Z-password-reset-otp' -export * as _20221226T213635517Z from './20221226T213635517Z-mute-init' -export * as _20221230T215012029Z from './20221230T215012029Z-moderation-init' -export * as _20230127T215753149Z from './20230127T215753149Z-indexed-at-on-record' -export * as _20230127T224743452Z from './20230127T224743452Z-repo-sync-data-pt1' -export * as _20230201T200606704Z from './20230201T200606704Z-repo-sync-data-pt2' -export * as _20230202T170426672Z from './20230202T170426672Z-user-partitioned-cids' -export * as _20230202T170435937Z from './20230202T170435937Z-delete-account-token' -export * as _20230202T172831900Z from './20230202T172831900Z-moderation-subject-blob' -export * as _20230202T213952826Z from './20230202T213952826Z-repo-seq' -export * as _20230208T081544325Z from './20230208T081544325Z-post-hydrate-indices' -export * as _20230208T222001557Z from './20230208T222001557Z-user-table-did-pkey' -export * as _20230210T210132396Z from './20230210T210132396Z-post-hierarchy' -export * as _20230214T172233550Z from './20230214T172233550Z-embed-records' -export * as _20230301T222603402Z from './20230301T222603402Z-repo-ops' -export * as _20230304T193548198Z from './20230304T193548198Z-pagination-indices' -export * as _20230308T234640077Z from './20230308T234640077Z-record-indexes' -export * as _20230309T012947663Z from './20230309T012947663Z-app-migration' -export * as _20230310T205728933Z from './20230310T205728933Z-subscription-init' -export * as _20230313T232322844Z from './20230313T232322844Z-blob-creator' -export * as _20230314T023842127Z from './20230314T023842127Z-refresh-grace-period' -export * as _20230323T162732466Z from './20230323T162732466Z-remove-scenes' -export * as _20230328T214311000Z from './20230328T214311000Z-remove-declarations-assertions-confirmations' -export * as _20230328T214311001Z from './20230328T214311001Z-votes-to-likes' -export * as _20230328T214311002Z from './20230328T214311002Z-remove-post-entities' -export * as _20230328T214311003Z from './20230328T214311003Z-backlinks' -export * as _20230328T214311004Z from './20230328T214311004Z-profile-display-name-empty' -export * as _20230328T214311005Z from './20230328T214311005Z-rework-seq' -export * as _20230406T185855842Z from './20230406T185855842Z-feed-item-init' -export * as _20230411T175730759Z from './20230411T175730759Z-drop-message-queue' -export * as _20230411T180247652Z from './20230411T180247652Z-labels' -export * as _20230412T231807162Z from './20230412T231807162Z-moderation-action-labels' -export * as _20230416T221236745Z from './20230416T221236745Z-app-specific-passwords' -export * as _20230420T143821201Z from './20230420T143821201Z-post-profile-aggs' -export * as _20230427T194652255Z from './20230427T194652255Z-notif-record-index' -export * as _20230428T195614638Z from './20230428T195614638Z-actor-block-init' -export * as _20230508T193807762Z from './20230508T193807762Z-acct-deletion-indexes' -export * as _20230508T232711152Z from './20230508T232711152Z-disable-account-invites' -export * as _20230509T192324175Z from './20230509T192324175Z-seq-invalidated' -export * as _20230511T154721392Z from './20230511T154721392Z-mute-lists' -export * as _20230511T171739449Z from './20230511T171739449Z-actor-preferences' -export * as _20230511T200212974Z from './20230511T200212974Z-feed-generators' -export * as _20230523T183902064Z from './20230523T183902064Z-algo-whats-hot-view' -export * as _20230529T222706121Z from './20230529T222706121Z-suggested-follows' -export * as _20230530T213530067Z from './20230530T213530067Z-rebase-indices' -export * as _20230605T235529700Z from './20230605T235529700Z-outgoing-repo-seq' -export * as _20230703T044601833Z from './20230703T044601833Z-feed-and-label-indices' -export * as _20230718T170914772Z from './20230718T170914772Z-sequencer-leader-sequence' -export * as _20230727T172043676Z from './20230727T172043676Z-user-account-cursor-idx' -export * as _20230801T141349990Z from './20230801T141349990Z-invite-note' -export * as _20230801T195109532Z from './20230801T195109532Z-remove-moderation-fkeys' -export * as _20230807T035309811Z from './20230807T035309811Z-feed-item-delete-invite-for-user-idx' -export * as _20230808T172813122Z from './20230808T172813122Z-repo-rev' -export * as _20230810T203412859Z from './20230810T203412859Z-action-duration' -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' ->>>>>>> main diff --git a/packages/pds/src/image/index.ts b/packages/pds/src/image/index.ts index 5a825ac84bd..4f70321f0ed 100644 --- a/packages/pds/src/image/index.ts +++ b/packages/pds/src/image/index.ts @@ -46,33 +46,13 @@ export async function getInfo(stream: Readable): Promise { return maybeInfo } -<<<<<<< HEAD export type Dimensions = { height: number; width: number } -======= -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 -} ->>>>>>> main export type ImageInfo = Dimensions & { size: number mime: `image/${string}` | 'unknown' } -<<<<<<< HEAD -======= -export type Dimensions = { height: number; width: number } - ->>>>>>> main export const formatsToMimes: { [s in keyof sharp.FormatEnum]?: `image/${string}` } = { diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index b80ef4f6df5..42544eba492 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -8,17 +8,13 @@ import express from 'express' import cors from 'cors' import http from 'http' import events from 'events' -import { MINUTE } from '@atproto/common' import { RateLimiter, RateLimiterCreator, RateLimiterOpts, Options as XrpcServerOptions, } from '@atproto/xrpc-server' -<<<<<<< HEAD -======= import { DAY, HOUR, MINUTE } from '@atproto/common' ->>>>>>> main import API from './api' import * as basicRoutes from './basic-routes' import * as wellKnown from './well-known' @@ -26,33 +22,15 @@ import * as error from './error' import { dbLogger, loggerMiddleware, seqLogger } from './logger' import { ServerConfig, ServerSecrets } from './config' import { createServer } from './lexicon' -<<<<<<< HEAD import { createHttpTerminator, HttpTerminator } from 'http-terminator' import AppContext, { AppContextOptions } from './context' import compression from './util/compression' export * from './config' -======= -import { createServices } from './services' -import { createHttpTerminator, HttpTerminator } from 'http-terminator' -import AppContext from './context' -import { Sequencer, SequencerLeader } from './sequencer' -import { BackgroundQueue } from './background' -import DidSqlCache from './did-cache' -import { Crawlers } from './crawlers' -import { getRedisClient } from './redis' -import { RuntimeFlags } from './runtime-flags' - -export type { ServerConfigValues } from './config' -export { ServerConfig } from './config' ->>>>>>> main export { Database } from './db' export { DiskBlobStore, MemoryBlobStore } from './storage' export { AppContext } from './context' -<<<<<<< HEAD export { httpLogger } from './logger' -======= ->>>>>>> main export class PDS { public ctx: AppContext @@ -67,119 +45,18 @@ export class PDS { this.app = opts.app } -<<<<<<< HEAD static async create( cfg: ServerConfig, secrets: ServerSecrets, overrides?: Partial, ): Promise { -======= - static create(opts: { - db: Database - blobstore: BlobStore - repoSigningKey: crypto.Keypair - plcRotationKey: crypto.Keypair - config: ServerConfig - }): PDS { - const { db, blobstore, repoSigningKey, plcRotationKey, config } = opts - const auth = new ServerAuth({ - jwtSecret: config.jwtSecret, - adminPass: config.adminPassword, - moderatorPass: config.moderatorPassword, - triagePass: config.triagePassword, - }) - - const didCache = new DidSqlCache( - db, - config.didCacheStaleTTL, - config.didCacheMaxTTL, - ) - const idResolver = new IdResolver({ - plcUrl: config.didPlcUrl, - didCache, - backupNameservers: config.handleResolveNameservers, - }) - - const sequencer = new Sequencer(db) - const sequencerLeader = config.sequencerLeaderEnabled - ? new SequencerLeader(db, config.sequencerLeaderLockId) - : null - - const serverMailTransport = - config.emailSmtpUrl !== undefined - ? createTransport(config.emailSmtpUrl) - : createTransport({ jsonTransport: true }) - - const moderationMailTransport = - config.moderationEmailSmtpUrl !== undefined - ? createTransport(config.moderationEmailSmtpUrl) - : createTransport({ jsonTransport: true }) - - const mailer = new ServerMailer(serverMailTransport, config) - const moderationMailer = new ModerationMailer( - moderationMailTransport, - config, - ) - ->>>>>>> main const app = express() app.set('trust proxy', true) app.use(cors()) app.use(loggerMiddleware) app.use(compression()) -<<<<<<< HEAD const ctx = await AppContext.fromConfig(cfg, secrets, overrides) -======= - const backgroundQueue = new BackgroundQueue(db) - const crawlers = new Crawlers( - config.hostname, - config.crawlersToNotify ?? [], - ) - - const appviewAgent = new AtpAgent({ service: config.bskyAppViewEndpoint }) - - const services = createServices({ - repoSigningKey, - blobstore, - appviewAgent, - appviewDid: config.bskyAppViewDid, - appviewCdnUrlPattern: config.bskyAppViewCdnUrlPattern, - backgroundQueue, - crawlers, - }) - - const runtimeFlags = new RuntimeFlags(db) - - let redisScratch: Redis | undefined = undefined - if (config.redisScratchAddress) { - redisScratch = getRedisClient( - config.redisScratchAddress, - config.redisScratchPassword, - ) - } - - const ctx = new AppContext({ - db, - blobstore, - redisScratch, - repoSigningKey, - plcRotationKey, - idResolver, - didCache, - cfg: config, - auth, - sequencer, - sequencerLeader, - runtimeFlags, - services, - mailer, - moderationMailer, - backgroundQueue, - appviewAgent, - crawlers, - }) ->>>>>>> main const xrpcOpts: XrpcServerOptions = { validateResponse: false, @@ -190,31 +67,24 @@ export class PDS { }, } if (cfg.rateLimits.enabled) { + const bypassSecret = cfg.rateLimits.bypassKey + const bypassIps = cfg.rateLimits.bypassIps let rlCreator: RateLimiterCreator if (cfg.rateLimits.mode === 'redis') { if (!ctx.redisScratch) { throw new Error('Redis not set up for ratelimiting mode: `redis`') } rlCreator = (opts: RateLimiterOpts) => -<<<<<<< HEAD RateLimiter.redis(ctx.redisScratch, { - // bypassSecret: cfg.rateLimits., -======= - RateLimiter.redis(redisScratch, { - bypassSecret: config.rateLimitBypassKey, - bypassIps: config.rateLimitBypassIps, ->>>>>>> main + bypassSecret, + bypassIps, ...opts, }) } else { rlCreator = (opts: RateLimiterOpts) => RateLimiter.memory({ -<<<<<<< HEAD - // bypassSecret: config.rateLimitBypassKey, -======= - bypassSecret: config.rateLimitBypassKey, - bypassIps: config.rateLimitBypassIps, ->>>>>>> main + bypassSecret, + bypassIps, ...opts, }) } diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts index da6da9c5214..06c0fd4bf9e 100644 --- a/packages/pds/src/services/account/index.ts +++ b/packages/pds/src/services/account/index.ts @@ -1,9 +1,6 @@ import { sql } from 'kysely' -<<<<<<< HEAD import { randomStr } from '@atproto/crypto' import { InvalidRequestError } from '@atproto/xrpc-server' -======= ->>>>>>> main import { dbLogger as log } from '../../logger' import Database from '../../db' import * as scrypt from '../../db/scrypt' @@ -11,18 +8,10 @@ import { UserAccountEntry } from '../../db/tables/user-account' import { DidHandle } from '../../db/tables/did-handle' import { RepoRoot } from '../../db/tables/repo-root' import { countAll, notSoftDeletedClause } from '../../db/util' -<<<<<<< HEAD import { paginate, TimeCidKeyset } from '../../db/pagination' import * as sequencer from '../../sequencer' import { AppPassword } from '../../lexicon/types/com/atproto/server/createAppPassword' -======= 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' ->>>>>>> main export class AccountService { constructor(public db: Database) {} @@ -285,120 +274,12 @@ export class AccountService { .execute() } -<<<<<<< HEAD -======= - async mute(info: { did: string; mutedByDid: string; createdAt?: Date }) { - const { did, mutedByDid, createdAt = new Date() } = info - await this.db.db - .insertInto('mute') - .values({ - did, - mutedByDid, - createdAt: createdAt.toISOString(), - }) - .onConflict((oc) => oc.doNothing()) - .execute() - } - - async unmute(info: { did: string; mutedByDid: string }) { - const { did, mutedByDid } = info - await this.db.db - .deleteFrom('mute') - .where('did', '=', did) - .where('mutedByDid', '=', mutedByDid) - .execute() - } - - async getMute(mutedBy: string, did: string): Promise { - const mutes = await this.getMutes(mutedBy, [did]) - return mutes[did] ?? false - } - - async getMutes( - mutedBy: string, - dids: string[], - ): Promise> { - if (dids.length === 0) return {} - const res = await this.db.db - .selectFrom('mute') - .where('mutedByDid', '=', mutedBy) - .where('did', 'in', dids) - .selectAll() - .execute() - return res.reduce((acc, cur) => { - acc[cur.did] = true - return acc - }, {} as Record) - } - - async muteActorList(info: { - list: string - mutedByDid: string - createdAt?: Date - }) { - const { list, mutedByDid, createdAt = new Date() } = info - await this.db.db - .insertInto('list_mute') - .values({ - listUri: list, - mutedByDid, - createdAt: createdAt.toISOString(), - }) - .onConflict((oc) => oc.doNothing()) - .execute() - } - - async unmuteActorList(info: { list: string; mutedByDid: string }) { - const { list, mutedByDid } = info - await this.db.db - .deleteFrom('list_mute') - .where('listUri', '=', list) - .where('mutedByDid', '=', mutedByDid) - .execute() - } - ->>>>>>> main async search(opts: { term: string limit: number cursor?: string includeSoftDeleted?: boolean }): Promise<(RepoRoot & DidHandle)[]> { - const { term, limit, cursor, includeSoftDeleted } = opts - const { ref } = this.db.db.dynamic - -<<<<<<< HEAD - const builder = this.db.db - .selectFrom('did_handle') - .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') - .innerJoin('user_account', 'user_account.did', 'did_handle.did') - .if(!includeSoftDeleted, (qb) => - qb.where(notSoftDeletedClause(ref('repo_root'))), - ) - .where((qb) => { - // sqlite doesn't support "ilike", but performs "like" case-insensitively - const likeOp = this.db.dialect === 'pg' ? 'ilike' : 'like' - if (term.includes('@')) { - return qb.where('user_account.email', likeOp, `%${term}%`) - } - if (term.startsWith('did:')) { - return qb.where('did_handle.did', '=', term) - } - return qb.where('did_handle.handle', likeOp, `${term}%`) - }) - .selectAll(['did_handle', 'repo_root']) - - const keyset = new ListKeyset( - ref('repo_root.indexedAt'), - ref('did_handle.handle'), - ) - - return await paginate(builder, { - limit, - cursor, - keyset, - }).execute() -======= const builder = this.db.dialect === 'pg' ? getUserSearchQueryPg(this.db, opts) @@ -410,7 +291,6 @@ export class AccountService { .select(sql`0`.as('distance')) return await builder.execute() ->>>>>>> main } async list(opts: { diff --git a/packages/pds/src/services/index.ts b/packages/pds/src/services/index.ts index 2f7b1fed63b..954a5544e6e 100644 --- a/packages/pds/src/services/index.ts +++ b/packages/pds/src/services/index.ts @@ -14,32 +14,20 @@ import { LocalService } from './local' export function createServices(resources: { repoSigningKey: crypto.Keypair blobstore: BlobStore -<<<<<<< HEAD pdsHostname: string appViewAgent?: AtpAgent appViewDid?: string appViewCdnUrlPattern?: string -======= - appviewAgent?: AtpAgent - appviewDid?: string - appviewCdnUrlPattern?: string ->>>>>>> main backgroundQueue: BackgroundQueue crawlers: Crawlers }): Services { const { repoSigningKey, blobstore, -<<<<<<< HEAD pdsHostname, appViewAgent, appViewDid, appViewCdnUrlPattern, -======= - appviewAgent, - appviewDid, - appviewCdnUrlPattern, ->>>>>>> main backgroundQueue, crawlers, } = resources diff --git a/packages/pds/src/services/local/index.ts b/packages/pds/src/services/local/index.ts index 7af82ee4173..c5cc782357f 100644 --- a/packages/pds/src/services/local/index.ts +++ b/packages/pds/src/services/local/index.ts @@ -40,7 +40,7 @@ export class LocalService { public db: Database, public signingKey: Keypair, public pdsHostname: string, - public appviewAgent?: AtpAgent, + public appViewAgent?: AtpAgent, public appviewDid?: string, public appviewCdnUrlPattern?: string, ) {} @@ -48,7 +48,7 @@ export class LocalService { static creator( signingKey: Keypair, pdsHostname: string, - appviewAgent?: AtpAgent, + appViewAgent?: AtpAgent, appviewDid?: string, appviewCdnUrlPattern?: string, ) { @@ -57,7 +57,7 @@ export class LocalService { db, signingKey, pdsHostname, - appviewAgent, + appViewAgent, appviewDid, appviewCdnUrlPattern, ) @@ -258,12 +258,12 @@ export class LocalService { } async formatRecordEmbedInternal(did: string, embed: EmbedRecord) { - if (!this.appviewAgent || !this.appviewDid) { + if (!this.appViewAgent || !this.appviewDid) { return null } const collection = new AtUri(embed.record.uri).collection if (collection === ids.AppBskyFeedPost) { - const res = await this.appviewAgent.api.app.bsky.feed.getPosts( + const res = await this.appViewAgent.api.app.bsky.feed.getPosts( { uris: [embed.record.uri], }, @@ -282,7 +282,7 @@ export class LocalService { indexedAt: post.indexedAt, } } else if (collection === ids.AppBskyFeedGenerator) { - const res = await this.appviewAgent.api.app.bsky.feed.getFeedGenerator( + const res = await this.appViewAgent.api.app.bsky.feed.getFeedGenerator( { feed: embed.record.uri, }, @@ -293,7 +293,7 @@ export class LocalService { ...res.data.view, } } else if (collection === ids.AppBskyGraphList) { - const res = await this.appviewAgent.api.app.bsky.graph.getList( + const res = await this.appViewAgent.api.app.bsky.graph.getList( { list: embed.record.uri, }, diff --git a/packages/pds/src/services/record/index.ts b/packages/pds/src/services/record/index.ts index 748230bdcf4..5ddc9b79a09 100644 --- a/packages/pds/src/services/record/index.ts +++ b/packages/pds/src/services/record/index.ts @@ -70,22 +70,14 @@ export class RecordService { async deleteRecord(uri: AtUri) { this.db.assertTransaction() log.debug({ uri }, 'deleting indexed record') - await this.db.db - .deleteFrom('backlink') - .where('uri', '=', uri.toString()) -<<<<<<< HEAD - .execute() - await this.db.db + const deleteQuery = this.db.db .deleteFrom('record') .where('uri', '=', uri.toString()) - .execute() -======= const backlinkQuery = this.db.db .deleteFrom('backlink') .where('uri', '=', uri.toString()) await Promise.all([deleteQuery.execute(), backlinkQuery.execute()]) ->>>>>>> main log.info({ uri }, 'deleted indexed record') } diff --git a/packages/pds/tests/_util.ts b/packages/pds/tests/_util.ts index 5b6da75ee2c..69ea84d4826 100644 --- a/packages/pds/tests/_util.ts +++ b/packages/pds/tests/_util.ts @@ -13,10 +13,7 @@ import { PDS, Database } from '../src' import { FeedViewPost } from '../src/lexicon/types/app/bsky/feed/defs' import AppContext from '../src/context' import { lexToJson } from '@atproto/lexicon' -<<<<<<< HEAD import { ServerEnvironment, envToCfg, envToSecrets } from '../src/config' -======= ->>>>>>> main const ADMIN_PASSWORD = 'admin-pass' const MODERATOR_PASSWORD = 'moderator-pass' @@ -84,26 +81,9 @@ export const runTestServer = async ( adminPassword: ADMIN_PASSWORD, moderatorPassword: MODERATOR_PASSWORD, jwtSecret: 'jwt-secret', -<<<<<<< HEAD inviteRequired: false, inviteEpoch: Date.now(), triagePassword: TRIAGE_PASSWORD, -======= - availableUserDomains: ['.test'], - rateLimitsEnabled: false, - appUrlPasswordReset: 'app://forgot-password', - emailNoReplyAddress: 'noreply@blueskyweb.xyz', - publicUrl: 'https://pds.public.url', - dbPostgresUrl: process.env.DB_POSTGRES_URL, - blobstoreLocation: `${blobstoreLoc}/blobs`, - blobstoreTmp: `${blobstoreLoc}/tmp`, - maxSubscriptionBuffer: 200, - repoBackfillLimitMs: HOUR, - sequencerLeaderLockId: uniqueLockId(), - bskyAppViewEndpoint: 'http://fake_address.invalid', - bskyAppViewDid: 'did:example:fake', - dbTxLockNonce: await randomStr(32, 'base32'), ->>>>>>> main ...params, } @@ -130,21 +110,6 @@ export const runTestServer = async ( await migrationDb.close() } -<<<<<<< HEAD -======= - const blobstore = - cfg.blobstoreLocation !== undefined - ? await DiskBlobStore.create(cfg.blobstoreLocation, cfg.blobstoreTmp) - : new MemoryBlobStore() - - const pds = PDS.create({ - db, - blobstore, - repoSigningKey, - plcRotationKey, - config: cfg, - }) ->>>>>>> main const pdsServer = await pds.start() const pdsPort = (pdsServer.address() as AddressInfo).port From 1351560820fac07e2bc34ff3fe75b5a664217122 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 26 Sep 2023 18:46:36 -0500 Subject: [PATCH 079/105] building dev-env --- packages/api/tests/bsky-agent.test.ts | 25 --------- packages/dev-env/src/bin-network.ts | 77 --------------------------- packages/dev-env/src/bin.ts | 4 -- packages/dev-env/src/mock/index.ts | 28 ---------- packages/dev-env/src/pds.ts | 36 ------------- packages/dev-env/src/types.ts | 4 -- 6 files changed, 174 deletions(-) delete mode 100644 packages/dev-env/src/bin-network.ts diff --git a/packages/api/tests/bsky-agent.test.ts b/packages/api/tests/bsky-agent.test.ts index 53dc777e61b..8066bd61f3a 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/bsky-agent.test.ts @@ -42,12 +42,7 @@ describe('agent', () => { email: 'user1@test.com', password: 'password', }) -<<<<<<< HEAD - - const displayName1 = await await getProfileDisplayName(agent) -======= const displayName1 = await getProfileDisplayName(agent) ->>>>>>> main expect(displayName1).toBeFalsy() await agent.upsertProfile((existing) => { @@ -57,11 +52,7 @@ describe('agent', () => { } }) -<<<<<<< HEAD - const displayName2 = await await getProfileDisplayName(agent) -======= const displayName2 = await getProfileDisplayName(agent) ->>>>>>> main expect(displayName2).toBe('Bob') await agent.upsertProfile((existing) => { @@ -71,11 +62,7 @@ describe('agent', () => { } }) -<<<<<<< HEAD - const displayName3 = await await getProfileDisplayName(agent) -======= const displayName3 = await getProfileDisplayName(agent) ->>>>>>> main expect(displayName3).toBe('BOB') }) @@ -88,11 +75,7 @@ describe('agent', () => { password: 'password', }) -<<<<<<< HEAD - const displayName1 = await await getProfileDisplayName(agent) -======= const displayName1 = await getProfileDisplayName(agent) ->>>>>>> main expect(displayName1).toBeFalsy() let hasConflicted = false @@ -118,11 +101,7 @@ describe('agent', () => { }) expect(ranTwice).toBe(true) -<<<<<<< HEAD - const displayName2 = await await getProfileDisplayName(agent) -======= const displayName2 = await getProfileDisplayName(agent) ->>>>>>> main expect(displayName2).toBe('Bob') }) @@ -135,11 +114,7 @@ describe('agent', () => { password: 'password', }) -<<<<<<< HEAD - const displayName1 = await await getProfileDisplayName(agent) -======= const displayName1 = await getProfileDisplayName(agent) ->>>>>>> main expect(displayName1).toBeFalsy() const p = agent.upsertProfile(async (_existing) => { diff --git a/packages/dev-env/src/bin-network.ts b/packages/dev-env/src/bin-network.ts deleted file mode 100644 index 605c6b680bd..00000000000 --- a/packages/dev-env/src/bin-network.ts +++ /dev/null @@ -1,77 +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, - enableLabelsCache: true, - dbPostgresSchema: 'pds', - }, - bsky: { - dbPostgresSchema: 'bsky', - }, - plc: { port: 2582 }, - }) - await enableProxy(network) - 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() - -// @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/dev-env/src/bin.ts b/packages/dev-env/src/bin.ts index 4e31daa5170..be6084c987a 100644 --- a/packages/dev-env/src/bin.ts +++ b/packages/dev-env/src/bin.ts @@ -15,15 +15,11 @@ const run = async () => { const network = await TestNetwork.create({ pds: { port: 2583, -<<<<<<< HEAD hostname: 'localhost', -======= - publicUrl: 'http://localhost:2583', dbPostgresSchema: 'pds', }, bsky: { dbPostgresSchema: 'bsky', ->>>>>>> main }, plc: { port: 2582 }, }) diff --git a/packages/dev-env/src/mock/index.ts b/packages/dev-env/src/mock/index.ts index 6e33b9123a1..10f76b1c259 100644 --- a/packages/dev-env/src/mock/index.ts +++ b/packages/dev-env/src/mock/index.ts @@ -186,33 +186,6 @@ export async function generateMockSetup(env: TestNetwork) { }, ) -<<<<<<< HEAD - // @TODO get from appview instead - // const ctx = env.pds.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 ctx = env.bsky.ctx if (ctx) { const labelSrvc = ctx.services.label(ctx.db.getPrimary()) @@ -235,7 +208,6 @@ export async function generateMockSetup(env: TestNetwork) { }, ]) } ->>>>>>> main // a set of replies for (let i = 0; i < 100; i++) { diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index 2a1a759241c..46a73e8813b 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -39,7 +39,6 @@ export class TestPds { moderatorPassword: MOD_PASSWORD, triagePassword: TRIAGE_PASSWORD, jwtSecret: 'jwt-secret', -<<<<<<< HEAD serviceHandleDomains: ['.test'], sequencerLeaderLockId: uniqueLockId(), bskyAppViewCdnUrlPattern: 'http://cdn.appview.com/%s/%s/%s', @@ -66,41 +65,6 @@ export class TestPds { if (migrationDb !== server.ctx.db) { await migrationDb.close() } -======= - availableUserDomains: ['.test'], - rateLimitsEnabled: false, - appUrlPasswordReset: 'app://forgot-password', - emailNoReplyAddress: 'noreply@blueskyweb.xyz', - publicUrl: 'https://pds.public.url', - dbPostgresUrl: cfg.dbPostgresUrl, - maxSubscriptionBuffer: 200, - repoBackfillLimitMs: 1000 * 60 * 60, // 1hr - sequencerLeaderLockId: uniqueLockId(), - dbTxLockNonce: await randomStr(32, 'base32'), - bskyAppViewEndpoint: cfg.bskyAppViewEndpoint ?? 'http://fake_address', - bskyAppViewDid: cfg.bskyAppViewDid ?? 'did:example:fake', - bskyAppViewCdnUrlPattern: 'http://cdn.appview.com/%s/%s/%s', - ...cfg, - }) - - const blobstore = new pds.MemoryBlobStore() - const db = config.dbPostgresUrl - ? pds.Database.postgres({ - url: config.dbPostgresUrl, - schema: config.dbPostgresSchema, - txLockNonce: config.dbTxLockNonce, - }) - : pds.Database.memory() - await db.migrateToLatestOrThrow() - - const server = pds.PDS.create({ - db, - blobstore, - repoSigningKey, - plcRotationKey, - config, - }) ->>>>>>> main await server.start() diff --git a/packages/dev-env/src/types.ts b/packages/dev-env/src/types.ts index 1ddb83937a6..3bbcaf15257 100644 --- a/packages/dev-env/src/types.ts +++ b/packages/dev-env/src/types.ts @@ -10,10 +10,6 @@ export type PlcConfig = { export type PdsConfig = Partial & { didPlcUrl: string migration?: string -<<<<<<< HEAD - enableLabelsCache?: boolean -======= ->>>>>>> main } export type BskyConfig = Partial & { From 08fde876b7f06150b74e03ef5664946b785e743e Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 26 Sep 2023 19:13:55 -0500 Subject: [PATCH 080/105] merging tests --- packages/pds/tests/account-deletion.test.ts | 12 ------ packages/pds/tests/admin/moderation.test.ts | 23 ----------- packages/pds/tests/handles.test.ts | 28 -------------- packages/pds/tests/proxied/admin.test.ts | 43 --------------------- packages/pds/tests/server.test.ts | 18 +-------- 5 files changed, 1 insertion(+), 123 deletions(-) diff --git a/packages/pds/tests/account-deletion.test.ts b/packages/pds/tests/account-deletion.test.ts index d4bb6e45133..1d4a285b03d 100644 --- a/packages/pds/tests/account-deletion.test.ts +++ b/packages/pds/tests/account-deletion.test.ts @@ -16,10 +16,6 @@ import { Blob } from '../src/db/tables/blob' 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' -<<<<<<< HEAD -======= -import { UserState } from '../src/db/tables/user-state' ->>>>>>> main describe('account deletion', () => { let server: util.TestServerInfo @@ -228,18 +224,10 @@ type DbContents = { } const getDbContents = async (db: Database): Promise => { -<<<<<<< HEAD const [roots, users, 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(), -======= - 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(), ->>>>>>> main db.db .selectFrom('ipld_block') .orderBy('creator') diff --git a/packages/pds/tests/admin/moderation.test.ts b/packages/pds/tests/admin/moderation.test.ts index 393af913385..47599ef92da 100644 --- a/packages/pds/tests/admin/moderation.test.ts +++ b/packages/pds/tests/admin/moderation.test.ts @@ -918,23 +918,6 @@ describe('moderation', () => { 'Must be a full moderator to perform an account takedown', ) }) -<<<<<<< HEAD:packages/pds/tests/admin/moderation.test.ts -======= - - async function reverse(actionId: number) { - await agent.api.com.atproto.admin.reverseModerationAction( - { - id: actionId, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) - } ->>>>>>> main:packages/pds/tests/moderation.test.ts }) describe('blob takedown', () => { @@ -981,7 +964,6 @@ describe('moderation', () => { await expect(referenceBlob).rejects.toThrow('Could not find blob:') }) -<<<<<<< HEAD:packages/pds/tests/admin/moderation.test.ts it('prevents image blob from being served, even when cached.', async () => { const attempt = agent.api.com.atproto.sync.getBlob({ did: sc.dids.carol, @@ -990,8 +972,6 @@ describe('moderation', () => { await expect(attempt).rejects.toThrow('Blob not found') }) -======= ->>>>>>> main:packages/pds/tests/moderation.test.ts it('restores blob when action is reversed.', async () => { await agent.api.com.atproto.admin.reverseModerationAction( { @@ -1008,7 +988,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() -<<<<<<< HEAD:packages/pds/tests/admin/moderation.test.ts // Can fetch through image server const res = await agent.api.com.atproto.sync.getBlob({ @@ -1017,8 +996,6 @@ describe('moderation', () => { }) expect(res.data.byteLength).toBeGreaterThan(9000) -======= ->>>>>>> main:packages/pds/tests/moderation.test.ts }) }) }) diff --git a/packages/pds/tests/handles.test.ts b/packages/pds/tests/handles.test.ts index dec695d9633..e9b03e3deb9 100644 --- a/packages/pds/tests/handles.test.ts +++ b/packages/pds/tests/handles.test.ts @@ -50,21 +50,13 @@ describe('handles', () => { await close() }) -<<<<<<< HEAD - const getDbHandle = async (did: string): Promise => { -======= const getHandleFromDb = async (did: string): Promise => { ->>>>>>> main const res = await ctx.db.db .selectFrom('did_handle') .selectAll() .where('did', '=', did) .executeTakeFirst() -<<<<<<< HEAD - return res?.handle ?? null -======= return res?.handle ->>>>>>> main } it('resolves handles', async () => { @@ -182,13 +174,8 @@ describe('handles', () => { }, { headers: sc.getHeaders(alice), encoding: 'application/json' }, ) -<<<<<<< HEAD - const handle = await getDbHandle(alice) - expect(handle).toBe('alice.external') -======= const dbHandle = await getHandleFromDb(alice) expect(dbHandle).toBe('alice.external') ->>>>>>> main const data = await idResolver.did.resolveAtprotoData(alice) expect(data.handle).toBe('alice.external') @@ -215,13 +202,8 @@ describe('handles', () => { 'External handle did not resolve to DID', ) -<<<<<<< HEAD - const handle = await getDbHandle(alice) - expect(handle).toBe('alice.external') -======= const dbHandle = await getHandleFromDb(alice) expect(dbHandle).toBe('alice.external') ->>>>>>> main }) it('allows admin overrules of service domains', async () => { @@ -235,14 +217,9 @@ describe('handles', () => { encoding: 'application/json', }, ) -<<<<<<< HEAD - const handle = await getDbHandle(bob) - expect(handle).toBe('bob-alt.test') -======= const dbHandle = await getHandleFromDb(bob) expect(dbHandle).toBe('bob-alt.test') ->>>>>>> main }) it('allows admin override of reserved domains', async () => { @@ -257,13 +234,8 @@ describe('handles', () => { }, ) -<<<<<<< HEAD - const handle = await getDbHandle(bob) - expect(handle).toBe('dril.test') -======= const dbHandle = await getHandleFromDb(bob) expect(dbHandle).toBe('dril.test') ->>>>>>> main }) it('requires admin auth', async () => { diff --git a/packages/pds/tests/proxied/admin.test.ts b/packages/pds/tests/proxied/admin.test.ts index 614a28ad0c2..a2948d2b40e 100644 --- a/packages/pds/tests/proxied/admin.test.ts +++ b/packages/pds/tests/proxied/admin.test.ts @@ -245,10 +245,6 @@ describe('proxies admin requests', () => { }) it('takesdown and labels repos, and reverts.', async () => { -<<<<<<< HEAD -======= - const { db, services } = network.bsky.ctx ->>>>>>> main // takedown repo const { data: action } = await agent.api.com.atproto.admin.takeModerationAction( @@ -278,13 +274,6 @@ describe('proxies admin requests', () => { await expect(tryGetProfileAppview).rejects.toThrow( 'Account has been taken down', ) -<<<<<<< HEAD -======= - const labelsA = await services - .label(db.getPrimary()) - .getLabels(sc.dids.alice, { includeNeg: false, skipCache: true }) - expect(labelsA.map((l) => l.val)).toEqual(['dogs']) ->>>>>>> main // reverse action await agent.api.com.atproto.admin.reverseModerationAction( { id: action.id, createdBy: 'did:example:admin', reason: 'X' }, @@ -303,20 +292,9 @@ describe('proxies admin requests', () => { expect(profileAppview).toEqual( expect.objectContaining({ did: sc.dids.alice, handle: 'alice.test' }), ) -<<<<<<< HEAD }) it('takesdown and labels records, and reverts.', async () => { -======= - 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.bsky.ctx ->>>>>>> main const post = sc.posts[sc.dids.alice][0] // takedown post const { data: action } = @@ -339,25 +317,11 @@ describe('proxies admin requests', () => { }, ) // check thread and labels -<<<<<<< HEAD const tryGetPost = agent.api.app.bsky.feed.getPostThread( { uri: post.ref.uriStr, depth: 0 }, { headers: sc.getHeaders(sc.dids.carol) }, ) await expect(tryGetPost).rejects.toThrow(NotFoundError) -======= - 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(tryGetPostAppview).rejects.toThrow(NotFoundError) - const labelsA = await services - .label(db.getPrimary()) - .getLabels(post.ref.uriStr, { includeNeg: false, skipCache: true }) - expect(labelsA.map((l) => l.val)).toEqual(['dogs']) ->>>>>>> main // reverse action await agent.api.com.atproto.admin.reverseModerationAction( { id: action.id, createdBy: 'did:example:admin', reason: 'X' }, @@ -376,13 +340,6 @@ describe('proxies admin requests', () => { expect(threadAppview.thread.post).toEqual( expect.objectContaining({ uri: post.ref.uriStr, cid: post.ref.cidStr }), ) -<<<<<<< HEAD -======= - const labelsB = await services - .label(db.getPrimary()) - .getLabels(post.ref.uriStr, { includeNeg: false, skipCache: true }) - expect(labelsB.map((l) => l.val)).toEqual(['cats']) ->>>>>>> main }) it('does not persist actions and reports on pds.', async () => { diff --git a/packages/pds/tests/server.test.ts b/packages/pds/tests/server.test.ts index 33381f55631..86714e1bb6d 100644 --- a/packages/pds/tests/server.test.ts +++ b/packages/pds/tests/server.test.ts @@ -1,21 +1,13 @@ import { AddressInfo } from 'net' import express from 'express' import axios, { AxiosError } from 'axios' -<<<<<<< HEAD -import AtpAgent from '@atproto/api' -======= import AtpAgent, { AtUri } from '@atproto/api' -import { CloseFn, runTestServer, TestServerInfo } from './_util' ->>>>>>> main import { handler as errorHandler } from '../src/error' import { SeedClient } from './seeds/client' import basicSeed from './seeds/basic' import { Database } from '../src' -<<<<<<< HEAD import { TestNetwork } from '@atproto/dev-env' -======= import { randomStr } from '@atproto/crypto' ->>>>>>> main describe('server', () => { let network: TestNetwork @@ -115,11 +107,7 @@ describe('server', () => { const uri = new AtUri(createRes.data.uri) const res = await axios.get( -<<<<<<< HEAD - `${network.pds.url}/xrpc/app.bsky.feed.getTimeline`, -======= - `${server.url}/xrpc/com.atproto.repo.getRecord?repo=${uri.host}&collection=${uri.collection}&rkey=${uri.rkey}`, ->>>>>>> main + `${network.pds.url}/xrpc/com.atproto.repo.getRecord?repo=${uri.host}&collection=${uri.collection}&rkey=${uri.rkey}`, { decompress: false, headers: { ...sc.getHeaders(alice), 'accept-encoding': 'gzip' }, @@ -153,11 +141,7 @@ describe('server', () => { it('healthcheck fails when database is unavailable.', async () => { // destroy to release lock & allow db to close -<<<<<<< HEAD await network.pds.ctx.sequencerLeader?.destroy() -======= - await server.ctx.sequencerLeader?.destroy() ->>>>>>> main await db.close() let error: AxiosError From e6ec83b5171611dbfff213a1fe7ef5e5a07f6028 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 26 Sep 2023 19:18:11 -0500 Subject: [PATCH 081/105] merge service entry --- services/pds/index.js | 74 ------------------------------------------- 1 file changed, 74 deletions(-) diff --git a/services/pds/index.js b/services/pds/index.js index f69f0b6f6f7..a99b7ae980e 100644 --- a/services/pds/index.js +++ b/services/pds/index.js @@ -12,7 +12,6 @@ require('dd-trace') // Only works with commonjs // Tracer code above must come before anything else const path = require('path') -<<<<<<< HEAD const { PDS, Database, @@ -20,14 +19,6 @@ const { envToSecrets, readEnv, httpLogger, -======= -const { KmsKeypair, S3BlobStore } = require('@atproto/aws') -const { - Database, - ServerConfig, - PDS, - ViewMaintainer, ->>>>>>> main PeriodicModerationActionReversal, } = require('@atproto/pds') const pkg = require('@atproto/pds/package.json') @@ -49,27 +40,6 @@ const main = async () => { } else { await pds.ctx.db.migrateToLatestOrThrow() } -<<<<<<< HEAD -======= - const cfg = ServerConfig.readEnv({ - port: env.port, - recoveryKey, - emailSmtpUrl: smtpUrl({ - host: env.smtpHost, - username: env.smtpUsername, - password: env.smtpPassword, - }), - }) - const pds = PDS.create({ - db, - blobstore: s3Blobstore, - repoSigningKey, - plcRotationKey, - config: cfg, - }) - const viewMaintainer = new ViewMaintainer(migrateDb) - const viewMaintainerRunning = viewMaintainer.run() ->>>>>>> main // If the PDS is configured to proxy moderation, this will be running on appview instead of pds. // Also don't run this on the sequencer leader, which may not be configured regarding moderation proxying at all. @@ -96,50 +66,6 @@ const main = async () => { }) } -<<<<<<< HEAD -======= -const pgUrl = ({ - username = 'postgres', - password = 'postgres', - host = 'localhost', - port = '5432', - database = 'postgres', - sslmode, -}) => { - const enc = encodeURIComponent - return `postgresql://${username}:${enc( - password, - )}@${host}:${port}/${database}${sslmode ? `?sslmode=${enc(sslmode)}` : ''}` -} - -const smtpUrl = ({ username, password, host }) => { - const enc = encodeURIComponent - return `smtps://${username}:${enc(password)}@${host}` -} - -const maybeParseInt = (str) => { - const parsed = parseInt(str) - return isNaN(parsed) ? undefined : parsed -} - -const getEnv = () => ({ - port: parseInt(process.env.PORT), - plcRotationKeyId: process.env.PLC_ROTATION_KEY_ID, - repoSigningKey: process.env.REPO_SIGNING_KEY, - recoveryKeyId: process.env.RECOVERY_KEY_ID, - dbCreds: JSON.parse(process.env.DB_CREDS_JSON), - dbMigrateCreds: JSON.parse(process.env.DB_MIGRATE_CREDS_JSON), - dbSchema: process.env.DB_SCHEMA || undefined, - dbPoolSize: maybeParseInt(process.env.DB_POOL_SIZE), - dbPoolMaxUses: maybeParseInt(process.env.DB_POOL_MAX_USES), - dbPoolIdleTimeoutMs: maybeParseInt(process.env.DB_POOL_IDLE_TIMEOUT_MS), - smtpHost: process.env.SMTP_HOST, - smtpUsername: process.env.SMTP_USERNAME, - smtpPassword: process.env.SMTP_PASSWORD, - s3Bucket: process.env.S3_BUCKET_NAME, -}) - ->>>>>>> main const maintainXrpcResource = (span, req) => { // Show actual xrpc method as resource rather than the route pattern if (span && req.originalUrl?.startsWith('/xrpc/')) { From 33db3db1066ed8f9114fad7c789f0d35f7024b58 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 26 Sep 2023 19:29:18 -0500 Subject: [PATCH 082/105] test fixing --- packages/pds/src/api/com/atproto/repo/getRecord.ts | 10 +++++++++- .../tests/proxied/__snapshots__/views.test.ts.snap | 12 ++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/pds/src/api/com/atproto/repo/getRecord.ts b/packages/pds/src/api/com/atproto/repo/getRecord.ts index 5362b821b9f..5c99a7226c1 100644 --- a/packages/pds/src/api/com/atproto/repo/getRecord.ts +++ b/packages/pds/src/api/com/atproto/repo/getRecord.ts @@ -14,9 +14,17 @@ export default function (server: Server, ctx: AppContext) { const record = await ctx.services .record(ctx.db) .getRecord(uri, cid || null) - if (!record) { + if (!record || record.takedownId !== null) { throw new InvalidRequestError(`Could not locate record: ${uri}`) } + return { + encoding: 'application/json', + body: { + uri: uri.toString(), + cid: record.cid, + value: record.value, + }, + } } const res = await ctx.appViewAgent.api.com.atproto.repo.getRecord(params) diff --git a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap index 2a2e2bbf886..67c7286359d 100644 --- a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap @@ -870,7 +870,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/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", }, @@ -903,7 +903,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1015,7 +1015,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/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", }, @@ -1048,7 +1048,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1251,7 +1251,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1262,7 +1262,7 @@ Object { }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", From 701fba844d1c3ca7e2f9369471611962a326dd99 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 26 Sep 2023 20:09:36 -0500 Subject: [PATCH 083/105] tidy --- packages/pds/src/config/config.ts | 22 +++--- packages/pds/src/config/env.ts | 108 ++++++++++++++---------------- packages/pds/src/config/util.ts | 12 ++-- services/pds/index.js | 2 +- 4 files changed, 73 insertions(+), 71 deletions(-) diff --git a/packages/pds/src/config/config.ts b/packages/pds/src/config/config.ts index de66d9f8028..7f24c9ebced 100644 --- a/packages/pds/src/config/config.ts +++ b/packages/pds/src/config/config.ts @@ -37,11 +37,11 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { dbCfg = { dialect: 'pg', url: env.dbPostgresUrl, - migrationUrl: env.dbPostgresMigrationUrl || env.dbPostgresUrl, + migrationUrl: env.dbPostgresMigrationUrl ?? env.dbPostgresUrl, schema: env.dbPostgresSchema, pool: { idleTimeoutMs: env.dbPostgresPoolIdleTimeoutMs ?? 10000, - maxUses: env.dbPostgresPoolMaxUses || Infinity, + maxUses: env.dbPostgresPoolMaxUses ?? Infinity, size: env.dbPostgresPoolSize ?? 10, }, } @@ -60,7 +60,7 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { provider: 'disk', location: env.blobstoreDiskLocation, tempLocation: - env.blobstoreDiskTmpLocation || path.join(os.tmpdir(), 'pds/blobs'), + env.blobstoreDiskTmpLocation ?? path.join(os.tmpdir(), 'pds/blobs'), } } else { throw new Error('Must configure either S3 or disk blobstore') @@ -84,10 +84,10 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { } const identityCfg: ServerConfig['identity'] = { - plcUrl: env.didPlcUrl || 'https://plc.bsky-sandbox.dev', - cacheMaxTTL: env.didCacheMaxTTL || DAY, - cacheStaleTTL: env.didCacheStaleTTL || HOUR, - resolverTimeout: env.resolverTimeout || 3 * SECOND, + plcUrl: env.didPlcUrl ?? 'https://plc.bsky-sandbox.dev', + cacheMaxTTL: env.didCacheMaxTTL ?? DAY, + cacheStaleTTL: env.didCacheStaleTTL ?? HOUR, + resolverTimeout: env.resolverTimeout ?? 3 * SECOND, recoveryDidKey: env.recoveryDidKey ?? null, serviceHandleDomains, } @@ -109,7 +109,9 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { emailCfg = null } else { if (!env.emailFromAddress || !env.emailSmtpUrl) { - throw new Error('Partial email config') + throw new Error( + 'Partial email config, must set both emailFromAddress and emailSmtpUrl', + ) } emailCfg = { smtpUrl: env.emailSmtpUrl, @@ -122,7 +124,9 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { moderationEmailCfg = null } else { if (!env.moderationEmailAddress || !env.moderationEmailSmtpUrl) { - throw new Error('Partial email config') + throw new Error( + 'Partial moderation email config, must set both emailFromAddress and emailSmtpUrl', + ) } moderationEmailCfg = { smtpUrl: env.moderationEmailSmtpUrl, diff --git a/packages/pds/src/config/env.ts b/packages/pds/src/config/env.ts index efb2fee43d0..8dde8710e32 100644 --- a/packages/pds/src/config/env.ts +++ b/packages/pds/src/config/env.ts @@ -3,98 +3,92 @@ import { envInt, envStr, envBool, envList } from './util' export const readEnv = (): ServerEnvironment => { return { // service - port: envInt(process.env.PDS_PORT), - hostname: envStr(process.env.PDS_HOSTNAME), - serviceDid: envStr(process.env.PDS_SERVICE_DID), - version: envStr(process.env.PDS_VERSION), - privacyPolicyUrl: envStr(process.env.PDS_PRIVACY_POLICY_URL), - termsOfServiceUrl: envStr(process.env.PDS_TERMS_OF_SERVICE_URL), + port: envInt('PDS_PORT'), + hostname: envStr('PDS_HOSTNAME'), + serviceDid: envStr('PDS_SERVICE_DID'), + version: envStr('PDS_VERSION'), + privacyPolicyUrl: envStr('PDS_PRIVACY_POLICY_URL'), + termsOfServiceUrl: envStr('PDS_TERMS_OF_SERVICE_URL'), // db: one required // sqlite - dbSqliteLocation: envStr(process.env.PDS_DB_SQLITE_LOCATION), + dbSqliteLocation: envStr('PDS_DB_SQLITE_LOCATION'), // postgres - dbPostgresUrl: envStr(process.env.PDS_DB_POSTGRES_URL), - dbPostgresMigrationUrl: envStr(process.env.PDS_DB_POSTGRES_MIGRATION_URL), - dbPostgresSchema: envStr(process.env.PDS_DB_POSTGRES_SCHEMA), - dbPostgresPoolSize: envInt(process.env.PDS_DB_POSTGRES_POOL_SIZE), - dbPostgresPoolMaxUses: envInt(process.env.PDS_DB_POSTGRES_POOL_MAX_USES), - dbPostgresPoolIdleTimeoutMs: envInt( - process.env.PDS_DB_POSTGRES_POOL_IDLE_TIMEOUT_MS, - ), + dbPostgresUrl: envStr('PDS_DB_POSTGRES_URL'), + dbPostgresMigrationUrl: envStr('PDS_DB_POSTGRES_MIGRATION_URL'), + dbPostgresSchema: envStr('PDS_DB_POSTGRES_SCHEMA'), + dbPostgresPoolSize: envInt('PDS_DB_POSTGRES_POOL_SIZE'), + dbPostgresPoolMaxUses: envInt('PDS_DB_POSTGRES_POOL_MAX_USES'), + dbPostgresPoolIdleTimeoutMs: envInt('PDS_DB_POSTGRES_POOL_IDLE_TIMEOUT_MS'), // blobstore: one required // s3 - blobstoreS3Bucket: envStr(process.env.PDS_BLOBSTORE_S3_BUCKET), + blobstoreS3Bucket: envStr('PDS_BLOBSTORE_S3_BUCKET'), // disk - blobstoreDiskLocation: envStr(process.env.PDS_BLOBSTORE_DISK_LOCATION), - blobstoreDiskTmpLocation: envStr( - process.env.PDS_BLOBSTORE_DISK_TMP_LOCATION, - ), + blobstoreDiskLocation: envStr('PDS_BLOBSTORE_DISK_LOCATION'), + blobstoreDiskTmpLocation: envStr('PDS_BLOBSTORE_DISK_TMP_LOCATION'), // identity - didPlcUrl: envStr(process.env.PDS_DID_PLC_URL), - didCacheStaleTTL: envInt(process.env.PDS_DID_CACHE_STALE_TTL), - didCacheMaxTTL: envInt(process.env.PDS_DID_CACHE_MAX_TTL), - resolverTimeout: envInt(process.env.PDS_ID_RESOLVER_TIMEOUT), - recoveryDidKey: envStr(process.env.PDS_RECOVERY_DID_KEY), - serviceHandleDomains: envList(process.env.PDS_SERVICE_HANDLE_DOMAINS), + didPlcUrl: envStr('PDS_DID_PLC_URL'), + didCacheStaleTTL: envInt('PDS_DID_CACHE_STALE_TTL'), + didCacheMaxTTL: envInt('PDS_DID_CACHE_MAX_TTL'), + resolverTimeout: envInt('PDS_ID_RESOLVER_TIMEOUT'), + recoveryDidKey: envStr('PDS_RECOVERY_DID_KEY'), + serviceHandleDomains: envList('PDS_SERVICE_HANDLE_DOMAINS'), // invites - inviteRequired: envBool(process.env.PDS_INVITE_REQUIRED), - inviteInterval: envInt(process.env.PDS_INVITE_INTERVAL), - inviteEpoch: envInt(process.env.PDS_INVITE_EPOCH), + inviteRequired: envBool('PDS_INVITE_REQUIRED'), + inviteInterval: envInt('PDS_INVITE_INTERVAL'), + inviteEpoch: envInt('PDS_INVITE_EPOCH'), // email - emailSmtpUrl: envStr(process.env.PDS_EMAIL_SMTP_URL), - emailFromAddress: envStr(process.env.PDS_EMAIL_FROM_ADDRESS), - moderationEmailSmtpUrl: envStr(process.env.PDS_MODERATION_EMAIL_SMTP_URL), - moderationEmailAddress: envStr(process.env.PDS_MODERATION_EMAIL_ADDRESS), + emailSmtpUrl: envStr('PDS_EMAIL_SMTP_URL'), + emailFromAddress: envStr('PDS_EMAIL_FROM_ADDRESS'), + moderationEmailSmtpUrl: envStr('PDS_MODERATION_EMAIL_SMTP_URL'), + moderationEmailAddress: envStr('PDS_MODERATION_EMAIL_ADDRESS'), // subscription - maxSubscriptionBuffer: envInt(process.env.PDS_MAX_SUBSCRIPTION_BUFFER), - repoBackfillLimitMs: envInt(process.env.PDS_REPO_BACKFILL_LIMIT_MS), - sequencerLeaderEnabled: envBool(process.env.PDS_SEQUENCER_LEADER_ENABLED), - sequencerLeaderLockId: envInt(process.env.PDS_SEQUENCER_LEADER_LOCK_ID), + maxSubscriptionBuffer: envInt('PDS_MAX_SUBSCRIPTION_BUFFER'), + repoBackfillLimitMs: envInt('PDS_REPO_BACKFILL_LIMIT_MS'), + sequencerLeaderEnabled: envBool('PDS_SEQUENCER_LEADER_ENABLED'), + sequencerLeaderLockId: envInt('PDS_SEQUENCER_LEADER_LOCK_ID'), // appview - bskyAppViewUrl: envStr(process.env.PDS_BSKY_APP_VIEW_URL), - bskyAppViewDid: envStr(process.env.PDS_BSKY_APP_VIEW_DID), - bskyAppViewModeration: envBool(process.env.PDS_BSKY_APP_VIEW_MODERATION), - bskyAppViewCdnUrlPattern: envStr( - process.env.PDS_BSKY_APP_VIEW_CDN_URL_PATTERN, - ), + bskyAppViewUrl: envStr('PDS_BSKY_APP_VIEW_URL'), + bskyAppViewDid: envStr('PDS_BSKY_APP_VIEW_DID'), + bskyAppViewModeration: envBool('PDS_BSKY_APP_VIEW_MODERATION'), + bskyAppViewCdnUrlPattern: envStr('PDS_BSKY_APP_VIEW_CDN_URL_PATTERN'), // rate limits - rateLimitsEnabled: envBool(process.env.PDS_RATE_LIMITS_ENABLED), - rateLimitBypassKey: envStr(process.env.PDS_RATE_LIMIT_BYPASS_KEY), - rateLimitBypassIps: envList(process.env.PDS_RATE_LIMIT_BYPASS_IPS), + rateLimitsEnabled: envBool('PDS_RATE_LIMITS_ENABLED'), + rateLimitBypassKey: envStr('PDS_RATE_LIMIT_BYPASS_KEY'), + rateLimitBypassIps: envList('PDS_RATE_LIMIT_BYPASS_IPS'), // redis - redisScratchAddress: envStr(process.env.PDS_REDIS_SCRATCH_ADDRESS), - redisScratchPassword: envStr(process.env.PDS_REDIS_SCRATCH_PASSWORD), + redisScratchAddress: envStr('PDS_REDIS_SCRATCH_ADDRESS'), + redisScratchPassword: envStr('PDS_REDIS_SCRATCH_PASSWORD'), // crawlers - crawlers: envList(process.env.PDS_CRAWLERS), + crawlers: envList('PDS_CRAWLERS'), // secrets - jwtSecret: envStr(process.env.PDS_JWT_SECRET), - adminPassword: envStr(process.env.PDS_ADMIN_PASSWORD), - moderatorPassword: envStr(process.env.PDS_MODERATOR_PASSWORD), - triagePassword: envStr(process.env.PDS_TRIAGE_PASSWORD), + jwtSecret: envStr('PDS_JWT_SECRET'), + adminPassword: envStr('PDS_ADMIN_PASSWORD'), + moderatorPassword: envStr('PDS_MODERATOR_PASSWORD'), + triagePassword: envStr('PDS_TRIAGE_PASSWORD'), // keys: only one of each required // kms - repoSigningKeyKmsKeyId: envStr(process.env.PDS_REPO_SIGNING_KEY_KMS_KEY_ID), + repoSigningKeyKmsKeyId: envStr('PDS_REPO_SIGNING_KEY_KMS_KEY_ID'), // memory repoSigningKeyK256PrivateKeyHex: envStr( - process.env.PDS_REPO_SIGNING_KEY_K256_PRIVATE_KEY_HEX, + 'PDS_REPO_SIGNING_KEY_K256_PRIVATE_KEY_HEX', ), // kms - plcRotationKeyKmsKeyId: envStr(process.env.PDS_PLC_ROTATION_KEY_KMS_KEY_ID), + plcRotationKeyKmsKeyId: envStr('PDS_PLC_ROTATION_KEY_KMS_KEY_ID'), // memory plcRotationKeyK256PrivateKeyHex: envStr( - process.env.PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX, + 'PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX', ), } } diff --git a/packages/pds/src/config/util.ts b/packages/pds/src/config/util.ts index 653e84bcf3b..2bf858621bf 100644 --- a/packages/pds/src/config/util.ts +++ b/packages/pds/src/config/util.ts @@ -1,21 +1,25 @@ import { parseIntWithFallback } from '@atproto/common' -export const envInt = (str: string | undefined): number | undefined => { +export const envInt = (name: string): number | undefined => { + const str = process.env[name] return parseIntWithFallback(str, undefined) } -export const envStr = (str: string | undefined): string | undefined => { +export const envStr = (name: string): string | undefined => { + const str = process.env[name] if (str === undefined || str.length === 0) return undefined return str } -export const envBool = (str: string | undefined): boolean | undefined => { +export const envBool = (name: string): boolean | undefined => { + const str = process.env[name] if (str === 'true' || str === '1') return true if (str === 'false' || str === '0') return false return undefined } -export const envList = (str: string | undefined): string[] => { +export const envList = (name: string): string[] => { + const str = process.env[name] if (str === undefined || str.length === 0) return [] return str.split(',') } diff --git a/services/pds/index.js b/services/pds/index.js index a99b7ae980e..67872f3bd74 100644 --- a/services/pds/index.js +++ b/services/pds/index.js @@ -25,7 +25,7 @@ const pkg = require('@atproto/pds/package.json') const main = async () => { const env = readEnv() - env.version ||= pkg.version + env.version ??= pkg.version const cfg = envToCfg(env) const secrets = envToSecrets(env) const pds = await PDS.create(cfg, secrets) From 660ad217444294cad213f6489e851fa53f144726 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 26 Sep 2023 20:17:31 -0500 Subject: [PATCH 084/105] fix admin search --- .../src/api/com/atproto/admin/searchRepos.ts | 8 +- packages/pds/src/services/account/index.ts | 47 ++++-- packages/pds/src/services/util/search.ts | 144 ------------------ 3 files changed, 38 insertions(+), 161 deletions(-) delete mode 100644 packages/pds/src/services/util/search.ts diff --git a/packages/pds/src/api/com/atproto/admin/searchRepos.ts b/packages/pds/src/api/com/atproto/admin/searchRepos.ts index 3ee72dec892..da2d7fa3788 100644 --- a/packages/pds/src/api/com/atproto/admin/searchRepos.ts +++ b/packages/pds/src/api/com/atproto/admin/searchRepos.ts @@ -26,11 +26,11 @@ export default function (server: Server, ctx: AppContext) { const { db, services } = ctx const moderationService = services.moderation(db) const { limit, cursor, invitedBy } = params - const term = params.term?.trim() ?? '' + const query = params.q?.trim() ?? params.term?.trim() ?? '' const keyset = new ListKeyset(sql``, sql``) - if (!term) { + if (!query) { const results = await services .account(db) .list({ limit, cursor, includeSoftDeleted: true, invitedBy }) @@ -47,13 +47,13 @@ export default function (server: Server, ctx: AppContext) { const results = await services .account(db) - .search({ term, limit, cursor, includeSoftDeleted: true }) + .search({ query, limit, cursor, includeSoftDeleted: true }) return { encoding: 'application/json', body: { // For did search, we can only find 1 or no match, cursors can be ignored entirely - cursor: term.startsWith('did:') + cursor: query.startsWith('did:') ? undefined : keyset.packFromResult(results), repos: await moderationService.views.repo(results, { diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts index 06c0fd4bf9e..3987d022a23 100644 --- a/packages/pds/src/services/account/index.ts +++ b/packages/pds/src/services/account/index.ts @@ -11,7 +11,6 @@ import { countAll, notSoftDeletedClause } from '../../db/util' import { paginate, TimeCidKeyset } from '../../db/pagination' import * as sequencer from '../../sequencer' import { AppPassword } from '../../lexicon/types/com/atproto/server/createAppPassword' -import { getUserSearchQueryPg, getUserSearchQuerySqlite } from '../util/search' export class AccountService { constructor(public db: Database) {} @@ -275,22 +274,44 @@ export class AccountService { } async search(opts: { - term: string + query: string limit: number cursor?: string includeSoftDeleted?: boolean }): Promise<(RepoRoot & DidHandle)[]> { - const builder = - this.db.dialect === 'pg' - ? getUserSearchQueryPg(this.db, opts) - .selectAll('did_handle') - .selectAll('repo_root') - : getUserSearchQuerySqlite(this.db, opts) - .selectAll('did_handle') - .selectAll('repo_root') - .select(sql`0`.as('distance')) - - return await builder.execute() + const { query, limit, cursor, includeSoftDeleted } = opts + const { ref } = this.db.db.dynamic + + const builder = this.db.db + .selectFrom('did_handle') + .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') + .innerJoin('user_account', 'user_account.did', 'did_handle.did') + .if(!includeSoftDeleted, (qb) => + qb.where(notSoftDeletedClause(ref('repo_root'))), + ) + .where((qb) => { + // sqlite doesn't support "ilike", but performs "like" case-insensitively + const likeOp = this.db.dialect === 'pg' ? 'ilike' : 'like' + if (query.includes('@')) { + return qb.where('user_account.email', likeOp, `%${query}%`) + } + if (query.startsWith('did:')) { + return qb.where('did_handle.did', '=', query) + } + return qb.where('did_handle.handle', likeOp, `${query}%`) + }) + .selectAll(['did_handle', 'repo_root']) + + const keyset = new ListKeyset( + ref('repo_root.indexedAt'), + ref('did_handle.handle'), + ) + + return await paginate(builder, { + limit, + cursor, + keyset, + }).execute() } async list(opts: { diff --git a/packages/pds/src/services/util/search.ts b/packages/pds/src/services/util/search.ts deleted file mode 100644 index 26b3e81bf06..00000000000 --- a/packages/pds/src/services/util/search.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { sql } from 'kysely' -import { InvalidRequestError } from '@atproto/xrpc-server' -import Database from '../../db' -import { notSoftDeletedClause, DbRef } from '../../db/util' -import { GenericKeyset, paginate } from '../../db/pagination' - -// @TODO utilized in both pds and app-view -export const getUserSearchQueryPg = ( - db: Database, - opts: { - term: string - limit: number - cursor?: string - includeSoftDeleted?: boolean - invitedBy?: string - }, -) => { - const { ref } = db.db.dynamic - const { term, limit, cursor, includeSoftDeleted } = opts - // Matching user accounts based on handle - const distanceAccount = distance(term, ref('handle')) - const accountsQb = getMatchingAccountsQb(db, { term, includeSoftDeleted }) - return paginate(accountsQb, { - limit, - cursor, - direction: 'asc', - keyset: new SearchKeyset(distanceAccount, ref('handle')), - }) -} - -// Matching user accounts based on handle -const getMatchingAccountsQb = ( - db: Database, - opts: { term: string; includeSoftDeleted?: boolean }, -) => { - const { ref } = db.db.dynamic - const { term, includeSoftDeleted } = opts - const distanceAccount = distance(term, ref('handle')) - return db.db - .selectFrom('did_handle') - .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') - .if(!includeSoftDeleted, (qb) => - qb.where(notSoftDeletedClause(ref('repo_root'))), - ) - .where(similar(term, ref('handle'))) // Coarse filter engaging trigram index - .select(['did_handle.did as did', distanceAccount.as('distance')]) -} - -export const getUserSearchQuerySqlite = ( - db: Database, - opts: { - term: string - limit: number - cursor?: string - includeSoftDeleted?: boolean - }, -) => { - const { ref } = db.db.dynamic - const { term, limit, cursor, includeSoftDeleted } = opts - - // Take the first three words in the search term. We're going to build a dynamic query - // based on the number of words, so to keep things predictable just ignore words 4 and - // beyond. We also remove the special wildcard characters supported by the LIKE operator, - // since that's where these values are heading. - const safeWords = term - .replace(/[%_]/g, '') - .split(/\s+/) - .filter(Boolean) - .slice(0, 3) - - if (!safeWords.length) { - // Return no results. This could happen with weird input like ' % _ '. - 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, - // so that we can reliably match word prefixes using LIKE operator. - const textForMatch = sql`lower(' ' || ${ref( - 'did_handle.handle', - )} || ' ' || coalesce(${ref('profile.displayName')}, ''))` - - const keyset = new SearchKeyset(sql``, sql``) - const unpackedCursor = keyset.unpackCursor(cursor) - - return db.db - .selectFrom('did_handle') - .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') - .if(!includeSoftDeleted, (qb) => - qb.where(notSoftDeletedClause(ref('repo_root'))), - ) - .where((q) => { - safeWords.forEach((word) => { - // Match word prefixes against contents of handle and displayName - q = q.where(textForMatch, 'like', `% ${word.toLowerCase()}%`) - }) - return q - }) - .if(!!unpackedCursor, (qb) => - unpackedCursor ? qb.where('handle', '>', unpackedCursor.secondary) : qb, - ) - .orderBy('handle') - .limit(limit) -} - -// Remove leading @ in case a handle is input that way -export const cleanTerm = (term: string) => term.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})` - -// 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})` - -type Result = { distance: number; handle: string } -type LabeledResult = { primary: number; secondary: string } -export class SearchKeyset extends GenericKeyset { - labelResult(result: Result) { - return { - primary: result.distance, - secondary: result.handle, - } - } - labeledResultToCursor(labeled: LabeledResult) { - return { - primary: labeled.primary.toString().replace('0.', '.'), - secondary: labeled.secondary, - } - } - cursorToLabeledResult(cursor: { primary: string; secondary: string }) { - const distance = parseFloat(cursor.primary) - if (isNaN(distance)) { - throw new InvalidRequestError('Malformed cursor') - } - return { - primary: distance, - secondary: cursor.secondary, - } - } -} From 0e4821ccb519ace729cc0a6b9e07b147e7fd86d9 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 26 Sep 2023 20:19:37 -0500 Subject: [PATCH 085/105] tidy --- packages/bsky/CHANGELOG.md | 4 ---- packages/dev-env/CHANGELOG.md | 4 ---- packages/pds/CHANGELOG.md | 4 ---- 3 files changed, 12 deletions(-) diff --git a/packages/bsky/CHANGELOG.md b/packages/bsky/CHANGELOG.md index 3d71922bd68..f797a848ce9 100644 --- a/packages/bsky/CHANGELOG.md +++ b/packages/bsky/CHANGELOG.md @@ -1,7 +1,5 @@ # @atproto/bsky -# <<<<<<< HEAD - ## 0.0.10 ### Patch Changes @@ -9,8 +7,6 @@ - Updated dependencies [[`35b616cd`](https://github.com/bluesky-social/atproto/commit/35b616cd82232879937afc88d3f77d20c6395276)]: - @atproto/api@0.6.19 -> > > > > > > main - ## 0.0.9 ### Patch Changes diff --git a/packages/dev-env/CHANGELOG.md b/packages/dev-env/CHANGELOG.md index d0771fdb359..1bc9601f4c9 100644 --- a/packages/dev-env/CHANGELOG.md +++ b/packages/dev-env/CHANGELOG.md @@ -1,7 +1,5 @@ # @atproto/dev-env -# <<<<<<< HEAD - ## 0.2.10 ### Patch Changes @@ -11,8 +9,6 @@ - @atproto/bsky@0.0.10 - @atproto/pds@0.1.19 -> > > > > > > main - ## 0.2.9 ### Patch Changes diff --git a/packages/pds/CHANGELOG.md b/packages/pds/CHANGELOG.md index 2d4b22b519d..88c86c867d1 100644 --- a/packages/pds/CHANGELOG.md +++ b/packages/pds/CHANGELOG.md @@ -1,7 +1,5 @@ # @atproto/pds -# <<<<<<< HEAD - ## 0.1.19 ### Patch Changes @@ -9,8 +7,6 @@ - Updated dependencies [[`35b616cd`](https://github.com/bluesky-social/atproto/commit/35b616cd82232879937afc88d3f77d20c6395276)]: - @atproto/api@0.6.19 -> > > > > > > main - ## 0.1.18 ### Patch Changes From 58d527b5c3084c275756aeb025ff448a6736e8fc Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 26 Sep 2023 20:21:16 -0500 Subject: [PATCH 086/105] tidy --- packages/api/CHANGELOG.md | 6 + packages/pds/src/api/app/bsky/simple.ts | 383 ------------------------ 2 files changed, 6 insertions(+), 383 deletions(-) delete mode 100644 packages/pds/src/api/app/bsky/simple.ts 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/pds/src/api/app/bsky/simple.ts b/packages/pds/src/api/app/bsky/simple.ts deleted file mode 100644 index 4603895228e..00000000000 --- a/packages/pds/src/api/app/bsky/simple.ts +++ /dev/null @@ -1,383 +0,0 @@ -import { Server } from '../../../lexicon' -import AppContext from '../../../context' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.actor.getSuggestions({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - const res = await ctx.appViewAgent.api.app.bsky.actor.getSuggestions( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.actor.searchActors({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - const res = await ctx.appViewAgent.api.app.bsky.actor.searchActors( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.actor.searchActorsTypeahead({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - const res = - await ctx.appViewAgent.api.app.bsky.actor.searchActorsTypeahead( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.feed.getActorFeeds({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - const res = await ctx.appViewAgent.api.app.bsky.feed.getActorFeeds( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.feed.getFeed({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - 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, - } - }, - }) - - server.app.bsky.feed.getFeedGenerator({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - const res = await ctx.appViewAgent.api.app.bsky.feed.getFeedGenerator( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.feed.getFeedGenerators({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - const res = await ctx.appViewAgent.api.app.bsky.feed.getFeedGenerators( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.feed.getPosts({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - const res = await ctx.appViewAgent.api.app.bsky.feed.getPosts( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.feed.getLikes({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - const res = await ctx.appViewAgent.api.app.bsky.feed.getLikes( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.feed.getRepostedBy({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - const res = await ctx.appViewAgent.api.app.bsky.feed.getRepostedBy( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.unspecced.getPopularFeedGenerators({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - 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, - } - }, - }) - - server.app.bsky.graph.getBlocks({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - const res = await ctx.appViewAgent.api.app.bsky.graph.getBlocks( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.graph.getFollowers({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - const res = await ctx.appViewAgent.api.app.bsky.graph.getFollowers( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.graph.getFollows({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - const res = await ctx.appViewAgent.api.app.bsky.graph.getFollows( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.graph.getList({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - const res = await ctx.appViewAgent.api.app.bsky.graph.getList( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.graph.getListMutes({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - const res = await ctx.appViewAgent.api.app.bsky.graph.getListMutes( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.graph.getListBlocks({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - const res = await ctx.appViewAgent.api.app.bsky.graph.getListBlocks( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.graph.getLists({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - const res = await ctx.appViewAgent.api.app.bsky.graph.getLists( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.graph.getMutes({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - const res = await ctx.appViewAgent.api.app.bsky.graph.getMutes( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.graph.muteActor({ - auth: ctx.accessVerifier, - handler: async ({ input, auth }) => { - const requester = auth.credentials.did - await ctx.appViewAgent.api.app.bsky.graph.muteActor(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), - encoding: 'application/json', - }) - }, - }) - - server.app.bsky.graph.muteActorList({ - auth: ctx.accessVerifier, - handler: async ({ input, auth }) => { - const requester = auth.credentials.did - await ctx.appViewAgent.api.app.bsky.graph.muteActorList(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), - encoding: 'application/json', - }) - }, - }) - - server.app.bsky.graph.unmuteActor({ - auth: ctx.accessVerifier, - handler: async ({ input, auth }) => { - const requester = auth.credentials.did - await ctx.appViewAgent.api.app.bsky.graph.unmuteActor(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), - encoding: 'application/json', - }) - }, - }) - - server.app.bsky.graph.unmuteActorList({ - auth: ctx.accessVerifier, - handler: async ({ input, auth }) => { - const requester = auth.credentials.did - await ctx.appViewAgent.api.app.bsky.graph.unmuteActorList(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), - encoding: 'application/json', - }) - }, - }) - - server.app.bsky.notification.getUnreadCount({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - const res = - await ctx.appViewAgent.api.app.bsky.notification.getUnreadCount( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.notification.listNotifications({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - const res = - await ctx.appViewAgent.api.app.bsky.notification.listNotifications( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) - - server.app.bsky.notification.updateSeen({ - auth: ctx.accessVerifier, - handler: async ({ input, auth }) => { - const requester = auth.credentials.did - await ctx.appViewAgent.api.app.bsky.notification.updateSeen(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), - encoding: 'application/json', - }) - }, - }) -} From c3f48604f4796c4c432296fa425417821d496b5c Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 26 Sep 2023 20:29:02 -0500 Subject: [PATCH 087/105] add snap for getListFeed --- .../proxied/__snapshots__/views.test.ts.snap | 782 ++++++++++++++++++ 1 file changed, 782 insertions(+) diff --git a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap index 67c7286359d..44f82378227 100644 --- a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap @@ -1556,6 +1556,788 @@ Object { } `; +exports[`proxies view requests feed.getListFeed 2`] = ` +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/sample-img/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/sample-img/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/sample-img/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/sample-img/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/sample-img/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(6)", + }, + "size": 4114, + }, + }, + Object { + "alt": "tests/sample-img/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 [ From 1c30e8c191dd6d8b4b01f5cf7535671b58c6c953 Mon Sep 17 00:00:00 2001 From: dholms Date: Wed, 27 Sep 2023 18:19:10 -0500 Subject: [PATCH 088/105] add backup nameserver cfg --- packages/pds/src/config/config.ts | 2 ++ packages/pds/src/config/env.ts | 2 ++ packages/pds/src/context.ts | 1 + 3 files changed, 5 insertions(+) diff --git a/packages/pds/src/config/config.ts b/packages/pds/src/config/config.ts index 7f24c9ebced..b3fef9e47fc 100644 --- a/packages/pds/src/config/config.ts +++ b/packages/pds/src/config/config.ts @@ -90,6 +90,7 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { resolverTimeout: env.resolverTimeout ?? 3 * SECOND, recoveryDidKey: env.recoveryDidKey ?? null, serviceHandleDomains, + handleBackupNameservers: env.handleBackupNameservers, } // default to being required if left undefined @@ -246,6 +247,7 @@ export type IdentityConfig = { cacheMaxTTL: number recoveryDidKey: string | null serviceHandleDomains: string[] + handleBackupNameservers?: string[] } export type InvitesConfig = diff --git a/packages/pds/src/config/env.ts b/packages/pds/src/config/env.ts index 8dde8710e32..170e26d5976 100644 --- a/packages/pds/src/config/env.ts +++ b/packages/pds/src/config/env.ts @@ -35,6 +35,7 @@ export const readEnv = (): ServerEnvironment => { resolverTimeout: envInt('PDS_ID_RESOLVER_TIMEOUT'), recoveryDidKey: envStr('PDS_RECOVERY_DID_KEY'), serviceHandleDomains: envList('PDS_SERVICE_HANDLE_DOMAINS'), + handleBackupNameservers: envList('PDS_HANDLE_BACKUP_NAMESERVERS'), // invites inviteRequired: envBool('PDS_INVITE_REQUIRED'), @@ -123,6 +124,7 @@ export type ServerEnvironment = { resolverTimeout?: number recoveryDidKey?: string serviceHandleDomains?: string[] // public hostname by default + handleBackupNameservers?: string[] // invites inviteRequired?: boolean diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index 466bc9a5613..328b61893a1 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -133,6 +133,7 @@ export class AppContext { plcUrl: cfg.identity.plcUrl, didCache, timeout: cfg.identity.resolverTimeout, + backupNameservers: cfg.identity.handleBackupNameservers, }) const plcClient = new plc.Client(cfg.identity.plcUrl) From b2864ca345cd23cd13df695d9cebe817e253d91c Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 28 Sep 2023 11:45:34 -0500 Subject: [PATCH 089/105] tidy + pr feedback --- packages/dev-env/src/network-no-appview.ts | 1 - packages/pds/src/config/config.ts | 11 +- packages/pds/src/storage/disk-blobstore.ts | 1 - .../proxied/__snapshots__/views.test.ts.snap | 782 ------------------ packages/pds/tests/proxied/views.test.ts | 32 - services/pds/index.js | 12 - 6 files changed, 8 insertions(+), 831 deletions(-) diff --git a/packages/dev-env/src/network-no-appview.ts b/packages/dev-env/src/network-no-appview.ts index 8ab85a5f8b4..f69da964ee3 100644 --- a/packages/dev-env/src/network-no-appview.ts +++ b/packages/dev-env/src/network-no-appview.ts @@ -22,7 +22,6 @@ export class TestNetworkNoAppView { const pds = await TestPds.create({ dbPostgresUrl, dbPostgresSchema, - dbPostgresPoolSize: 5, dbSqliteLocation, didPlcUrl: plc.url, ...params.pds, diff --git a/packages/pds/src/config/config.ts b/packages/pds/src/config/config.ts index b3fef9e47fc..68d043e6431 100644 --- a/packages/pds/src/config/config.ts +++ b/packages/pds/src/config/config.ts @@ -84,7 +84,7 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { } const identityCfg: ServerConfig['identity'] = { - plcUrl: env.didPlcUrl ?? 'https://plc.bsky-sandbox.dev', + plcUrl: env.didPlcUrl ?? 'https://plc.directory', cacheMaxTTL: env.didCacheMaxTTL ?? DAY, cacheStaleTTL: env.didCacheStaleTTL ?? HOUR, resolverTimeout: env.resolverTimeout ?? 3 * SECOND, @@ -142,9 +142,14 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { sequencerLeaderLockId: env.sequencerLeaderLockId ?? 1100, } + if (!env.bskyAppViewUrl) { + throw new Error('Must configure PDS_BSKY_APP_VIEW_URL') + } else if (!env.bskyAppViewDid) { + throw new Error('Must configure PDS_BSKY_APP_VIEW_DID') + } const bskyAppViewCfg: ServerConfig['bskyAppView'] = { - url: env.bskyAppViewUrl ?? 'https://api.bsky-sandbox.dev', - did: env.bskyAppViewDid ?? 'did:plc:abc', // get real did + url: env.bskyAppViewUrl, + did: env.bskyAppViewDid, proxyModeration: env.bskyAppViewModeration ?? false, cdnUrlPattern: env.bskyAppViewCdnUrlPattern, } diff --git a/packages/pds/src/storage/disk-blobstore.ts b/packages/pds/src/storage/disk-blobstore.ts index f1407ed74eb..496e7b42c52 100644 --- a/packages/pds/src/storage/disk-blobstore.ts +++ b/packages/pds/src/storage/disk-blobstore.ts @@ -24,7 +24,6 @@ export class DiskBlobStore implements BlobStore { this.quarantineLocation = quarantineLocation } - // @TODO move quarantine out of temp by default static async create( location: string, tmpLocation?: string, diff --git a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap index 44f82378227..67c7286359d 100644 --- a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap @@ -1556,788 +1556,6 @@ Object { } `; -exports[`proxies view requests feed.getListFeed 2`] = ` -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/sample-img/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/sample-img/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/sample-img/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/sample-img/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/sample-img/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(6)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/sample-img/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 55351c75228..38b486a1d05 100644 --- a/packages/pds/tests/proxied/views.test.ts +++ b/packages/pds/tests/proxied/views.test.ts @@ -228,38 +228,6 @@ 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/services/pds/index.js b/services/pds/index.js index 67872f3bd74..4c7477f6f14 100644 --- a/services/pds/index.js +++ b/services/pds/index.js @@ -14,7 +14,6 @@ require('dd-trace') // Only works with commonjs const path = require('path') const { PDS, - Database, envToCfg, envToSecrets, readEnv, @@ -29,17 +28,6 @@ const main = async () => { const cfg = envToCfg(env) const secrets = envToSecrets(env) const pds = await PDS.create(cfg, secrets) - if (cfg.db.dialect === 'pg') { - // Migrate using credentialed user - const migrateDb = Database.postgres({ - url: cfg.db.migrationUrl, - schema: cfg.db.schema, - }) - await migrateDb.migrateToLatestOrThrow() - await migrateDb.close() - } else { - await pds.ctx.db.migrateToLatestOrThrow() - } // If the PDS is configured to proxy moderation, this will be running on appview instead of pds. // Also don't run this on the sequencer leader, which may not be configured regarding moderation proxying at all. From 75f3653d5c5e4f83c58d16c34596785040b5774a Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 28 Sep 2023 11:53:04 -0500 Subject: [PATCH 090/105] tidy --- package.json | 1 - .../app/bsky/preferences/getPreferences.ts | 26 ----------- .../api/app/bsky/preferences/getProfile.ts | 38 --------------- .../api/app/bsky/preferences/getProfiles.ts | 46 ------------------- .../app/bsky/preferences/getSuggestions.ts | 19 -------- .../pds/src/api/app/bsky/preferences/index.ts | 19 -------- .../app/bsky/preferences/putPreferences.ts | 28 ----------- .../api/app/bsky/preferences/searchActors.ts | 19 -------- .../bsky/preferences/searchActorsTypeahead.ts | 20 -------- pnpm-lock.yaml | 3 -- 10 files changed, 219 deletions(-) delete mode 100644 packages/pds/src/api/app/bsky/preferences/getPreferences.ts delete mode 100644 packages/pds/src/api/app/bsky/preferences/getProfile.ts delete mode 100644 packages/pds/src/api/app/bsky/preferences/getProfiles.ts delete mode 100644 packages/pds/src/api/app/bsky/preferences/getSuggestions.ts delete mode 100644 packages/pds/src/api/app/bsky/preferences/index.ts delete mode 100644 packages/pds/src/api/app/bsky/preferences/putPreferences.ts delete mode 100644 packages/pds/src/api/app/bsky/preferences/searchActors.ts delete mode 100644 packages/pds/src/api/app/bsky/preferences/searchActorsTypeahead.ts diff --git a/package.json b/package.json index 4906b1827f5..9a3f4b42993 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "eslint": "^8.24.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", - "get-port": "^6.1.2", "jest": "^28.1.2", "node-gyp": "^9.3.1", "pino-pretty": "^9.1.0", diff --git a/packages/pds/src/api/app/bsky/preferences/getPreferences.ts b/packages/pds/src/api/app/bsky/preferences/getPreferences.ts deleted file mode 100644 index 1bca50f0bd1..00000000000 --- a/packages/pds/src/api/app/bsky/preferences/getPreferences.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' -import { AuthScope } from '../../../../auth' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.actor.getPreferences({ - auth: ctx.accessVerifier, - handler: async ({ auth }) => { - const requester = auth.credentials.did - const { services, db } = ctx - 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 }, - } - }, - }) -} diff --git a/packages/pds/src/api/app/bsky/preferences/getProfile.ts b/packages/pds/src/api/app/bsky/preferences/getProfile.ts deleted file mode 100644 index c200e1dd75f..00000000000 --- a/packages/pds/src/api/app/bsky/preferences/getProfile.ts +++ /dev/null @@ -1,38 +0,0 @@ -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' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.actor.getProfile({ - auth: ctx.accessOrRoleVerifier, - handler: async ({ req, auth, params }) => { - const requester = - auth.credentials.type === 'access' ? auth.credentials.did : null - 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, - } - }, - }) -} - -const getProfileMunge = async ( - ctx: AppContext, - original: OutputSchema, - local: LocalRecords, -): Promise => { - if (!local.profile) return original - return ctx.services - .local(ctx.db) - .updateProfileDetailed(original, local.profile.record) -} diff --git a/packages/pds/src/api/app/bsky/preferences/getProfiles.ts b/packages/pds/src/api/app/bsky/preferences/getProfiles.ts deleted file mode 100644 index ebec9e36938..00000000000 --- a/packages/pds/src/api/app/bsky/preferences/getProfiles.ts +++ /dev/null @@ -1,46 +0,0 @@ -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) { - server.app.bsky.actor.getProfiles({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - 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 getProfilesMunge = async ( - ctx: AppContext, - original: OutputSchema, - local: LocalRecords, - requester: string, -): Promise => { - const localProf = local.profile - if (!localProf) return original - const profiles = original.profiles.map((prof) => { - if (prof.did !== requester) return prof - return ctx.services - .local(ctx.db) - .updateProfileDetailed(prof, localProf.record) - }) - return { - ...original, - profiles, - } -} diff --git a/packages/pds/src/api/app/bsky/preferences/getSuggestions.ts b/packages/pds/src/api/app/bsky/preferences/getSuggestions.ts deleted file mode 100644 index 03c6fa6e609..00000000000 --- a/packages/pds/src/api/app/bsky/preferences/getSuggestions.ts +++ /dev/null @@ -1,19 +0,0 @@ -import AppContext from '../../../../context' -import { Server } from '../../../../lexicon' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.actor.getSuggestions({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - const res = await ctx.appViewAgent.api.app.bsky.actor.getSuggestions( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) -} diff --git a/packages/pds/src/api/app/bsky/preferences/index.ts b/packages/pds/src/api/app/bsky/preferences/index.ts deleted file mode 100644 index db69616f7e8..00000000000 --- a/packages/pds/src/api/app/bsky/preferences/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -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/api/app/bsky/preferences/putPreferences.ts b/packages/pds/src/api/app/bsky/preferences/putPreferences.ts deleted file mode 100644 index 27528595116..00000000000 --- a/packages/pds/src/api/app/bsky/preferences/putPreferences.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' -import { UserPreference } from '../../../../services/account' -import { InvalidRequestError } from '@atproto/xrpc-server' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.actor.putPreferences({ - auth: ctx.accessVerifierCheckTakedown, - handler: async ({ auth, input }) => { - const { preferences } = input.body - const requester = auth.credentials.did - const { services, db } = ctx - const checkedPreferences: UserPreference[] = [] - for (const pref of preferences) { - if (typeof pref.$type === 'string') { - checkedPreferences.push(pref as UserPreference) - } else { - throw new InvalidRequestError('Preference is missing a $type') - } - } - await db.transaction(async (tx) => { - await services - .account(tx) - .putPreferences(requester, checkedPreferences, 'app.bsky') - }) - }, - }) -} diff --git a/packages/pds/src/api/app/bsky/preferences/searchActors.ts b/packages/pds/src/api/app/bsky/preferences/searchActors.ts deleted file mode 100644 index 0ae4d9b211c..00000000000 --- a/packages/pds/src/api/app/bsky/preferences/searchActors.ts +++ /dev/null @@ -1,19 +0,0 @@ -import AppContext from '../../../../context' -import { Server } from '../../../../lexicon' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.actor.searchActors({ - auth: ctx.accessVerifier, - handler: async ({ auth, params }) => { - const requester = auth.credentials.did - const res = await ctx.appViewAgent.api.app.bsky.actor.searchActors( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) -} diff --git a/packages/pds/src/api/app/bsky/preferences/searchActorsTypeahead.ts b/packages/pds/src/api/app/bsky/preferences/searchActorsTypeahead.ts deleted file mode 100644 index 3ece0083ec2..00000000000 --- a/packages/pds/src/api/app/bsky/preferences/searchActorsTypeahead.ts +++ /dev/null @@ -1,20 +0,0 @@ -import AppContext from '../../../../context' -import { Server } from '../../../../lexicon' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.actor.searchActorsTypeahead({ - auth: ctx.accessVerifier, - handler: async ({ params, auth }) => { - const requester = auth.credentials.did - const res = - await ctx.appViewAgent.api.app.bsky.actor.searchActorsTypeahead( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, - } - }, - }) -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 698a8bc86b5..32dca6c6b0f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,9 +65,6 @@ importers: eslint-plugin-prettier: specifier: ^4.2.1 version: 4.2.1(eslint-config-prettier@8.5.0)(eslint@8.24.0)(prettier@2.7.1) - get-port: - specifier: ^6.1.2 - version: 6.1.2 jest: specifier: ^28.1.2 version: 28.1.2(@types/node@18.0.0)(ts-node@10.8.2) From 5603a19204c5396ea5f7b678e52775b95d482a1c Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 28 Sep 2023 12:13:14 -0500 Subject: [PATCH 091/105] tidy env --- Makefile | 12 +++--------- package.json | 1 - packages/bsky/example.dev.env | 5 ----- packages/bsky/package.json | 1 - packages/{pds => dev-env}/example.dev.env | 0 packages/dev-env/package.json | 2 +- packages/dev-env/src/bin.ts | 1 + packages/{bsky => dev-env}/src/env.ts | 0 packages/pds/{.env.example => example.env} | 0 packages/pds/package.json | 1 - packages/pds/src/env.ts | 9 --------- pnpm-lock.yaml | 12 ++---------- 12 files changed, 7 insertions(+), 37 deletions(-) delete mode 100644 packages/bsky/example.dev.env rename packages/{pds => dev-env}/example.dev.env (100%) rename packages/{bsky => dev-env}/src/env.ts (100%) rename packages/pds/{.env.example => example.env} (100%) delete mode 100644 packages/pds/src/env.ts diff --git a/Makefile b/Makefile index f9e90a86a44..3bc977c15a3 100644 --- a/Makefile +++ b/Makefile @@ -22,15 +22,9 @@ test: ## Run all tests run-dev-env: ## Run a "development environment" shell cd packages/dev-env; pnpm run start -.PHONY: run-dev-pds -run-dev-pds: ## Run PDS locally - if [ ! -f "packages/pds/.dev.env" ]; then cp packages/pds/example.dev.env packages/pds/.dev.env; fi - cd packages/pds; ENV=dev pnpm run start | pnpm exec pino-pretty - -.PHONY: run-dev-bsky -run-dev-bsky: ## Run appview ('bsky') locally - if [ ! -f "packages/bsky/.dev.env" ]; then cp packages/bsky/example.dev.env packages/bsky/.dev.env; fi - cd packages/bsky; ENV=dev pnpm run start | pnpm exec pino-pretty +.PHONY: run-dev-env-logged +run-dev-env: ## Run a "development environment" shell + LOG_ENABLED=true cd packages/dev-env; pnpm run start | pnpm exec pino-pretty .PHONY: codegen codegen: ## Re-generate packages from lexicon/ files diff --git a/package.json b/package.json index 9a3f4b42993..44266a35294 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "@typescript-eslint/eslint-plugin": "^5.38.1", "@typescript-eslint/parser": "^5.38.1", "babel-eslint": "^10.1.0", - "dotenv": "^16.0.3", "esbuild": "^0.14.48", "esbuild-node-externals": "^1.5.0", "esbuild-plugin-copy": "^1.6.0", diff --git a/packages/bsky/example.dev.env b/packages/bsky/example.dev.env deleted file mode 100644 index 021790c9731..00000000000 --- a/packages/bsky/example.dev.env +++ /dev/null @@ -1,5 +0,0 @@ -DB_POSTGRES_URL="postgres://bsky:yksb@localhost/bsky_dev" -DEBUG_MODE=1 -LOG_ENABLED="true" -LOG_LEVEL=debug -LOG_DESTINATION=1 diff --git a/packages/bsky/package.json b/packages/bsky/package.json index 1ea154701b5..e4eef4e8707 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -43,7 +43,6 @@ "@isaacs/ttlcache": "^1.4.1", "compression": "^1.7.4", "cors": "^2.8.5", - "dotenv": "^16.0.0", "express": "^4.17.2", "express-async-errors": "^3.1.1", "form-data": "^4.0.0", diff --git a/packages/pds/example.dev.env b/packages/dev-env/example.dev.env similarity index 100% rename from packages/pds/example.dev.env rename to packages/dev-env/example.dev.env diff --git a/packages/dev-env/package.json b/packages/dev-env/package.json index dde8dfb3e38..f379526cdc2 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -37,7 +37,7 @@ "@did-plc/server": "^0.0.1", "better-sqlite3": "^7.6.2", "chalk": "^5.0.1", - "dotenv": "^16.0.1", + "dotenv": "^16.0.3", "express": "^4.18.2", "get-port": "^6.1.2", "sharp": "^0.31.2", diff --git a/packages/dev-env/src/bin.ts b/packages/dev-env/src/bin.ts index be6084c987a..3910e930146 100644 --- a/packages/dev-env/src/bin.ts +++ b/packages/dev-env/src/bin.ts @@ -1,3 +1,4 @@ +import './env' import { generateMockSetup } from './mock' import { TestNetwork } from './network' diff --git a/packages/bsky/src/env.ts b/packages/dev-env/src/env.ts similarity index 100% rename from packages/bsky/src/env.ts rename to packages/dev-env/src/env.ts diff --git a/packages/pds/.env.example b/packages/pds/example.env similarity index 100% rename from packages/pds/.env.example rename to packages/pds/example.env diff --git a/packages/pds/package.json b/packages/pds/package.json index 6d86a8860e9..065d6de9e5b 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -46,7 +46,6 @@ "bytes": "^3.1.2", "compression": "^1.7.4", "cors": "^2.8.5", - "dotenv": "^16.0.0", "express": "^4.17.2", "express-async-errors": "^3.1.1", "file-type": "^16.5.4", diff --git a/packages/pds/src/env.ts b/packages/pds/src/env.ts deleted file mode 100644 index f26b2571b08..00000000000 --- a/packages/pds/src/env.ts +++ /dev/null @@ -1,9 +0,0 @@ -// NOTE: this file should be imported first, particularly before `@atproto/common` (for logging), to ensure that environment variables are respected in library code -import dotenv from 'dotenv' - -const env = process.env.ENV -if (env) { - dotenv.config({ path: `./.${env}.env` }) -} else { - dotenv.config() -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32dca6c6b0f..c50e8d609e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,9 +44,6 @@ importers: babel-eslint: specifier: ^10.1.0 version: 10.1.0(eslint@8.24.0) - dotenv: - specifier: ^16.0.3 - version: 16.0.3 esbuild: specifier: ^0.14.48 version: 0.14.48 @@ -192,9 +189,6 @@ importers: cors: specifier: ^2.8.5 version: 2.8.5 - dotenv: - specifier: ^16.0.0 - version: 16.0.3 express: specifier: ^4.17.2 version: 4.18.2 @@ -368,7 +362,7 @@ importers: specifier: ^5.0.1 version: 5.1.1 dotenv: - specifier: ^16.0.1 + specifier: ^16.0.3 version: 16.0.3 express: specifier: ^4.18.2 @@ -507,9 +501,6 @@ importers: cors: specifier: ^2.8.5 version: 2.8.5 - dotenv: - specifier: ^16.0.0 - version: 16.0.3 express: specifier: ^4.17.2 version: 4.18.2 @@ -6687,6 +6678,7 @@ packages: /dotenv@16.0.3: resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} engines: {node: '>=12'} + dev: false /dotenv@8.6.0: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} From 6dd27aa0a11a670778e8b422e40dae0943f1f11a Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 28 Sep 2023 12:13:43 -0500 Subject: [PATCH 092/105] bit more --- packages/dev-env/example.dev.env | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 packages/dev-env/example.dev.env diff --git a/packages/dev-env/example.dev.env b/packages/dev-env/example.dev.env deleted file mode 100644 index 561947cf22d..00000000000 --- a/packages/dev-env/example.dev.env +++ /dev/null @@ -1,6 +0,0 @@ -DB_POSTGRES_URL="postgres://bsky:yksb@localhost/pds_dev" -DEBUG_MODE=1 -LOG_ENABLED="true" -LOG_LEVEL=debug -LOG_DESTINATION=1 -AVAILABLE_USER_DOMAINS=".test,.dev.bsky.dev" From 497d1001c88c4b80268f68ca799c9cc1c1439140 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 28 Sep 2023 12:59:27 -0500 Subject: [PATCH 093/105] re-add dotenv to root package.json --- package.json | 1 + pnpm-lock.yaml | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 44266a35294..9a3f4b42993 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@typescript-eslint/eslint-plugin": "^5.38.1", "@typescript-eslint/parser": "^5.38.1", "babel-eslint": "^10.1.0", + "dotenv": "^16.0.3", "esbuild": "^0.14.48", "esbuild-node-externals": "^1.5.0", "esbuild-plugin-copy": "^1.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c50e8d609e5..ade852d4323 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: babel-eslint: specifier: ^10.1.0 version: 10.1.0(eslint@8.24.0) + dotenv: + specifier: ^16.0.3 + version: 16.0.3 esbuild: specifier: ^0.14.48 version: 0.14.48 @@ -6678,7 +6681,6 @@ packages: /dotenv@16.0.3: resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} engines: {node: '>=12'} - dev: false /dotenv@8.6.0: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} From fb75e28260ee9eaf82ecb51812b899f32daf6bbd Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 28 Sep 2023 14:08:00 -0500 Subject: [PATCH 094/105] fix dep --- packages/pds/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/pds/package.json b/packages/pds/package.json index 5cb9e34e2d1..ac8e316a9dc 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -47,6 +47,7 @@ "compression": "^1.7.4", "cors": "^2.8.5", "disposable-email": "^0.2.3", + "express": "^4.17.2", "express-async-errors": "^3.1.1", "file-type": "^16.5.4", "form-data": "^4.0.0", From 20b97c2377e6f2e698bb7a7e2a989bb436f9943e Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 28 Sep 2023 14:52:21 -0500 Subject: [PATCH 095/105] build branch --- .github/workflows/build-and-push-bsky-aws.yaml | 1 - .github/workflows/build-and-push-bsky-ghcr.yaml | 1 - .github/workflows/build-and-push-pds-aws.yaml | 1 + 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build-and-push-bsky-aws.yaml b/.github/workflows/build-and-push-bsky-aws.yaml index 80258f3a7df..36b1aa23cb3 100644 --- a/.github/workflows/build-and-push-bsky-aws.yaml +++ b/.github/workflows/build-and-push-bsky-aws.yaml @@ -3,7 +3,6 @@ on: push: branches: - main - - simplify-pds env: REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }} diff --git a/.github/workflows/build-and-push-bsky-ghcr.yaml b/.github/workflows/build-and-push-bsky-ghcr.yaml index b9137db1a54..5d22cd9a389 100644 --- a/.github/workflows/build-and-push-bsky-ghcr.yaml +++ b/.github/workflows/build-and-push-bsky-ghcr.yaml @@ -3,7 +3,6 @@ on: push: branches: - main - - simplify-pds env: REGISTRY: ghcr.io USERNAME: ${{ github.actor }} diff --git a/.github/workflows/build-and-push-pds-aws.yaml b/.github/workflows/build-and-push-pds-aws.yaml index 097f782d88e..8d132401d21 100644 --- a/.github/workflows/build-and-push-pds-aws.yaml +++ b/.github/workflows/build-and-push-pds-aws.yaml @@ -3,6 +3,7 @@ on: push: branches: - main + - simplify-pds env: REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }} From 1134fd84f8785fadf3e9ec50456c8e07f7f64418 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 28 Sep 2023 15:07:46 -0500 Subject: [PATCH 096/105] fix tests --- packages/api/package.json | 2 +- packages/api/tests/bsky-agent.test.ts | 44 ++++++++----------- packages/dev-env/src/pds.ts | 2 + .../20230922T033938477Z-remove-appview.ts | 29 ------------ .../20230926T195532354Z-email-tokens.ts | 9 ++++ packages/pds/tests/_util.ts | 2 + pnpm-lock.yaml | 6 +-- 7 files changed, 36 insertions(+), 58 deletions(-) delete mode 100644 packages/pds/src/db/migrations/20230922T033938477Z-remove-appview.ts diff --git a/packages/api/package.json b/packages/api/package.json index 20780bfb888..a8af7b6b3d7 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -40,7 +40,7 @@ }, "devDependencies": { "@atproto/lex-cli": "workspace:^", - "@atproto/pds": "workspace:^", + "@atproto/dev-env": "workspace:^", "common-tags": "^1.8.2" } } diff --git a/packages/api/tests/bsky-agent.test.ts b/packages/api/tests/bsky-agent.test.ts index 8066bd61f3a..5582f7ac021 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/bsky-agent.test.ts @@ -1,23 +1,17 @@ -import { - CloseFn, - runTestServer, - TestServerInfo, -} from '@atproto/pds/tests/_util' +import { TestNetworkNoAppView } from '@atproto/dev-env' import { BskyAgent, ComAtprotoRepoPutRecord, AppBskyActorProfile } from '..' describe('agent', () => { - let server: TestServerInfo - let close: CloseFn + let network: TestNetworkNoAppView beforeAll(async () => { - server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'bsky_agent', }) - close = server.close }) afterAll(async () => { - await close() + await network.close() }) const getProfileDisplayName = async ( @@ -35,7 +29,7 @@ describe('agent', () => { } it('upsertProfile correctly creates and updates profiles.', async () => { - const agent = new BskyAgent({ service: server.url }) + const agent = new BskyAgent({ service: network.pds.url }) await agent.createAccount({ handle: 'user1.test', @@ -67,7 +61,7 @@ describe('agent', () => { }) it('upsertProfile correctly handles CAS failures.', async () => { - const agent = new BskyAgent({ service: server.url }) + const agent = new BskyAgent({ service: network.pds.url }) await agent.createAccount({ handle: 'user2.test', @@ -106,7 +100,7 @@ describe('agent', () => { }) it('upsertProfile wont endlessly retry CAS failures.', async () => { - const agent = new BskyAgent({ service: server.url }) + const agent = new BskyAgent({ service: network.pds.url }) await agent.createAccount({ handle: 'user3.test', @@ -135,7 +129,7 @@ describe('agent', () => { }) it('upsertProfile validates the record.', async () => { - const agent = new BskyAgent({ service: server.url }) + const agent = new BskyAgent({ service: network.pds.url }) await agent.createAccount({ handle: 'user4.test', @@ -153,70 +147,70 @@ describe('agent', () => { describe('app', () => { it('should retrieve the api app', () => { - const agent = new BskyAgent({ service: server.url }) + const agent = new BskyAgent({ service: network.pds.url }) expect(agent.app).toBe(agent.api.app) }) }) describe('post', () => { it('should throw if no session', async () => { - const agent = new BskyAgent({ service: server.url }) + const agent = new BskyAgent({ service: network.pds.url }) await expect(agent.post({ text: 'foo' })).rejects.toThrow('Not logged in') }) }) describe('deletePost', () => { it('should throw if no session', async () => { - const agent = new BskyAgent({ service: server.url }) + const agent = new BskyAgent({ service: network.pds.url }) await expect(agent.deletePost('foo')).rejects.toThrow('Not logged in') }) }) describe('like', () => { it('should throw if no session', async () => { - const agent = new BskyAgent({ service: server.url }) + const agent = new BskyAgent({ service: network.pds.url }) await expect(agent.like('foo', 'bar')).rejects.toThrow('Not logged in') }) }) describe('deleteLike', () => { it('should throw if no session', async () => { - const agent = new BskyAgent({ service: server.url }) + const agent = new BskyAgent({ service: network.pds.url }) await expect(agent.deleteLike('foo')).rejects.toThrow('Not logged in') }) }) describe('repost', () => { it('should throw if no session', async () => { - const agent = new BskyAgent({ service: server.url }) + const agent = new BskyAgent({ service: network.pds.url }) await expect(agent.repost('foo', 'bar')).rejects.toThrow('Not logged in') }) }) describe('deleteRepost', () => { it('should throw if no session', async () => { - const agent = new BskyAgent({ service: server.url }) + const agent = new BskyAgent({ service: network.pds.url }) await expect(agent.deleteRepost('foo')).rejects.toThrow('Not logged in') }) }) describe('follow', () => { it('should throw if no session', async () => { - const agent = new BskyAgent({ service: server.url }) + const agent = new BskyAgent({ service: network.pds.url }) await expect(agent.follow('foo')).rejects.toThrow('Not logged in') }) }) describe('deleteFollow', () => { it('should throw if no session', async () => { - const agent = new BskyAgent({ service: server.url }) + const agent = new BskyAgent({ service: network.pds.url }) await expect(agent.deleteFollow('foo')).rejects.toThrow('Not logged in') }) }) describe('preferences methods', () => { it('gets and sets preferences correctly', async () => { - const agent = new BskyAgent({ service: server.url }) + const agent = new BskyAgent({ service: network.pds.url }) await agent.createAccount({ handle: 'user5.test', @@ -714,7 +708,7 @@ describe('agent', () => { }) it('resolves duplicates correctly', async () => { - const agent = new BskyAgent({ service: server.url }) + const agent = new BskyAgent({ service: network.pds.url }) await agent.createAccount({ handle: 'user6.test', diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index 46a73e8813b..501ae390cdb 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -41,6 +41,8 @@ export class TestPds { jwtSecret: 'jwt-secret', serviceHandleDomains: ['.test'], sequencerLeaderLockId: uniqueLockId(), + bskyAppViewUrl: 'https://appview.invalid', + bskyAppViewDid: 'did:example:invalid', bskyAppViewCdnUrlPattern: 'http://cdn.appview.com/%s/%s/%s', repoSigningKeyK256PrivateKeyHex: repoSigningPriv, plcRotationKeyK256PrivateKeyHex: plcRotationPriv, diff --git a/packages/pds/src/db/migrations/20230922T033938477Z-remove-appview.ts b/packages/pds/src/db/migrations/20230922T033938477Z-remove-appview.ts deleted file mode 100644 index f66825fa722..00000000000 --- a/packages/pds/src/db/migrations/20230922T033938477Z-remove-appview.ts +++ /dev/null @@ -1,29 +0,0 @@ -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/20230926T195532354Z-email-tokens.ts b/packages/pds/src/db/migrations/20230926T195532354Z-email-tokens.ts index 2a02500ddd1..44cefc18899 100644 --- a/packages/pds/src/db/migrations/20230926T195532354Z-email-tokens.ts +++ b/packages/pds/src/db/migrations/20230926T195532354Z-email-tokens.ts @@ -21,6 +21,8 @@ export async function up(db: Kysely, dialect: Dialect): Promise { .addColumn('emailConfirmedAt', 'varchar') .execute() + await db.schema.dropIndex('user_account_password_reset_token_idx').execute() + await db.schema .alterTable('user_account') .dropColumn('passwordResetToken') @@ -41,6 +43,13 @@ export async function down(db: Kysely): Promise { .dropColumn('emailConfirmedAt') .execute() + await db.schema + .createIndex('user_account_password_reset_token_idx') + .unique() + .on('user_account') + .column('passwordResetToken') + .execute() + await db.schema .alterTable('user_account') .addColumn('passwordResetToken', 'varchar') diff --git a/packages/pds/tests/_util.ts b/packages/pds/tests/_util.ts index 69ea84d4826..79f9a1c8da8 100644 --- a/packages/pds/tests/_util.ts +++ b/packages/pds/tests/_util.ts @@ -76,6 +76,8 @@ export const runTestServer = async ( didPlcUrl: plcUrl, serviceHandleDomains: ['.test'], sequencerLeaderLockId: uniqueLockId(), + bskyAppViewUrl: 'https://appview.invalid', + bskyAppViewDid: 'did:example:invalid', repoSigningKeyK256PrivateKeyHex: repoSigningPriv, plcRotationKeyK256PrivateKeyHex: plcRotationPriv, adminPassword: ADMIN_PASSWORD, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bdcf79440c1..309e59c502a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,12 +111,12 @@ importers: specifier: ^2.1.0 version: 2.1.0 devDependencies: + '@atproto/dev-env': + specifier: workspace:^ + version: link:../dev-env '@atproto/lex-cli': specifier: workspace:^ version: link:../lex-cli - '@atproto/pds': - specifier: workspace:^ - version: link:../pds common-tags: specifier: ^1.8.2 version: 1.8.2 From 5b2f4d12647b09c7c8d40ff143b830b06ad2317f Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 28 Sep 2023 19:23:04 -0500 Subject: [PATCH 097/105] Refactor tests to make better use of dev-env (#1690) * refactor pds tests to use dev env * refactor bsky tests * fix pds test * tidy bsky tests --- packages/api/tests/agent.test.ts | 36 +- packages/api/tests/errors.test.ts | 16 +- .../feed-generation.test.ts.snap | 24 +- .../tests/__snapshots__/indexing.test.ts.snap | 8 +- packages/bsky/tests/algos/hot-classic.test.ts | 8 +- packages/bsky/tests/algos/whats-hot.test.ts | 8 +- .../bsky/tests/algos/with-friends.test.ts | 6 +- .../auto-moderator/fuzzy-matcher.test.ts | 5 +- .../bsky/tests/auto-moderator/labeler.test.ts | 12 +- .../tests/auto-moderator/takedowns.test.ts | 15 +- packages/bsky/tests/blob-resolver.test.ts | 4 +- packages/bsky/tests/did-cache.test.ts | 7 +- packages/bsky/tests/feed-generation.test.ts | 14 +- .../bsky/tests/handle-invalidation.test.ts | 5 +- packages/bsky/tests/image/server.test.ts | 5 +- packages/bsky/tests/image/sharp.test.ts | 2 +- packages/bsky/tests/indexing.test.ts | 5 +- packages/bsky/tests/moderation.test.ts | 6 +- .../bsky/tests/notification-server.test.ts | 5 +- .../bsky/tests/pipeline/backpressure.test.ts | 5 +- packages/bsky/tests/pipeline/reingest.test.ts | 11 +- .../bsky/tests/pipeline/repartition.test.ts | 5 +- packages/bsky/tests/reprocessing.test.ts | 8 +- .../{image/fixtures => sample-img}/at.png | Bin .../{image/fixtures => sample-img}/hd-key.jpg | Bin .../fixtures => sample-img}/key-alt.jpg | Bin .../key-landscape-large.jpg | Bin .../key-landscape-small.jpg | Bin .../key-portrait-large.jpg | Bin .../key-portrait-small.jpg | Bin packages/bsky/tests/seeds/basic.ts | 8 +- packages/bsky/tests/seeds/client.ts | 466 ------------------ packages/bsky/tests/seeds/follows.ts | 2 +- packages/bsky/tests/seeds/likes.ts | 2 +- packages/bsky/tests/seeds/reposts.ts | 2 +- packages/bsky/tests/seeds/users-bulk.ts | 2 +- packages/bsky/tests/seeds/users.ts | 2 +- packages/bsky/tests/server.test.ts | 4 +- packages/bsky/tests/subscription/repo.test.ts | 6 +- .../__snapshots__/author-feed.test.ts.snap | 48 +- .../views/__snapshots__/blocks.test.ts.snap | 4 +- .../__snapshots__/list-feed.test.ts.snap | 12 +- .../__snapshots__/mute-lists.test.ts.snap | 4 +- .../views/__snapshots__/mutes.test.ts.snap | 4 +- .../__snapshots__/notifications.test.ts.snap | 4 +- .../views/__snapshots__/posts.test.ts.snap | 16 +- .../views/__snapshots__/thread.test.ts.snap | 20 +- .../views/__snapshots__/timeline.test.ts.snap | 116 ++--- packages/bsky/tests/views/actor-likes.test.ts | 5 +- .../bsky/tests/views/actor-search.test.ts | 6 +- .../tests/views/admin/repo-search.test.ts | 6 +- packages/bsky/tests/views/author-feed.test.ts | 6 +- packages/bsky/tests/views/block-lists.test.ts | 8 +- packages/bsky/tests/views/blocks.test.ts | 5 +- packages/bsky/tests/views/follows.test.ts | 6 +- packages/bsky/tests/views/likes.test.ts | 7 +- packages/bsky/tests/views/list-feed.test.ts | 6 +- packages/bsky/tests/views/mute-lists.test.ts | 7 +- packages/bsky/tests/views/mutes.test.ts | 7 +- .../bsky/tests/views/notifications.test.ts | 6 +- packages/bsky/tests/views/posts.test.ts | 5 +- packages/bsky/tests/views/profile.test.ts | 9 +- packages/bsky/tests/views/reposts.test.ts | 6 +- .../tests/views/suggested-follows.test.ts | 5 +- packages/bsky/tests/views/suggestions.test.ts | 6 +- packages/bsky/tests/views/thread.test.ts | 6 +- .../bsky/tests/views/threadgating.test.ts | 5 +- packages/bsky/tests/views/timeline.test.ts | 6 +- packages/dev-env/package.json | 2 + packages/dev-env/src/index.ts | 2 + packages/dev-env/src/network-no-appview.ts | 6 + .../client.ts => dev-env/src/seed-client.ts} | 17 +- packages/dev-env/src/util.ts | 5 +- packages/pds/bench/sequencer.bench.ts | 25 +- packages/pds/package.json | 3 +- packages/pds/tests/_util.ts | 144 ------ packages/pds/tests/account-deletion.test.ts | 28 +- packages/pds/tests/account.test.ts | 44 +- .../tests/admin/get-moderation-action.test.ts | 21 +- .../admin/get-moderation-actions.test.ts | 31 +- .../tests/admin/get-moderation-report.test.ts | 21 +- .../admin/get-moderation-reports.test.ts | 59 +-- packages/pds/tests/admin/get-record.test.ts | 30 +- packages/pds/tests/admin/get-repo.test.ts | 34 +- packages/pds/tests/admin/invites.test.ts | 76 ++- packages/pds/tests/admin/moderation.test.ts | 123 +++-- packages/pds/tests/admin/repo-search.test.ts | 17 +- packages/pds/tests/app-passwords.test.ts | 16 +- packages/pds/tests/auth.test.ts | 23 +- packages/pds/tests/blob-deletes.test.ts | 25 +- packages/pds/tests/create-post.test.ts | 15 +- packages/pds/tests/crud.test.ts | 28 +- packages/pds/tests/db.test.ts | 13 +- packages/pds/tests/email-confirmation.test.ts | 5 +- packages/pds/tests/file-uploads.test.ts | 49 +- packages/pds/tests/handles.test.ts | 24 +- packages/pds/tests/invite-codes.test.ts | 67 +-- packages/pds/tests/preferences.test.ts | 19 +- packages/pds/tests/proxied/admin.test.ts | 5 +- packages/pds/tests/proxied/feedgen.test.ts | 5 +- packages/pds/tests/proxied/notif.test.ts | 5 +- packages/pds/tests/proxied/procedures.test.ts | 5 +- .../tests/proxied/read-after-write.test.ts | 5 +- packages/pds/tests/proxied/views.test.ts | 5 +- packages/pds/tests/races.test.ts | 17 +- packages/pds/tests/rate-limits.test.ts | 23 +- packages/pds/tests/seeds/basic.ts | 5 +- packages/pds/tests/seeds/follows.ts | 2 +- packages/pds/tests/seeds/likes.ts | 2 +- packages/pds/tests/seeds/reposts.ts | 2 +- packages/pds/tests/seeds/thread.ts | 2 +- packages/pds/tests/seeds/users-bulk.ts | 2 +- packages/pds/tests/seeds/users.ts | 2 +- packages/pds/tests/sequencer.test.ts | 19 +- packages/pds/tests/server.test.ts | 5 +- packages/pds/tests/sql-repo-storage.test.ts | 23 +- packages/pds/tests/sync/list.test.ts | 14 +- .../pds/tests/sync/subscribe-repos.test.ts | 25 +- packages/pds/tests/sync/sync.test.ts | 19 +- pnpm-lock.yaml | 10 +- 120 files changed, 747 insertions(+), 1492 deletions(-) rename packages/bsky/tests/{image/fixtures => sample-img}/at.png (100%) rename packages/bsky/tests/{image/fixtures => sample-img}/hd-key.jpg (100%) rename packages/bsky/tests/{image/fixtures => sample-img}/key-alt.jpg (100%) rename packages/bsky/tests/{image/fixtures => sample-img}/key-landscape-large.jpg (100%) rename packages/bsky/tests/{image/fixtures => sample-img}/key-landscape-small.jpg (100%) rename packages/bsky/tests/{image/fixtures => sample-img}/key-portrait-large.jpg (100%) rename packages/bsky/tests/{image/fixtures => sample-img}/key-portrait-small.jpg (100%) delete mode 100644 packages/bsky/tests/seeds/client.ts rename packages/{pds/tests/seeds/client.ts => dev-env/src/seed-client.ts} (97%) diff --git a/packages/api/tests/agent.test.ts b/packages/api/tests/agent.test.ts index 82d590ec85b..933326c43f2 100644 --- a/packages/api/tests/agent.test.ts +++ b/packages/api/tests/agent.test.ts @@ -1,29 +1,23 @@ import { defaultFetchHandler } from '@atproto/xrpc' -import { - CloseFn, - runTestServer, - TestServerInfo, -} from '@atproto/pds/tests/_util' import { AtpAgent, AtpAgentFetchHandlerResponse, AtpSessionEvent, AtpSessionData, } from '..' +import { TestNetworkNoAppView } from '@atproto/dev-env' describe('agent', () => { - let server: TestServerInfo - let close: CloseFn + let network: TestNetworkNoAppView beforeAll(async () => { - server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'api_agent', }) - close = server.close }) afterAll(async () => { - await close() + await network.close() }) it('creates a new session on account creation.', async () => { @@ -34,7 +28,7 @@ describe('agent', () => { sessions.push(sess) } - const agent = new AtpAgent({ service: server.url, persistSession }) + const agent = new AtpAgent({ service: network.pds.url, persistSession }) const res = await agent.createAccount({ handle: 'user1.test', @@ -74,7 +68,7 @@ describe('agent', () => { sessions.push(sess) } - const agent1 = new AtpAgent({ service: server.url, persistSession }) + const agent1 = new AtpAgent({ service: network.pds.url, persistSession }) const email = 'user2@test.com' await agent1.createAccount({ @@ -83,7 +77,7 @@ describe('agent', () => { password: 'password', }) - const agent2 = new AtpAgent({ service: server.url, persistSession }) + const agent2 = new AtpAgent({ service: network.pds.url, persistSession }) const res1 = await agent2.login({ identifier: 'user2.test', password: 'password', @@ -122,7 +116,7 @@ describe('agent', () => { sessions.push(sess) } - const agent1 = new AtpAgent({ service: server.url, persistSession }) + const agent1 = new AtpAgent({ service: network.pds.url, persistSession }) await agent1.createAccount({ handle: 'user3.test', @@ -133,7 +127,7 @@ describe('agent', () => { throw new Error('No session created') } - const agent2 = new AtpAgent({ service: server.url, persistSession }) + const agent2 = new AtpAgent({ service: network.pds.url, persistSession }) const res1 = await agent2.resumeSession(agent1.session) expect(agent2.hasSession).toEqual(true) @@ -165,7 +159,7 @@ describe('agent', () => { sessions.push(sess) } - const agent = new AtpAgent({ service: server.url, persistSession }) + const agent = new AtpAgent({ service: network.pds.url, persistSession }) // create an account and a session with it await agent.createAccount({ @@ -230,7 +224,7 @@ describe('agent', () => { sessions.push(sess) } - const agent = new AtpAgent({ service: server.url, persistSession }) + const agent = new AtpAgent({ service: network.pds.url, persistSession }) // create an account and a session with it await agent.createAccount({ @@ -309,7 +303,7 @@ describe('agent', () => { sessions.push(sess) } - const agent = new AtpAgent({ service: server.url, persistSession }) + const agent = new AtpAgent({ service: network.pds.url, persistSession }) try { await agent.login({ @@ -349,7 +343,7 @@ describe('agent', () => { sessions.push(sess) } - const agent = new AtpAgent({ service: server.url, persistSession }) + const agent = new AtpAgent({ service: network.pds.url, persistSession }) // create an account and a session with it await agent.createAccount({ @@ -420,7 +414,7 @@ describe('agent', () => { newHandlerCallCount++ } - const agent = new AtpAgent({ service: server.url, persistSession }) + const agent = new AtpAgent({ service: network.pds.url, persistSession }) await agent.createAccount({ handle: 'user7.test', @@ -452,7 +446,7 @@ describe('agent', () => { sessions.push(sess) } - const agent = new AtpAgent({ service: server.url, persistSession }) + const agent = new AtpAgent({ service: network.pds.url, persistSession }) await expect( agent.createAccount({ diff --git a/packages/api/tests/errors.test.ts b/packages/api/tests/errors.test.ts index 76e1424badc..2d903f2f17e 100644 --- a/packages/api/tests/errors.test.ts +++ b/packages/api/tests/errors.test.ts @@ -1,25 +1,19 @@ -import { - CloseFn, - runTestServer, - TestServerInfo, -} from '@atproto/pds/tests/_util' import { AtpAgent, ComAtprotoServerCreateAccount } from '..' +import { TestNetworkNoAppView } from '@atproto/dev-env' describe('errors', () => { - let server: TestServerInfo + let network: TestNetworkNoAppView let client: AtpAgent - let close: CloseFn beforeAll(async () => { - server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'known_errors', }) - client = new AtpAgent({ service: server.url }) - close = server.close + client = network.pds.getClient() }) afterAll(async () => { - await close() + await network.close() }) it('constructs the correct error instance', async () => { diff --git a/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap b/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap index 3ddf6266fbd..1a5f8fc9281 100644 --- a/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap +++ b/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap @@ -419,12 +419,12 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(5)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(5)@jpeg", }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(6)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(6)@jpeg", }, @@ -475,7 +475,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -486,7 +486,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -679,12 +679,12 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(5)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(5)@jpeg", }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(6)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(6)@jpeg", }, @@ -735,7 +735,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -746,7 +746,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -906,12 +906,12 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(4)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(4)@jpeg", }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(5)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(5)@jpeg", }, @@ -962,7 +962,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -973,7 +973,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", diff --git a/packages/bsky/tests/__snapshots__/indexing.test.ts.snap b/packages/bsky/tests/__snapshots__/indexing.test.ts.snap index f7ccc4e688a..0ed4aeb4d02 100644 --- a/packages/bsky/tests/__snapshots__/indexing.test.ts.snap +++ b/packages/bsky/tests/__snapshots__/indexing.test.ts.snap @@ -101,7 +101,7 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(2)/cids(5)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(2)/cids(5)@jpeg", }, @@ -134,7 +134,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -245,7 +245,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -256,7 +256,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", diff --git a/packages/bsky/tests/algos/hot-classic.test.ts b/packages/bsky/tests/algos/hot-classic.test.ts index d7f3c221e1b..aa96967c2c5 100644 --- a/packages/bsky/tests/algos/hot-classic.test.ts +++ b/packages/bsky/tests/algos/hot-classic.test.ts @@ -1,8 +1,7 @@ import AtpAgent, { AtUri } from '@atproto/api' -import { SeedClient } from '../seeds/client' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import basicSeed from '../seeds/basic' import { makeAlgos } from '../../src' -import { TestNetwork } from '@atproto/dev-env' describe('algo hot-classic', () => { let network: TestNetwork @@ -26,8 +25,7 @@ describe('algo hot-classic', () => { bsky: { algos: makeAlgos(feedPublisherDid) }, }) agent = new AtpAgent({ service: network.bsky.url }) - const pdsAgent = new AtpAgent({ service: network.pds.url }) - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) alice = sc.dids.alice @@ -43,7 +41,7 @@ describe('algo hot-classic', () => { it('returns well liked posts', async () => { const img = await sc.uploadFile( alice, - 'tests/image/fixtures/key-landscape-small.jpg', + 'tests/sample-img/key-landscape-small.jpg', 'image/jpeg', ) const one = await sc.post(alice, 'first post', undefined, [img]) diff --git a/packages/bsky/tests/algos/whats-hot.test.ts b/packages/bsky/tests/algos/whats-hot.test.ts index 23cac215dda..9fb93a8ce50 100644 --- a/packages/bsky/tests/algos/whats-hot.test.ts +++ b/packages/bsky/tests/algos/whats-hot.test.ts @@ -1,9 +1,8 @@ import { HOUR } from '@atproto/common' import AtpAgent, { AtUri } from '@atproto/api' -import { SeedClient } from '../seeds/client' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import basicSeed from '../seeds/basic' import { makeAlgos } from '../../src' -import { TestNetwork } from '@atproto/dev-env' describe.skip('algo whats-hot', () => { let network: TestNetwork @@ -28,8 +27,7 @@ describe.skip('algo whats-hot', () => { bsky: { algos: makeAlgos(feedPublisherDid) }, }) agent = new AtpAgent({ service: network.bsky.url }) - const pdsAgent = new AtpAgent({ service: network.pds.url }) - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) alice = sc.dids.alice @@ -46,7 +44,7 @@ describe.skip('algo whats-hot', () => { it('returns well liked posts', async () => { const img = await sc.uploadFile( alice, - 'tests/image/fixtures/key-landscape-small.jpg', + 'tests/sample-img/key-landscape-small.jpg', 'image/jpeg', ) const one = await sc.post(carol, 'carol is in the chat') diff --git a/packages/bsky/tests/algos/with-friends.test.ts b/packages/bsky/tests/algos/with-friends.test.ts index 12f35083ae4..2c5339849c8 100644 --- a/packages/bsky/tests/algos/with-friends.test.ts +++ b/packages/bsky/tests/algos/with-friends.test.ts @@ -1,8 +1,7 @@ import AtpAgent, { AtUri } from '@atproto/api' -import { RecordRef, SeedClient } from '../seeds/client' import userSeed from '../seeds/users' import { makeAlgos } from '../../src' -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, SeedClient, RecordRef } from '@atproto/dev-env' describe.skip('algo with friends', () => { let network: TestNetwork @@ -28,8 +27,7 @@ describe.skip('algo with friends', () => { bsky: { algos: makeAlgos(feedPublisherDid) }, }) agent = new AtpAgent({ service: network.bsky.url }) - const pdsAgent = new AtpAgent({ service: network.pds.url }) - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await userSeed(sc) alice = sc.dids.alice diff --git a/packages/bsky/tests/auto-moderator/fuzzy-matcher.test.ts b/packages/bsky/tests/auto-moderator/fuzzy-matcher.test.ts index f9fc320dcb9..09422cd8d6e 100644 --- a/packages/bsky/tests/auto-moderator/fuzzy-matcher.test.ts +++ b/packages/bsky/tests/auto-moderator/fuzzy-matcher.test.ts @@ -1,6 +1,5 @@ +import { TestNetwork, SeedClient } from '@atproto/dev-env' import { FuzzyMatcher, encode } from '../../src/auto-moderator/fuzzy-matcher' -import { TestNetwork } from '@atproto/dev-env' -import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' import { AtpAgent } from '@atproto/api' import { ImageInvalidator } from '../../src/image/invalidator' @@ -25,7 +24,7 @@ describe('fuzzy matcher', () => { }) fuzzyMatcher = new FuzzyMatcher(['evil', 'mean', 'bad'], ['baddie']) agent = network.pds.getClient() - sc = new SeedClient(agent) + sc = network.getSeedClient() await basicSeed(sc) await network.processAll() alice = sc.dids.alice diff --git a/packages/bsky/tests/auto-moderator/labeler.test.ts b/packages/bsky/tests/auto-moderator/labeler.test.ts index 7227e769549..dbd486c6061 100644 --- a/packages/bsky/tests/auto-moderator/labeler.test.ts +++ b/packages/bsky/tests/auto-moderator/labeler.test.ts @@ -1,12 +1,11 @@ -import { AtUri, AtpAgent, BlobRef } from '@atproto/api' +import { TestNetwork } from '@atproto/dev-env' +import { AtUri, BlobRef } from '@atproto/api' import { Readable } from 'stream' import { AutoModerator } from '../../src/auto-moderator' import IndexerContext from '../../src/indexer/context' import { cidForRecord } from '@atproto/repo' -import { cidForCbor, TID } from '@atproto/common' +import { TID } from '@atproto/common' import { LabelService } from '../../src/services/label' -import { TestNetwork } from '@atproto/dev-env' -import { SeedClient } from '../seeds/client' import usersSeed from '../seeds/users' import { CID } from 'multiformats/cid' import { ImgLabeler } from '../../src/auto-moderator/hive' @@ -37,8 +36,7 @@ describe('labeler', () => { autoMod = ctx.autoMod autoMod.imgLabeler = new TestImgLabeler() labelSrvc = ctx.services.label(ctx.db) - const pdsAgent = new AtpAgent({ service: network.pds.url }) - const sc = new SeedClient(pdsAgent) + const sc = network.getSeedClient() await usersSeed(sc) await network.processAll() alice = sc.dids.alice @@ -58,7 +56,7 @@ describe('labeler', () => { await repoSvc.blobs.associateBlob( preparedBlobRef, postUri(), - await cidForCbor(1), + TID.nextStr(), alice, ) return blobRef diff --git a/packages/bsky/tests/auto-moderator/takedowns.test.ts b/packages/bsky/tests/auto-moderator/takedowns.test.ts index 1af6c4d6498..733c7a87baf 100644 --- a/packages/bsky/tests/auto-moderator/takedowns.test.ts +++ b/packages/bsky/tests/auto-moderator/takedowns.test.ts @@ -1,10 +1,9 @@ import fs from 'fs/promises' +import { TestNetwork, SeedClient, ImageRef } from '@atproto/dev-env' import { AtpAgent } from '@atproto/api' import { AutoModerator } from '../../src/auto-moderator' import IndexerContext from '../../src/indexer/context' import { sha256RawToCid } from '@atproto/common' -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' @@ -41,31 +40,31 @@ describe('takedowner', () => { autoMod = ctx.autoMod autoMod.imageFlagger = new TestFlagger() pdsAgent = new AtpAgent({ service: network.pds.url }) - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await usersSeed(sc) await network.processAll() alice = sc.dids.alice const fileBytes1 = await fs.readFile( - 'tests/image/fixtures/key-portrait-small.jpg', + 'tests/sample-img/key-portrait-small.jpg', ) const fileBytes2 = await fs.readFile( - 'tests/image/fixtures/key-portrait-large.jpg', + 'tests/sample-img/key-portrait-large.jpg', ) badCid1 = sha256RawToCid(await sha256(fileBytes1)) badCid2 = sha256RawToCid(await sha256(fileBytes2)) goodBlob = await sc.uploadFile( alice, - 'tests/image/fixtures/key-landscape-small.jpg', + 'tests/sample-img/key-landscape-small.jpg', 'image/jpeg', ) badBlob1 = await sc.uploadFile( alice, - 'tests/image/fixtures/key-portrait-small.jpg', + 'tests/sample-img/key-portrait-small.jpg', 'image/jpeg', ) badBlob2 = await sc.uploadFile( alice, - 'tests/image/fixtures/key-portrait-large.jpg', + 'tests/sample-img/key-portrait-large.jpg', 'image/jpeg', ) }) diff --git a/packages/bsky/tests/blob-resolver.test.ts b/packages/bsky/tests/blob-resolver.test.ts index fcb2b657ee5..9a4d7f55c72 100644 --- a/packages/bsky/tests/blob-resolver.test.ts +++ b/packages/bsky/tests/blob-resolver.test.ts @@ -2,7 +2,6 @@ import axios, { AxiosInstance } from 'axios' import { CID } from 'multiformats/cid' import { verifyCidForBytes } from '@atproto/common' import { TestNetwork } from '@atproto/dev-env' -import { SeedClient } from './seeds/client' import basicSeed from './seeds/basic' import { randomBytes } from '@atproto/crypto' @@ -16,8 +15,7 @@ describe('blob resolver', () => { network = await TestNetwork.create({ dbPostgresSchema: 'bsky_blob_resolver', }) - const pdsAgent = network.pds.getClient() - const sc = new SeedClient(pdsAgent) + const sc = network.getSeedClient() await basicSeed(sc) await network.processAll() await network.bsky.processAll() diff --git a/packages/bsky/tests/did-cache.test.ts b/packages/bsky/tests/did-cache.test.ts index be7544a4a3b..d0b94147bc6 100644 --- a/packages/bsky/tests/did-cache.test.ts +++ b/packages/bsky/tests/did-cache.test.ts @@ -1,6 +1,4 @@ -import AtpAgent from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' -import { SeedClient } from './seeds/client' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import userSeed from './seeds/users' import { IdResolver } from '@atproto/identity' import DidSqlCache from '../src/did-cache' @@ -23,8 +21,7 @@ describe('did cache', () => { }) idResolver = network.bsky.indexer.ctx.idResolver didCache = network.bsky.indexer.ctx.didCache - const pdsAgent = new AtpAgent({ service: network.pds.url }) - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await userSeed(sc) await network.processAll() alice = sc.dids.alice diff --git a/packages/bsky/tests/feed-generation.test.ts b/packages/bsky/tests/feed-generation.test.ts index 17df03de966..09dfd92acc8 100644 --- a/packages/bsky/tests/feed-generation.test.ts +++ b/packages/bsky/tests/feed-generation.test.ts @@ -1,8 +1,12 @@ import { TID } from '@atproto/common' import { AtUri, AtpAgent } from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' -import { TestFeedGen } from '@atproto/dev-env/src/feed-gen' -import { Handler as SkeletonHandler } from '@atproto/bsky/src/lexicon/types/app/bsky/feed/getFeedSkeleton' +import { + TestNetwork, + TestFeedGen, + SeedClient, + RecordRef, +} from '@atproto/dev-env' +import { Handler as SkeletonHandler } from '../src/lexicon/types/app/bsky/feed/getFeedSkeleton' import { GeneratorView } from '@atproto/api/src/client/types/app/bsky/feed/defs' import { UnknownFeedError } from '@atproto/api/src/client/types/app/bsky/feed/getFeed' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' @@ -11,9 +15,7 @@ import { FeedViewPost, SkeletonFeedPost, } from '../src/lexicon/types/app/bsky/feed/defs' -import { SeedClient } from './seeds/client' import basicSeed from './seeds/basic' -import { RecordRef } from './seeds/client' import { forSnapshot, paginateAll } from './_util' describe('feed generation', () => { @@ -38,7 +40,7 @@ describe('feed generation', () => { }) agent = network.bsky.getClient() pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) await network.processAll() alice = sc.dids.alice diff --git a/packages/bsky/tests/handle-invalidation.test.ts b/packages/bsky/tests/handle-invalidation.test.ts index 3b9ae789265..972f1b6cc58 100644 --- a/packages/bsky/tests/handle-invalidation.test.ts +++ b/packages/bsky/tests/handle-invalidation.test.ts @@ -1,7 +1,6 @@ import { DAY } from '@atproto/common' -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import { AtpAgent } from '@atproto/api' -import { SeedClient } from './seeds/client' import userSeed from './seeds/users' describe('handle invalidation', () => { @@ -20,7 +19,7 @@ describe('handle invalidation', () => { }) agent = network.bsky.getClient() pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await userSeed(sc) await network.processAll() diff --git a/packages/bsky/tests/image/server.test.ts b/packages/bsky/tests/image/server.test.ts index 072059b52df..3bce638ce45 100644 --- a/packages/bsky/tests/image/server.test.ts +++ b/packages/bsky/tests/image/server.test.ts @@ -1,10 +1,8 @@ import axios, { AxiosInstance } from 'axios' import { CID } from 'multiformats/cid' -import { AtpAgent } from '@atproto/api' import { cidForCbor } from '@atproto/common' import { TestNetwork } from '@atproto/dev-env' import { getInfo } from '../../src/image/sharp' -import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' import { ImageUriBuilder } from '../../src/image/uri' @@ -18,8 +16,7 @@ describe('image processing server', () => { network = await TestNetwork.create({ dbPostgresSchema: 'bsky_image_processing_server', }) - const pdsAgent = new AtpAgent({ service: network.pds.url }) - const sc = new SeedClient(pdsAgent) + const sc = network.getSeedClient() await basicSeed(sc) await network.processAll() await network.bsky.processAll() diff --git a/packages/bsky/tests/image/sharp.test.ts b/packages/bsky/tests/image/sharp.test.ts index d0a46b662b3..17b3b7f3964 100644 --- a/packages/bsky/tests/image/sharp.test.ts +++ b/packages/bsky/tests/image/sharp.test.ts @@ -178,7 +178,7 @@ describe('sharp image processor', () => { }) async function processFixture(fixture: string, options: Options) { - const image = createReadStream(`${__dirname}/fixtures/${fixture}`) + const image = createReadStream(`tests/sample-img/${fixture}`) const resized = await resize(image, options) return await getInfo(resized) } diff --git a/packages/bsky/tests/indexing.test.ts b/packages/bsky/tests/indexing.test.ts index f5c8083df09..9457544b3e5 100644 --- a/packages/bsky/tests/indexing.test.ts +++ b/packages/bsky/tests/indexing.test.ts @@ -11,9 +11,8 @@ import AtpAgent, { AppBskyFeedRepost, AppBskyGraphFollow, } from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import { forSnapshot } from './_util' -import { SeedClient } from './seeds/client' import usersSeed from './seeds/users' import basicSeed from './seeds/basic' import { ids } from '../src/lexicon/lexicons' @@ -31,7 +30,7 @@ describe('indexing', () => { }) agent = network.bsky.getClient() pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await usersSeed(sc) // Data in tests is not processed from subscription await network.processAll() diff --git a/packages/bsky/tests/moderation.test.ts b/packages/bsky/tests/moderation.test.ts index 109b576bb6f..e1af045693b 100644 --- a/packages/bsky/tests/moderation.test.ts +++ b/packages/bsky/tests/moderation.test.ts @@ -1,9 +1,8 @@ -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, ImageRef, RecordRef, SeedClient } from '@atproto/dev-env' import { TID, cidForCbor } from '@atproto/common' import AtpAgent, { ComAtprotoAdminTakeModerationAction } from '@atproto/api' import { AtUri } from '@atproto/syntax' import { forSnapshot } from './_util' -import { ImageRef, RecordRef, SeedClient } from './seeds/client' import basicSeed from './seeds/basic' import { ACKNOWLEDGE, @@ -27,8 +26,7 @@ describe('moderation', () => { dbPostgresSchema: 'bsky_moderation', }) agent = network.bsky.getClient() - const pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) await network.processAll() }) diff --git a/packages/bsky/tests/notification-server.test.ts b/packages/bsky/tests/notification-server.test.ts index aeb7f8ae97c..6f9c8b00224 100644 --- a/packages/bsky/tests/notification-server.test.ts +++ b/packages/bsky/tests/notification-server.test.ts @@ -1,6 +1,5 @@ import AtpAgent, { AtUri } from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' -import { SeedClient } from './seeds/client' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import basicSeed from './seeds/basic' import { NotificationServer } from '../src/notifications' import { Database } from '../src' @@ -21,7 +20,7 @@ describe('notification server', () => { }) agent = network.bsky.getClient() pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) await network.processAll() await network.bsky.processAll() diff --git a/packages/bsky/tests/pipeline/backpressure.test.ts b/packages/bsky/tests/pipeline/backpressure.test.ts index a265bc948c5..583d749100e 100644 --- a/packages/bsky/tests/pipeline/backpressure.test.ts +++ b/packages/bsky/tests/pipeline/backpressure.test.ts @@ -5,8 +5,8 @@ import { getIndexers, getIngester, processAll, + SeedClient, } from '@atproto/dev-env' -import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' import { BskyIngester } from '../../src' @@ -33,8 +33,7 @@ describe('pipeline backpressure', () => { name: TEST_NAME, partitionIdsByIndexer: [[0], [1]], }) - const pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) }) diff --git a/packages/bsky/tests/pipeline/reingest.test.ts b/packages/bsky/tests/pipeline/reingest.test.ts index ed8afdfe36d..3c860bcf680 100644 --- a/packages/bsky/tests/pipeline/reingest.test.ts +++ b/packages/bsky/tests/pipeline/reingest.test.ts @@ -1,5 +1,9 @@ -import { TestNetworkNoAppView, getIngester, ingestAll } from '@atproto/dev-env' -import { SeedClient } from '../seeds/client' +import { + TestNetworkNoAppView, + SeedClient, + getIngester, + ingestAll, +} from '@atproto/dev-env' import basicSeed from '../seeds/basic' import { BskyIngester } from '../../src' @@ -18,8 +22,7 @@ describe('pipeline reingestion', () => { name: TEST_NAME, ingesterPartitionCount: 1, }) - const pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) }) diff --git a/packages/bsky/tests/pipeline/repartition.test.ts b/packages/bsky/tests/pipeline/repartition.test.ts index 12205e56315..f228b954fb6 100644 --- a/packages/bsky/tests/pipeline/repartition.test.ts +++ b/packages/bsky/tests/pipeline/repartition.test.ts @@ -1,12 +1,12 @@ import { BskyIndexers, TestNetworkNoAppView, + SeedClient, getIndexers, getIngester, ingestAll, processAll, } from '@atproto/dev-env' -import { SeedClient } from '../seeds/client' import usersSeed from '../seeds/users' import { BskyIngester } from '../../src' import { countAll } from '../../src/db/util' @@ -36,8 +36,7 @@ describe('pipeline indexer repartitioning', () => { name: TEST_NAME, partitionIdsByIndexer: [[0], [1]], // two indexers, each consuming one partition }) - const pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await usersSeed(sc) }) diff --git a/packages/bsky/tests/reprocessing.test.ts b/packages/bsky/tests/reprocessing.test.ts index dd170c570ab..046bc58076b 100644 --- a/packages/bsky/tests/reprocessing.test.ts +++ b/packages/bsky/tests/reprocessing.test.ts @@ -1,14 +1,11 @@ import axios from 'axios' import { AtUri } from '@atproto/syntax' -import AtpAgent from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' -import { SeedClient } from './seeds/client' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import basicSeed from './seeds/basic' import { Database } from '../src/db' describe('reprocessing', () => { let network: TestNetwork - let pdsAgent: AtpAgent let sc: SeedClient let alice: string @@ -16,8 +13,7 @@ describe('reprocessing', () => { network = await TestNetwork.create({ dbPostgresSchema: 'bsky_reprocessing', }) - pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) alice = sc.dids.alice await network.processAll() diff --git a/packages/bsky/tests/image/fixtures/at.png b/packages/bsky/tests/sample-img/at.png similarity index 100% rename from packages/bsky/tests/image/fixtures/at.png rename to packages/bsky/tests/sample-img/at.png diff --git a/packages/bsky/tests/image/fixtures/hd-key.jpg b/packages/bsky/tests/sample-img/hd-key.jpg similarity index 100% rename from packages/bsky/tests/image/fixtures/hd-key.jpg rename to packages/bsky/tests/sample-img/hd-key.jpg diff --git a/packages/bsky/tests/image/fixtures/key-alt.jpg b/packages/bsky/tests/sample-img/key-alt.jpg similarity index 100% rename from packages/bsky/tests/image/fixtures/key-alt.jpg rename to packages/bsky/tests/sample-img/key-alt.jpg diff --git a/packages/bsky/tests/image/fixtures/key-landscape-large.jpg b/packages/bsky/tests/sample-img/key-landscape-large.jpg similarity index 100% rename from packages/bsky/tests/image/fixtures/key-landscape-large.jpg rename to packages/bsky/tests/sample-img/key-landscape-large.jpg diff --git a/packages/bsky/tests/image/fixtures/key-landscape-small.jpg b/packages/bsky/tests/sample-img/key-landscape-small.jpg similarity index 100% rename from packages/bsky/tests/image/fixtures/key-landscape-small.jpg rename to packages/bsky/tests/sample-img/key-landscape-small.jpg diff --git a/packages/bsky/tests/image/fixtures/key-portrait-large.jpg b/packages/bsky/tests/sample-img/key-portrait-large.jpg similarity index 100% rename from packages/bsky/tests/image/fixtures/key-portrait-large.jpg rename to packages/bsky/tests/sample-img/key-portrait-large.jpg diff --git a/packages/bsky/tests/image/fixtures/key-portrait-small.jpg b/packages/bsky/tests/sample-img/key-portrait-small.jpg similarity index 100% rename from packages/bsky/tests/image/fixtures/key-portrait-small.jpg rename to packages/bsky/tests/sample-img/key-portrait-small.jpg diff --git a/packages/bsky/tests/seeds/basic.ts b/packages/bsky/tests/seeds/basic.ts index c1bd7e41e09..22c6fba01c5 100644 --- a/packages/bsky/tests/seeds/basic.ts +++ b/packages/bsky/tests/seeds/basic.ts @@ -1,5 +1,5 @@ +import { SeedClient } from '@atproto/dev-env' import { ids } from '../../src/lexicon/lexicons' -import { SeedClient } from './client' import usersSeed from './users' export default async (sc: SeedClient, users = true) => { @@ -34,12 +34,12 @@ export default async (sc: SeedClient, users = true) => { }) const img1 = await sc.uploadFile( carol, - 'tests/image/fixtures/key-landscape-small.jpg', + 'tests/sample-img/key-landscape-small.jpg', 'image/jpeg', ) const img2 = await sc.uploadFile( carol, - 'tests/image/fixtures/key-alt.jpg', + 'tests/sample-img/key-alt.jpg', 'image/jpeg', ) await sc.post( @@ -100,7 +100,7 @@ export default async (sc: SeedClient, users = true) => { const replyImg = await sc.uploadFile( bob, - 'tests/image/fixtures/key-landscape-small.jpg', + 'tests/sample-img/key-landscape-small.jpg', 'image/jpeg', ) await sc.reply( diff --git a/packages/bsky/tests/seeds/client.ts b/packages/bsky/tests/seeds/client.ts deleted file mode 100644 index ee551214789..00000000000 --- a/packages/bsky/tests/seeds/client.ts +++ /dev/null @@ -1,466 +0,0 @@ -import fs from 'fs/promises' -import { CID } from 'multiformats/cid' -import AtpAgent from '@atproto/api' -import { AtUri } from '@atproto/syntax' -import { BlobRef } from '@atproto/lexicon' -import { Main as Facet } from '@atproto/api/src/client/types/app/bsky/richtext/facet' -import { InputSchema as TakeActionInput } from '@atproto/api/src/client/types/com/atproto/admin/takeModerationAction' -import { InputSchema as CreateReportInput } from '@atproto/api/src/client/types/com/atproto/moderation/createReport' -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' - -// Makes it simple to create data via the XRPC client, -// and keeps track of all created data in memory for convenience. - -let AVATAR_IMG: Uint8Array | undefined - -export type ImageRef = { - image: BlobRef - alt: string -} - -export class RecordRef { - uri: AtUri - cid: CID - - constructor(uri: AtUri | string, cid: CID | string) { - this.uri = new AtUri(uri.toString()) - this.cid = CID.parse(cid.toString()) - } - - get raw(): { uri: string; cid: string } { - return { - uri: this.uri.toString(), - cid: this.cid.toString(), - } - } - - get uriStr(): string { - return this.uri.toString() - } - - get cidStr(): string { - return this.cid.toString() - } -} - -export class SeedClient { - accounts: Record< - string, - { - did: string - accessJwt: string - refreshJwt: string - handle: string - email: string - password: string - } - > - profiles: Record< - string, - { - displayName: string - description: string - avatar: { cid: string; mimeType: string } - ref: RecordRef - } - > - follows: Record> - posts: Record< - string, - { text: string; ref: RecordRef; images: ImageRef[]; quote?: RecordRef }[] - > - likes: Record> - replies: Record - reposts: Record - lists: Record< - string, - Record }> - > - dids: Record - - constructor(public agent: AtpAgent, public adminAuth?: string) { - this.accounts = {} - this.profiles = {} - this.follows = {} - this.posts = {} - this.likes = {} - this.replies = {} - this.reposts = {} - this.lists = {} - this.dids = {} - } - - async createAccount( - shortName: string, - params: { - handle: string - email: string - password: string - }, - ) { - const { data: account } = - await this.agent.api.com.atproto.server.createAccount(params) - this.dids[shortName] = account.did - this.accounts[account.did] = { - ...account, - email: params.email, - password: params.password, - } - return this.accounts[account.did] - } - - async updateHandle(by: string, handle: string) { - await this.agent.api.com.atproto.identity.updateHandle( - { handle }, - { encoding: 'application/json', headers: this.getHeaders(by) }, - ) - } - - async createProfile( - by: string, - displayName: string, - description: string, - selfLabels?: string[], - ) { - AVATAR_IMG ??= await fs.readFile( - 'tests/image/fixtures/key-portrait-small.jpg', - ) - - let avatarBlob - { - const res = await this.agent.api.com.atproto.repo.uploadBlob(AVATAR_IMG, { - encoding: 'image/jpeg', - headers: this.getHeaders(by), - } as any) - avatarBlob = res.data.blob - } - - { - const res = await this.agent.api.app.bsky.actor.profile.create( - { repo: by }, - { - displayName, - description, - avatar: avatarBlob, - labels: selfLabels - ? { - $type: 'com.atproto.label.defs#selfLabels', - values: selfLabels.map((val) => ({ val })), - } - : undefined, - }, - this.getHeaders(by), - ) - this.profiles[by] = { - displayName, - description, - avatar: avatarBlob, - ref: new RecordRef(res.uri, res.cid), - } - } - return this.profiles[by] - } - - async follow(from: string, to: string, overrides?: Partial) { - const res = await this.agent.api.app.bsky.graph.follow.create( - { repo: from }, - { - subject: to, - createdAt: new Date().toISOString(), - ...overrides, - }, - this.getHeaders(from), - ) - this.follows[from] ??= {} - this.follows[from][to] = new RecordRef(res.uri, res.cid) - return this.follows[from][to] - } - - async unfollow(from: string, to: string) { - const follow = this.follows[from][to] - if (!follow) { - throw new Error('follow does not exist') - } - await this.agent.api.app.bsky.graph.follow.delete( - { repo: from, rkey: follow.uri.rkey }, - this.getHeaders(from), - ) - delete this.follows[from][to] - } - - async post( - by: string, - text: string, - facets?: Facet[], - images?: ImageRef[], - quote?: RecordRef, - overrides?: Partial, - ) { - const imageEmbed = images && { - $type: 'app.bsky.embed.images', - images, - } - const recordEmbed = quote && { - record: { uri: quote.uriStr, cid: quote.cidStr }, - } - const embed = - imageEmbed && recordEmbed - ? { - $type: 'app.bsky.embed.recordWithMedia', - record: recordEmbed, - media: imageEmbed, - } - : recordEmbed - ? { $type: 'app.bsky.embed.record', ...recordEmbed } - : imageEmbed - const res = await this.agent.api.app.bsky.feed.post.create( - { repo: by }, - { - text: text, - facets, - embed, - createdAt: new Date().toISOString(), - ...overrides, - }, - this.getHeaders(by), - ) - this.posts[by] ??= [] - const post = { - text, - ref: new RecordRef(res.uri, res.cid), - images: images ?? [], - quote, - } - this.posts[by].push(post) - return post - } - - async deletePost(by: string, uri: AtUri) { - await this.agent.api.app.bsky.feed.post.delete( - { - repo: by, - rkey: uri.rkey, - }, - this.getHeaders(by), - ) - } - - async uploadFile( - by: string, - filePath: string, - encoding: string, - ): Promise { - const file = await fs.readFile(filePath) - const res = await this.agent.api.com.atproto.repo.uploadBlob(file, { - headers: this.getHeaders(by), - encoding, - } as any) - return { image: res.data.blob, alt: filePath } - } - - async like(by: string, subject: RecordRef, overrides?: Partial) { - const res = await this.agent.api.app.bsky.feed.like.create( - { repo: by }, - { - subject: subject.raw, - createdAt: new Date().toISOString(), - ...overrides, - }, - this.getHeaders(by), - ) - this.likes[by] ??= {} - this.likes[by][subject.uriStr] = new AtUri(res.uri) - return this.likes[by][subject.uriStr] - } - - async reply( - by: string, - root: RecordRef, - parent: RecordRef, - text: string, - facets?: Facet[], - images?: ImageRef[], - ) { - const embed = images - ? { - $type: 'app.bsky.embed.images', - images, - } - : undefined - const res = await this.agent.api.app.bsky.feed.post.create( - { repo: by }, - { - text: text, - reply: { - root: root.raw, - parent: parent.raw, - }, - facets, - embed, - createdAt: new Date().toISOString(), - }, - this.getHeaders(by), - ) - this.replies[by] ??= [] - const reply = { - text, - ref: new RecordRef(res.uri, res.cid), - } - this.replies[by].push(reply) - return reply - } - - async repost(by: string, subject: RecordRef) { - const res = await this.agent.api.app.bsky.feed.repost.create( - { repo: by }, - { subject: subject.raw, createdAt: new Date().toISOString() }, - this.getHeaders(by), - ) - this.reposts[by] ??= [] - const repost = new RecordRef(res.uri, res.cid) - this.reposts[by].push(repost) - 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'] - reason?: string - createdBy?: string - }) { - if (!this.adminAuth) { - throw new Error('No admin auth provided to seed client') - } - const { - action, - subject, - reason = 'X', - createdBy = 'did:example:admin', - } = opts - const result = await this.agent.api.com.atproto.admin.takeModerationAction( - { action, subject, createdBy, reason }, - { - encoding: 'application/json', - headers: { authorization: this.adminAuth }, - }, - ) - return result.data - } - - async reverseModerationAction(opts: { - id: number - reason?: string - createdBy?: string - }) { - if (!this.adminAuth) { - throw new Error('No admin auth provided to seed client') - } - - const { id, reason = 'X', createdBy = 'did:example:admin' } = opts - const result = - await this.agent.api.com.atproto.admin.reverseModerationAction( - { id, reason, createdBy }, - { - encoding: 'application/json', - headers: { authorization: this.adminAuth }, - }, - ) - return result.data - } - - async resolveReports(opts: { - actionId: number - reportIds: number[] - createdBy?: string - }) { - if (!this.adminAuth) { - throw new Error('No admin auth provided to seed client') - } - - const { actionId, reportIds, createdBy = 'did:example:admin' } = opts - const result = - await this.agent.api.com.atproto.admin.resolveModerationReports( - { actionId, createdBy, reportIds }, - { - encoding: 'application/json', - headers: { authorization: this.adminAuth }, - }, - ) - return result.data - } - - async createReport(opts: { - reasonType: CreateReportInput['reasonType'] - subject: CreateReportInput['subject'] - reason?: string - reportedBy: string - }) { - const { reasonType, subject, reason, reportedBy } = opts - const result = await this.agent.api.com.atproto.moderation.createReport( - { reasonType, subject, reason }, - { - encoding: 'application/json', - headers: this.getHeaders(reportedBy), - }, - ) - return result.data - } - - getHeaders(did: string) { - return SeedClient.getHeaders(this.accounts[did].accessJwt) - } - - static getHeaders(jwt: string) { - return { authorization: `Bearer ${jwt}` } - } -} diff --git a/packages/bsky/tests/seeds/follows.ts b/packages/bsky/tests/seeds/follows.ts index f15156dbff5..1abe555ff00 100644 --- a/packages/bsky/tests/seeds/follows.ts +++ b/packages/bsky/tests/seeds/follows.ts @@ -1,4 +1,4 @@ -import { SeedClient } from './client' +import { SeedClient } from '@atproto/dev-env' export default async (sc: SeedClient) => { await sc.createAccount('alice', users.alice) diff --git a/packages/bsky/tests/seeds/likes.ts b/packages/bsky/tests/seeds/likes.ts index 1747fb2fa59..9c68375c52f 100644 --- a/packages/bsky/tests/seeds/likes.ts +++ b/packages/bsky/tests/seeds/likes.ts @@ -1,5 +1,5 @@ +import { SeedClient } from '@atproto/dev-env' import basicSeed from './basic' -import { SeedClient } from './client' export default async (sc: SeedClient) => { await basicSeed(sc) diff --git a/packages/bsky/tests/seeds/reposts.ts b/packages/bsky/tests/seeds/reposts.ts index 8de9b8ec655..9bb444ec8f2 100644 --- a/packages/bsky/tests/seeds/reposts.ts +++ b/packages/bsky/tests/seeds/reposts.ts @@ -1,5 +1,5 @@ +import { SeedClient } from '@atproto/dev-env' import basicSeed from './basic' -import { SeedClient } from './client' export default async (sc: SeedClient) => { await basicSeed(sc) diff --git a/packages/bsky/tests/seeds/users-bulk.ts b/packages/bsky/tests/seeds/users-bulk.ts index 647279c74db..c20ce85de51 100644 --- a/packages/bsky/tests/seeds/users-bulk.ts +++ b/packages/bsky/tests/seeds/users-bulk.ts @@ -1,5 +1,5 @@ +import { SeedClient } from '@atproto/dev-env' import { chunkArray } from '@atproto/common' -import { SeedClient } from './client' export default async (sc: SeedClient, max = Infinity) => { // @TODO when these are run in parallel, seem to get an intermittent diff --git a/packages/bsky/tests/seeds/users.ts b/packages/bsky/tests/seeds/users.ts index 8c14b894db4..2ed5762065a 100644 --- a/packages/bsky/tests/seeds/users.ts +++ b/packages/bsky/tests/seeds/users.ts @@ -1,4 +1,4 @@ -import { SeedClient } from './client' +import { SeedClient } from '@atproto/dev-env' export default async (sc: SeedClient) => { await sc.createAccount('alice', users.alice) diff --git a/packages/bsky/tests/server.test.ts b/packages/bsky/tests/server.test.ts index be2f1c0213e..3f54b2a37bd 100644 --- a/packages/bsky/tests/server.test.ts +++ b/packages/bsky/tests/server.test.ts @@ -4,7 +4,6 @@ import axios, { AxiosError } from 'axios' import { TestNetwork } from '@atproto/dev-env' import { handler as errorHandler } from '../src/error' import { Database } from '../src' -import { SeedClient } from './seeds/client' import basicSeed from './seeds/basic' describe('server', () => { @@ -16,8 +15,7 @@ describe('server', () => { network = await TestNetwork.create({ dbPostgresSchema: 'bsky_server', }) - const pdsAgent = network.pds.getClient() - const sc = new SeedClient(pdsAgent) + const sc = network.getSeedClient() await basicSeed(sc) await network.processAll() alice = sc.dids.alice diff --git a/packages/bsky/tests/subscription/repo.test.ts b/packages/bsky/tests/subscription/repo.test.ts index 43c1287ba95..dcdc77cd7a8 100644 --- a/packages/bsky/tests/subscription/repo.test.ts +++ b/packages/bsky/tests/subscription/repo.test.ts @@ -1,6 +1,5 @@ import AtpAgent from '@atproto/api' - -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import { CommitData } from '@atproto/repo' import { RepoService } from '@atproto/pds/src/services/repo' import { PreparedWrite } from '@atproto/pds/src/repo' @@ -11,7 +10,6 @@ import { ids } from '../../src/lexicon/lexicons' import { forSnapshot } from '../_util' import { AppContext, Database } from '../../src' import basicSeed from '../seeds/basic' -import { SeedClient } from '../seeds/client' describe('sync', () => { let network: TestNetwork @@ -25,7 +23,7 @@ describe('sync', () => { }) ctx = network.bsky.ctx pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) }) diff --git a/packages/bsky/tests/views/__snapshots__/author-feed.test.ts.snap b/packages/bsky/tests/views/__snapshots__/author-feed.test.ts.snap index 59123e54b20..a2549b0a52c 100644 --- a/packages/bsky/tests/views/__snapshots__/author-feed.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/author-feed.test.ts.snap @@ -77,7 +77,7 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/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", }, @@ -110,7 +110,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -260,7 +260,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -271,7 +271,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -486,7 +486,7 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(1)/cids(2)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(1)/cids(2)@jpeg", }, @@ -519,7 +519,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -745,12 +745,12 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(3)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(3)@jpeg", }, @@ -800,7 +800,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -811,7 +811,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1036,12 +1036,12 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(3)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(3)@jpeg", }, @@ -1091,7 +1091,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1102,7 +1102,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1224,7 +1224,7 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(5)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(5)@jpeg", }, @@ -1257,7 +1257,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1431,12 +1431,12 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(5)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(5)@jpeg", }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(8)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(8)@jpeg", }, @@ -1486,7 +1486,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1497,7 +1497,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1662,7 +1662,7 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/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", }, @@ -1695,7 +1695,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1848,7 +1848,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1859,7 +1859,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", diff --git a/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap b/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap index 5ee901c65d8..ba5c00182de 100644 --- a/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap @@ -295,7 +295,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(4)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(4)@jpeg", }, @@ -328,7 +328,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", diff --git a/packages/bsky/tests/views/__snapshots__/list-feed.test.ts.snap b/packages/bsky/tests/views/__snapshots__/list-feed.test.ts.snap index 34d5712d303..d6712c89c56 100644 --- a/packages/bsky/tests/views/__snapshots__/list-feed.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/list-feed.test.ts.snap @@ -78,7 +78,7 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/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", }, @@ -111,7 +111,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -209,7 +209,7 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/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", }, @@ -242,7 +242,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -444,7 +444,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -455,7 +455,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", 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 2585a96ec42..2824414f97b 100644 --- a/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap @@ -228,7 +228,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(6)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(6)@jpeg", }, @@ -261,7 +261,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", diff --git a/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap b/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap index fb0eb1fc5d1..ca8b664ec91 100644 --- a/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap @@ -205,7 +205,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(5)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(5)@jpeg", }, @@ -238,7 +238,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", diff --git a/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap b/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap index 5fddc479c76..bce3d4e5139 100644 --- a/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap @@ -272,7 +272,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -716,7 +716,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", diff --git a/packages/bsky/tests/views/__snapshots__/posts.test.ts.snap b/packages/bsky/tests/views/__snapshots__/posts.test.ts.snap index df8a4cdf826..3b14a184dc1 100644 --- a/packages/bsky/tests/views/__snapshots__/posts.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/posts.test.ts.snap @@ -156,12 +156,12 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(6)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(6)@jpeg", }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(7)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(7)@jpeg", }, @@ -212,7 +212,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -223,7 +223,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -286,12 +286,12 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(6)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(6)@jpeg", }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(7)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(7)@jpeg", }, @@ -342,7 +342,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -353,7 +353,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", diff --git a/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap b/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap index 4cdd3555805..6bc84753951 100644 --- a/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap @@ -73,7 +73,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/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", }, @@ -106,7 +106,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -310,7 +310,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(5)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(5)@jpeg", }, @@ -343,7 +343,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -551,7 +551,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(5)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(5)@jpeg", }, @@ -584,7 +584,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1116,7 +1116,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(4)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(4)@jpeg", }, @@ -1149,7 +1149,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1319,7 +1319,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(4)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(4)@jpeg", }, @@ -1352,7 +1352,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", diff --git a/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap b/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap index fe9b243c10a..b5863382fef 100644 --- a/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap @@ -999,12 +999,12 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(11)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(11)@jpeg", }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(12)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(12)@jpeg", }, @@ -1073,7 +1073,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1084,7 +1084,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1312,7 +1312,7 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(5)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(5)@jpeg", }, @@ -1345,7 +1345,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1515,12 +1515,12 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(5)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(5)@jpeg", }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(8)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(8)@jpeg", }, @@ -1589,7 +1589,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1600,7 +1600,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1748,7 +1748,7 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(5)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(5)@jpeg", }, @@ -1781,7 +1781,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2004,7 +2004,7 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(5)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(5)@jpeg", }, @@ -2037,7 +2037,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2241,7 +2241,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2252,7 +2252,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2448,12 +2448,12 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(5)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(5)@jpeg", }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(8)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(8)@jpeg", }, @@ -2522,7 +2522,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2533,7 +2533,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2638,12 +2638,12 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(5)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(5)@jpeg", }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(8)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(8)@jpeg", }, @@ -2712,7 +2712,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2723,7 +2723,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2897,12 +2897,12 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(3)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(3)@jpeg", }, @@ -2969,7 +2969,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2980,7 +2980,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -3127,7 +3127,7 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(2)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(2)@jpeg", }, @@ -3160,7 +3160,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -3392,7 +3392,7 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(2)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(2)@jpeg", }, @@ -3425,7 +3425,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -3638,7 +3638,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -3649,7 +3649,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -3832,12 +3832,12 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(3)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(3)@jpeg", }, @@ -3904,7 +3904,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -3915,7 +3915,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -4087,12 +4087,12 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(3)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(3)@jpeg", }, @@ -4160,7 +4160,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -4171,7 +4171,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -4320,7 +4320,7 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(2)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(2)@jpeg", }, @@ -4353,7 +4353,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -4649,7 +4649,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -4660,7 +4660,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -4814,12 +4814,12 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(3)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(3)@jpeg", }, @@ -4887,7 +4887,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -4898,7 +4898,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -5084,7 +5084,7 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(5)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(5)@jpeg", }, @@ -5117,7 +5117,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -5277,7 +5277,7 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(5)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(5)@jpeg", }, @@ -5310,7 +5310,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -5500,12 +5500,12 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(5)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(5)@jpeg", }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(9)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(9)@jpeg", }, @@ -5573,7 +5573,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", + "alt": "tests/sample-img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -5584,7 +5584,7 @@ Array [ }, }, Object { - "alt": "tests/image/fixtures/key-alt.jpg", + "alt": "tests/sample-img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", diff --git a/packages/bsky/tests/views/actor-likes.test.ts b/packages/bsky/tests/views/actor-likes.test.ts index cf0281fdde3..642b37e6446 100644 --- a/packages/bsky/tests/views/actor-likes.test.ts +++ b/packages/bsky/tests/views/actor-likes.test.ts @@ -1,6 +1,5 @@ import AtpAgent, { AtUri } from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' -import { SeedClient } from '../seeds/client' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import basicSeed from '../seeds/basic' describe('bsky actor likes feed views', () => { @@ -20,7 +19,7 @@ describe('bsky actor likes feed views', () => { }) agent = network.bsky.getClient() pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) await network.processAll() alice = sc.dids.alice diff --git a/packages/bsky/tests/views/actor-search.test.ts b/packages/bsky/tests/views/actor-search.test.ts index 77f657a9bf6..5562f747700 100644 --- a/packages/bsky/tests/views/actor-search.test.ts +++ b/packages/bsky/tests/views/actor-search.test.ts @@ -1,9 +1,8 @@ import AtpAgent from '@atproto/api' import { wait } from '@atproto/common' -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' import { forSnapshot, paginateAll, stripViewer } from '../_util' -import { SeedClient } from '../seeds/client' import usersBulkSeed from '../seeds/users-bulk' describe('pds actor search views', () => { @@ -17,8 +16,7 @@ describe('pds actor search views', () => { dbPostgresSchema: 'bsky_views_actor_search', }) agent = network.bsky.getClient() - const pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await wait(50) // allow pending sub to be established await network.bsky.ingester.sub.destroy() diff --git a/packages/bsky/tests/views/admin/repo-search.test.ts b/packages/bsky/tests/views/admin/repo-search.test.ts index ec53418eb46..6d9e8468dc1 100644 --- a/packages/bsky/tests/views/admin/repo-search.test.ts +++ b/packages/bsky/tests/views/admin/repo-search.test.ts @@ -1,7 +1,6 @@ import AtpAgent, { ComAtprotoAdminSearchRepos } from '@atproto/api' import { wait } from '@atproto/common' -import { TestNetwork } from '@atproto/dev-env' -import { SeedClient } from '../../seeds/client' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import usersBulkSeed from '../../seeds/users-bulk' describe('pds admin repo search views', () => { @@ -29,8 +28,7 @@ describe('pds admin repo search views', () => { dbPostgresSchema: 'bsky_views_repo_search', }) agent = network.bsky.getClient() - const pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await wait(100) // allow pending sub to be established await network.bsky.ingester.sub.destroy() diff --git a/packages/bsky/tests/views/author-feed.test.ts b/packages/bsky/tests/views/author-feed.test.ts index 62e0fd0826e..3d764335282 100644 --- a/packages/bsky/tests/views/author-feed.test.ts +++ b/packages/bsky/tests/views/author-feed.test.ts @@ -1,7 +1,6 @@ import AtpAgent from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import { forSnapshot, paginateAll, stripViewerFromPost } from '../_util' -import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' import { isRecord } from '../../src/lexicon/types/app/bsky/feed/post' @@ -24,8 +23,7 @@ describe('pds author feed views', () => { dbPostgresSchema: 'bsky_views_author_feed', }) agent = network.bsky.getClient() - const pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) await network.processAll() alice = sc.dids.alice diff --git a/packages/bsky/tests/views/block-lists.test.ts b/packages/bsky/tests/views/block-lists.test.ts index 0a8a223e046..d2ef0387777 100644 --- a/packages/bsky/tests/views/block-lists.test.ts +++ b/packages/bsky/tests/views/block-lists.test.ts @@ -1,9 +1,7 @@ import AtpAgent from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, SeedClient, RecordRef } from '@atproto/dev-env' import { forSnapshot } from '../_util' -import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' -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' @@ -25,7 +23,7 @@ describe('pds views with blocking from block lists', () => { }) agent = network.bsky.getClient() pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) alice = sc.dids.alice bob = sc.dids.bob @@ -52,7 +50,7 @@ describe('pds views with blocking from block lists', () => { it('creates a list with some items', async () => { const avatar = await sc.uploadFile( alice, - 'tests/image/fixtures/key-portrait-small.jpg', + 'tests/sample-img/key-portrait-small.jpg', 'image/jpeg', ) // alice creates block list with bob & carol that dan uses diff --git a/packages/bsky/tests/views/blocks.test.ts b/packages/bsky/tests/views/blocks.test.ts index 0109b93f82a..312e997cb36 100644 --- a/packages/bsky/tests/views/blocks.test.ts +++ b/packages/bsky/tests/views/blocks.test.ts @@ -1,4 +1,5 @@ import assert from 'assert' +import { TestNetwork, RecordRef, SeedClient } from '@atproto/dev-env' import AtpAgent, { AtUri } from '@atproto/api' import { BlockedActorError } from '@atproto/api/src/client/types/app/bsky/feed/getAuthorFeed' import { BlockedByActorError } from '@atproto/api/src/client/types/app/bsky/feed/getAuthorFeed' @@ -7,9 +8,7 @@ import { isViewRecord as isEmbedViewRecord, isViewBlocked as isEmbedViewBlocked, } from '@atproto/api/src/client/types/app/bsky/embed/record' -import { TestNetwork } from '@atproto/dev-env' import { forSnapshot } from '../_util' -import { RecordRef, SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' describe('pds views with blocking', () => { @@ -32,7 +31,7 @@ describe('pds views with blocking', () => { }) agent = network.bsky.getClient() pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) alice = sc.dids.alice carol = sc.dids.carol diff --git a/packages/bsky/tests/views/follows.test.ts b/packages/bsky/tests/views/follows.test.ts index e048b433b8e..3bf89ff965e 100644 --- a/packages/bsky/tests/views/follows.test.ts +++ b/packages/bsky/tests/views/follows.test.ts @@ -1,7 +1,6 @@ import AtpAgent from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import { forSnapshot, paginateAll, stripViewer } from '../_util' -import { SeedClient } from '../seeds/client' import followsSeed from '../seeds/follows' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' @@ -18,8 +17,7 @@ describe('pds follow views', () => { dbPostgresSchema: 'bsky_views_follows', }) agent = network.bsky.getClient() - const pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await followsSeed(sc) await network.processAll() await network.bsky.processAll() diff --git a/packages/bsky/tests/views/likes.test.ts b/packages/bsky/tests/views/likes.test.ts index baa2d161e11..f8f9c9a7fef 100644 --- a/packages/bsky/tests/views/likes.test.ts +++ b/packages/bsky/tests/views/likes.test.ts @@ -1,13 +1,11 @@ import AtpAgent from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' -import { SeedClient } from '../seeds/client' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import likesSeed from '../seeds/likes' import { constantDate, forSnapshot, paginateAll, stripViewer } from '../_util' describe('pds like views', () => { let network: TestNetwork let agent: AtpAgent - let pdsAgent: AtpAgent let sc: SeedClient // account dids, for convenience @@ -19,8 +17,7 @@ describe('pds like views', () => { dbPostgresSchema: 'bsky_views_likes', }) agent = network.bsky.getClient() - pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await likesSeed(sc) await network.processAll() alice = sc.dids.alice diff --git a/packages/bsky/tests/views/list-feed.test.ts b/packages/bsky/tests/views/list-feed.test.ts index c9d94f1a9d4..baef857f437 100644 --- a/packages/bsky/tests/views/list-feed.test.ts +++ b/packages/bsky/tests/views/list-feed.test.ts @@ -1,7 +1,6 @@ import AtpAgent from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, SeedClient, RecordRef } 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' @@ -22,8 +21,7 @@ describe('list feed views', () => { dbPostgresSchema: 'bsky_views_list_feed', }) agent = network.bsky.getClient() - const pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) alice = sc.dids.alice bob = sc.dids.bob diff --git a/packages/bsky/tests/views/mute-lists.test.ts b/packages/bsky/tests/views/mute-lists.test.ts index a1800ad1143..de2a047b654 100644 --- a/packages/bsky/tests/views/mute-lists.test.ts +++ b/packages/bsky/tests/views/mute-lists.test.ts @@ -1,7 +1,6 @@ import AtpAgent, { AtUri } from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, SeedClient, RecordRef } from '@atproto/dev-env' import { forSnapshot } from '../_util' -import { RecordRef, SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' describe('bsky views with mutes from mute lists', () => { @@ -21,7 +20,7 @@ describe('bsky views with mutes from mute lists', () => { }) agent = network.bsky.getClient() pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) alice = sc.dids.alice bob = sc.dids.bob @@ -43,7 +42,7 @@ describe('bsky views with mutes from mute lists', () => { it('creates a list with some items', async () => { const avatar = await sc.uploadFile( alice, - 'tests/image/fixtures/key-portrait-small.jpg', + 'tests/sample-img/key-portrait-small.jpg', 'image/jpeg', ) // alice creates mute list with bob & carol that dan uses diff --git a/packages/bsky/tests/views/mutes.test.ts b/packages/bsky/tests/views/mutes.test.ts index 15be18a7b27..6a00c427124 100644 --- a/packages/bsky/tests/views/mutes.test.ts +++ b/packages/bsky/tests/views/mutes.test.ts @@ -1,14 +1,12 @@ import AtpAgent from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import { forSnapshot, paginateAll } from '../_util' -import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' import usersBulkSeed from '../seeds/users-bulk' describe('mute views', () => { let network: TestNetwork let agent: AtpAgent - let pdsAgent: AtpAgent let sc: SeedClient let alice: string let bob: string @@ -22,8 +20,7 @@ describe('mute views', () => { dbPostgresSchema: 'bsky_views_mutes', }) agent = network.bsky.getClient() - pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) await usersBulkSeed(sc, 10) alice = sc.dids.alice diff --git a/packages/bsky/tests/views/notifications.test.ts b/packages/bsky/tests/views/notifications.test.ts index b125ffc3570..7bdd5d5f933 100644 --- a/packages/bsky/tests/views/notifications.test.ts +++ b/packages/bsky/tests/views/notifications.test.ts @@ -1,8 +1,7 @@ import AtpAgent from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' import { forSnapshot, paginateAll } from '../_util' -import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' import { Notification } from '../../src/lexicon/types/app/bsky/notification/listNotifications' @@ -19,8 +18,7 @@ describe('notification views', () => { dbPostgresSchema: 'bsky_views_notifications', }) agent = network.bsky.getClient() - const pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) await network.processAll() await network.bsky.processAll() diff --git a/packages/bsky/tests/views/posts.test.ts b/packages/bsky/tests/views/posts.test.ts index 6fa12a085df..a2710a02cf7 100644 --- a/packages/bsky/tests/views/posts.test.ts +++ b/packages/bsky/tests/views/posts.test.ts @@ -1,7 +1,6 @@ import AtpAgent, { AppBskyFeedPost } from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import { forSnapshot, stripViewerFromPost } from '../_util' -import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' describe('pds posts views', () => { @@ -16,7 +15,7 @@ describe('pds posts views', () => { }) agent = network.bsky.getClient() pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) await network.processAll() await network.bsky.processAll() diff --git a/packages/bsky/tests/views/profile.test.ts b/packages/bsky/tests/views/profile.test.ts index a1224283794..fd3bde6d0ef 100644 --- a/packages/bsky/tests/views/profile.test.ts +++ b/packages/bsky/tests/views/profile.test.ts @@ -1,10 +1,9 @@ import fs from 'fs/promises' import AtpAgent from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' import { forSnapshot, stripViewer } from '../_util' import { ids } from '../../src/lexicon/lexicons' -import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' describe('pds profile views', () => { @@ -24,7 +23,7 @@ describe('pds profile views', () => { }) agent = network.bsky.getClient() pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) await network.processAll() await network.bsky.processAll() @@ -109,10 +108,10 @@ describe('pds profile views', () => { it('presents avatars & banners', async () => { const avatarImg = await fs.readFile( - 'tests/image/fixtures/key-portrait-small.jpg', + 'tests/sample-img/key-portrait-small.jpg', ) const bannerImg = await fs.readFile( - 'tests/image/fixtures/key-landscape-small.jpg', + 'tests/sample-img/key-landscape-small.jpg', ) const avatarRes = await pdsAgent.api.com.atproto.repo.uploadBlob( avatarImg, diff --git a/packages/bsky/tests/views/reposts.test.ts b/packages/bsky/tests/views/reposts.test.ts index e401cc27a09..4d386121137 100644 --- a/packages/bsky/tests/views/reposts.test.ts +++ b/packages/bsky/tests/views/reposts.test.ts @@ -1,7 +1,6 @@ import AtpAgent from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import { forSnapshot, paginateAll, stripViewer } from '../_util' -import { SeedClient } from '../seeds/client' import repostsSeed from '../seeds/reposts' describe('pds repost views', () => { @@ -18,8 +17,7 @@ describe('pds repost views', () => { dbPostgresSchema: 'bsky_views_reposts', }) agent = network.bsky.getClient() - const pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await repostsSeed(sc) await network.processAll() alice = sc.dids.alice diff --git a/packages/bsky/tests/views/suggested-follows.test.ts b/packages/bsky/tests/views/suggested-follows.test.ts index 1d8cb5a91ba..e9aa3248df7 100644 --- a/packages/bsky/tests/views/suggested-follows.test.ts +++ b/packages/bsky/tests/views/suggested-follows.test.ts @@ -1,6 +1,5 @@ import AtpAgent, { AtUri } from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' -import { SeedClient } from '../seeds/client' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import likesSeed from '../seeds/likes' describe('suggested follows', () => { @@ -15,7 +14,7 @@ describe('suggested follows', () => { }) agent = network.bsky.getClient() pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await likesSeed(sc) await network.processAll() await network.bsky.processAll() diff --git a/packages/bsky/tests/views/suggestions.test.ts b/packages/bsky/tests/views/suggestions.test.ts index e69bd5e377e..2dcadf9e6ad 100644 --- a/packages/bsky/tests/views/suggestions.test.ts +++ b/packages/bsky/tests/views/suggestions.test.ts @@ -1,7 +1,6 @@ import AtpAgent from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import { stripViewer } from '../_util' -import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' describe('pds user search views', () => { @@ -14,8 +13,7 @@ describe('pds user search views', () => { dbPostgresSchema: 'bsky_views_suggestions', }) agent = network.bsky.getClient() - const pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) await network.processAll() await network.bsky.processAll() diff --git a/packages/bsky/tests/views/thread.test.ts b/packages/bsky/tests/views/thread.test.ts index d1c96f38603..bee609f197b 100644 --- a/packages/bsky/tests/views/thread.test.ts +++ b/packages/bsky/tests/views/thread.test.ts @@ -1,8 +1,7 @@ import AtpAgent, { AppBskyFeedGetPostThread } from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' import { forSnapshot, stripViewerFromThread } from '../_util' -import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' import assert from 'assert' import { isThreadViewPost } from '@atproto/api/src/client/types/app/bsky/feed/defs' @@ -22,8 +21,7 @@ describe('pds thread views', () => { dbPostgresSchema: 'bsky_views_thread', }) agent = network.bsky.getClient() - const pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) alice = sc.dids.alice bob = sc.dids.bob diff --git a/packages/bsky/tests/views/threadgating.test.ts b/packages/bsky/tests/views/threadgating.test.ts index 7d29addfcf5..8cfaedba44e 100644 --- a/packages/bsky/tests/views/threadgating.test.ts +++ b/packages/bsky/tests/views/threadgating.test.ts @@ -1,11 +1,10 @@ import assert from 'assert' import AtpAgent from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, SeedClient } 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' @@ -21,7 +20,7 @@ describe('views with thread gating', () => { }) agent = network.bsky.getClient() pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) await network.processAll() }) diff --git a/packages/bsky/tests/views/timeline.test.ts b/packages/bsky/tests/views/timeline.test.ts index e7db746c7f3..9cd3f688e33 100644 --- a/packages/bsky/tests/views/timeline.test.ts +++ b/packages/bsky/tests/views/timeline.test.ts @@ -1,9 +1,8 @@ import assert from 'assert' import AtpAgent from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' import { forSnapshot, getOriginator, paginateAll } from '../_util' -import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' import { FeedAlgorithm } from '../../src/api/app/bsky/util/feed' import { FeedViewPost } from '../../src/lexicon/types/app/bsky/feed/defs' @@ -24,8 +23,7 @@ describe('timeline views', () => { dbPostgresSchema: 'bsky_views_home_feed', }) agent = network.bsky.getClient() - const pdsAgent = network.pds.getClient() - sc = new SeedClient(pdsAgent) + sc = network.getSeedClient() await basicSeed(sc) await network.processAll() alice = sc.dids.alice diff --git a/packages/dev-env/package.json b/packages/dev-env/package.json index 41d37004223..28a13b69b1d 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -30,6 +30,7 @@ "@atproto/common-web": "workspace:^", "@atproto/crypto": "workspace:^", "@atproto/identity": "workspace:^", + "@atproto/lexicon": "workspace:^", "@atproto/pds": "workspace:^", "@atproto/syntax": "workspace:^", "@atproto/xrpc-server": "workspace:^", @@ -40,6 +41,7 @@ "dotenv": "^16.0.3", "express": "^4.18.2", "get-port": "^6.1.2", + "multiformats": "^9.9.0", "sharp": "^0.31.2", "uint8arrays": "3.0.0" }, diff --git a/packages/dev-env/src/index.ts b/packages/dev-env/src/index.ts index 18406c2cbba..160247f9fbb 100644 --- a/packages/dev-env/src/index.ts +++ b/packages/dev-env/src/index.ts @@ -3,5 +3,7 @@ export * from './network' export * from './network-no-appview' export * from './pds' export * from './plc' +export * from './feed-gen' +export * from './seed-client' export * from './types' export * from './util' diff --git a/packages/dev-env/src/network-no-appview.ts b/packages/dev-env/src/network-no-appview.ts index f69da964ee3..25054b2ab4e 100644 --- a/packages/dev-env/src/network-no-appview.ts +++ b/packages/dev-env/src/network-no-appview.ts @@ -4,6 +4,7 @@ import { TestPlc } from './plc' import { TestPds } from './pds' import { mockNetworkUtilities } from './util' import { TestFeedGen } from './feed-gen' +import { SeedClient } from './seed-client' export class TestNetworkNoAppView { feedGens: TestFeedGen[] = [] @@ -40,6 +41,11 @@ export class TestNetworkNoAppView { return fg } + getSeedClient(): SeedClient { + const agent = this.pds.getClient() + return new SeedClient(this, agent) + } + async processAll() { await this.pds.processAll() } diff --git a/packages/pds/tests/seeds/client.ts b/packages/dev-env/src/seed-client.ts similarity index 97% rename from packages/pds/tests/seeds/client.ts rename to packages/dev-env/src/seed-client.ts index 767ecdc18f1..b9b1eded96a 100644 --- a/packages/pds/tests/seeds/client.ts +++ b/packages/dev-env/src/seed-client.ts @@ -9,8 +9,7 @@ import { Record as LikeRecord } from '@atproto/api/src/client/types/app/bsky/fee import { Record as FollowRecord } from '@atproto/api/src/client/types/app/bsky/graph/follow' import { AtUri } from '@atproto/syntax' import { BlobRef } from '@atproto/lexicon' -import { adminAuth } from '../_util' -import { ids } from '../../src/lexicon/lexicons' +import { TestNetworkNoAppView } from './network-no-appview' // Makes it simple to create data via the XRPC client, // and keeps track of all created data in memory for convenience. @@ -83,7 +82,7 @@ export class SeedClient { > dids: Record - constructor(public agent: AtpAgent) { + constructor(public network: TestNetworkNoAppView, public agent: AtpAgent) { this.accounts = {} this.profiles = {} this.follows = {} @@ -170,7 +169,7 @@ export class SeedClient { const res = await this.agent.api.com.atproto.repo.putRecord( { repo: by, - collection: ids.AppBskyActorProfile, + collection: 'app.bsky.actor.profile', rkey: 'self', record, }, @@ -436,7 +435,7 @@ export class SeedClient { { action, subject, createdBy, reason }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: this.adminAuthHeaders(), }, ) return result.data @@ -453,7 +452,7 @@ export class SeedClient { { id, reason, createdBy }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: this.adminAuthHeaders(), }, ) return result.data @@ -470,7 +469,7 @@ export class SeedClient { { actionId, createdBy, reportIds }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: this.adminAuthHeaders(), }, ) return result.data @@ -493,6 +492,10 @@ export class SeedClient { return result.data } + adminAuthHeaders() { + return this.network.pds.adminAuthHeaders() + } + getHeaders(did: string) { return SeedClient.getHeaders(this.accounts[did].accessJwt) } diff --git a/packages/dev-env/src/util.ts b/packages/dev-env/src/util.ts index 268fad4a034..7e3f275ca98 100644 --- a/packages/dev-env/src/util.ts +++ b/packages/dev-env/src/util.ts @@ -28,11 +28,14 @@ export const mockResolvers = (idResolver: IdResolver, pds: TestPds) => { return result } + const origResolveHandleDns = idResolver.handle.resolveDns idResolver.handle.resolve = async (handle: string) => { const isPdsHandle = pds.ctx.cfg.identity.serviceHandleDomains.some( (domain) => handle.endsWith(domain), ) - if (!isPdsHandle) return undefined + if (!isPdsHandle) { + return origResolveHandleDns.call(idResolver.handle, handle) + } const url = `${pds.url}/.well-known/atproto-did` try { diff --git a/packages/pds/bench/sequencer.bench.ts b/packages/pds/bench/sequencer.bench.ts index 00c3e2c21c4..b7b054e9d8a 100644 --- a/packages/pds/bench/sequencer.bench.ts +++ b/packages/pds/bench/sequencer.bench.ts @@ -1,37 +1,38 @@ import { randomBytes } from '@atproto/crypto' import { cborEncode } from '@atproto/common' -import { TestServerInfo, runTestServer } from '../tests/_util' import { randomCid } from '@atproto/repo/tests/_util' import { BlockMap, blocksToCarFile } from '@atproto/repo' import { byFrame } from '@atproto/xrpc-server' import { WebSocket } from 'ws' import { Database } from '../src' +import { TestNetworkNoAppView } from '@atproto/dev-env' describe('sequencer bench', () => { - let server: TestServerInfo + let network: TestNetworkNoAppView let db: Database beforeAll(async () => { - server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'sequencer_bench', - maxSubscriptionBuffer: 20000, + pds: { + maxSubscriptionBuffer: 20000, + }, }) - if (!server.ctx.cfg.dbPostgresUrl) { + if (network.pds.ctx.cfg.db.dialect !== 'pg') { throw new Error('no postgres url') } db = Database.postgres({ - url: server.ctx.cfg.dbPostgresUrl, - schema: server.ctx.cfg.dbPostgresSchema, - txLockNonce: server.ctx.cfg.dbTxLockNonce, + url: network.pds.ctx.cfg.db.url, + schema: network.pds.ctx.cfg.db.schema, poolSize: 50, }) - server.ctx.sequencerLeader?.destroy() + network.pds.ctx.sequencerLeader?.destroy() }) afterAll(async () => { - await server.close() + await network.close() }) const doWrites = async (batches: number, batchSize: number) => { @@ -78,7 +79,7 @@ describe('sequencer bench', () => { totalToRead: number, cursor?: number, ): Promise => { - const serverHost = server.url.replace('http://', '') + const serverHost = network.pds.url.replace('http://', '') let url = `ws://${serverHost}/xrpc/com.atproto.sync.subscribeRepos` if (cursor !== undefined) { url += `?cursor=${cursor}` @@ -114,7 +115,7 @@ describe('sequencer bench', () => { await doWrites(BATCHES, BATCH_SIZE) const setup = Date.now() - await server.ctx.sequencerLeader?.sequenceOutgoing() + await network.pds.ctx.sequencerLeader?.sequenceOutgoing() const sequencingTime = Date.now() - setup const liveTailTime = await readAllPromise diff --git a/packages/pds/package.json b/packages/pds/package.json index ac8e316a9dc..4d966941f8a 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -84,6 +84,7 @@ "@types/pg": "^8.6.6", "@types/qs": "^6.9.7", "@types/sharp": "^0.31.0", - "axios": "^0.27.2" + "axios": "^0.27.2", + "ws": "^8.12.0" } } diff --git a/packages/pds/tests/_util.ts b/packages/pds/tests/_util.ts index 79f9a1c8da8..5624ac9a65a 100644 --- a/packages/pds/tests/_util.ts +++ b/packages/pds/tests/_util.ts @@ -1,151 +1,7 @@ -import { AddressInfo } from 'net' -import os from 'os' -import path from 'path' -import getPort from 'get-port' -import * as crypto from '@atproto/crypto' -import { PlcServer, Database as PlcDatabase } from '@did-plc/server' import { AtUri } from '@atproto/syntax' -import { randomStr } from '@atproto/crypto' -import { uniqueLockId } from '@atproto/dev-env' import { CID } from 'multiformats/cid' -import * as ui8 from 'uint8arrays' -import { PDS, Database } from '../src' import { FeedViewPost } from '../src/lexicon/types/app/bsky/feed/defs' -import AppContext from '../src/context' import { lexToJson } from '@atproto/lexicon' -import { ServerEnvironment, envToCfg, envToSecrets } from '../src/config' - -const ADMIN_PASSWORD = 'admin-pass' -const MODERATOR_PASSWORD = 'moderator-pass' -const TRIAGE_PASSWORD = 'triage-pass' - -export type CloseFn = () => Promise -export type TestServerInfo = { - url: string - ctx: AppContext - close: CloseFn - processAll: () => Promise -} - -export type TestServerOpts = { - migration?: string -} - -export const runTestServer = async ( - params: Partial = {}, - opts: TestServerOpts = {}, -): Promise => { - const dbPostgresUrl = params.dbPostgresUrl || process.env.DB_POSTGRES_URL - - // run plc server - let plcDb - if (dbPostgresUrl !== undefined) { - plcDb = PlcDatabase.postgres({ - url: dbPostgresUrl, - schema: `plc_test_${params.dbPostgresSchema}`, - }) - await plcDb.migrateToLatestOrThrow() - } else { - plcDb = PlcDatabase.mock() - } - const plcServer = PlcServer.create({ db: plcDb }) - const plcListener = await plcServer.start() - const plcPort = (plcListener.address() as AddressInfo).port - const plcUrl = `http://localhost:${plcPort}` - - const repoSigningKey = await crypto.Secp256k1Keypair.create({ - exportable: true, - }) - const repoSigningPriv = ui8.toString(await repoSigningKey.export(), 'hex') - const plcRotationKey = await crypto.Secp256k1Keypair.create({ - exportable: true, - }) - const plcRotationPriv = ui8.toString(await plcRotationKey.export(), 'hex') - const recoveryKey = (await crypto.Secp256k1Keypair.create()).did() - - const blobstoreLoc = path.join(os.tmpdir(), randomStr(5, 'base32')) - - const port = await getPort() - - const env: ServerEnvironment = { - port, - dbPostgresUrl: dbPostgresUrl, - dbSqliteLocation: dbPostgresUrl ? undefined : ':memory:', - blobstoreDiskLocation: blobstoreLoc, - recoveryDidKey: recoveryKey, - didPlcUrl: plcUrl, - serviceHandleDomains: ['.test'], - sequencerLeaderLockId: uniqueLockId(), - bskyAppViewUrl: 'https://appview.invalid', - bskyAppViewDid: 'did:example:invalid', - repoSigningKeyK256PrivateKeyHex: repoSigningPriv, - plcRotationKeyK256PrivateKeyHex: plcRotationPriv, - adminPassword: ADMIN_PASSWORD, - moderatorPassword: MODERATOR_PASSWORD, - jwtSecret: 'jwt-secret', - inviteRequired: false, - inviteEpoch: Date.now(), - triagePassword: TRIAGE_PASSWORD, - ...params, - } - - const cfg = envToCfg(env) - const secrets = envToSecrets(env) - - const pds = await PDS.create(cfg, secrets) - - // Separate migration db on postgres in case migration changes some - // connection state that we need in the tests, e.g. "alter database ... set ..." - const migrationDb = - cfg.db.dialect === 'pg' - ? Database.postgres({ - url: cfg.db.url, - schema: cfg.db.schema, - }) - : pds.ctx.db - if (opts.migration) { - await migrationDb.migrateToOrThrow(opts.migration) - } else { - await migrationDb.migrateToLatestOrThrow() - } - if (migrationDb !== pds.ctx.db) { - await migrationDb.close() - } - - const pdsServer = await pds.start() - const pdsPort = (pdsServer.address() as AddressInfo).port - - return { - url: `http://localhost:${pdsPort}`, - ctx: pds.ctx, - close: async () => { - await pds.destroy() - await plcServer.destroy() - }, - processAll: async () => { - await pds.ctx.backgroundQueue.processAll() - }, - } -} - -export const adminAuth = () => { - return basicAuth('admin', ADMIN_PASSWORD) -} - -export const moderatorAuth = () => { - return basicAuth('admin', MODERATOR_PASSWORD) -} - -export const triageAuth = () => { - return basicAuth('admin', TRIAGE_PASSWORD) -} - -const basicAuth = (username: string, password: string) => { - return ( - 'Basic ' + - ui8.toString(ui8.fromString(`${username}:${password}`, 'utf8'), 'base64pad') - ) -} // Swap out identifiers and dates with stable // values for the purpose of snapshot testing diff --git a/packages/pds/tests/account-deletion.test.ts b/packages/pds/tests/account-deletion.test.ts index ad09ca02bee..54f1de60c70 100644 --- a/packages/pds/tests/account-deletion.test.ts +++ b/packages/pds/tests/account-deletion.test.ts @@ -1,11 +1,10 @@ +import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' import { once, EventEmitter } from 'events' import { Selectable } from 'kysely' import Mail from 'nodemailer/lib/mailer' import AtpAgent from '@atproto/api' -import { SeedClient } from './seeds/client' import basicSeed from './seeds/basic' import { Database } from '../src' -import * as util from './_util' import { ServerMailer } from '../src/mailer' import { BlobNotFoundError, BlobStore } from '@atproto/repo' import { RepoRoot } from '../src/db/tables/repo-root' @@ -18,9 +17,8 @@ import { RepoSeq } from '../src/db/tables/repo-seq' import { ACKNOWLEDGE } from '../src/lexicon/types/com/atproto/admin/defs' describe('account deletion', () => { - let server: util.TestServerInfo + let network: TestNetworkNoAppView let agent: AtpAgent - let close: util.CloseFn let sc: SeedClient let mailer: ServerMailer @@ -35,15 +33,14 @@ describe('account deletion', () => { let carol beforeAll(async () => { - server = await util.runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'account_deletion', }) - close = server.close - mailer = server.ctx.mailer - db = server.ctx.db - blobstore = server.ctx.blobstore - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) + mailer = network.pds.ctx.mailer + db = network.pds.ctx.db + blobstore = network.pds.ctx.blobstore + agent = new AtpAgent({ service: network.pds.url }) + sc = network.getSeedClient() await basicSeed(sc) carol = sc.accounts[sc.dids.carol] @@ -60,9 +57,7 @@ describe('account deletion', () => { afterAll(async () => { mailer.transporter.sendMail = _origSendMail - if (close) { - await close() - } + await network.close() }) const getMailFrom = async (promise): Promise => { @@ -91,7 +86,6 @@ describe('account deletion', () => { return expect(token).toBeDefined() } }) - return it('fails account deletion with a bad token', async () => { const attempt = agent.api.com.atproto.server.deleteAccount({ @@ -125,7 +119,7 @@ describe('account deletion', () => { }, { encoding: 'application/json', - headers: { authorization: util.adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) await agent.api.com.atproto.server.deleteAccount({ @@ -133,7 +127,7 @@ describe('account deletion', () => { did: carol.did, password: carol.password, }) - await server.processAll() // Finish background hard-deletions + await network.processAll() // Finish background hard-deletions }) it('no longer lets the user log in', async () => { diff --git a/packages/pds/tests/account.test.ts b/packages/pds/tests/account.test.ts index 6af50704e10..f157380a1c1 100644 --- a/packages/pds/tests/account.test.ts +++ b/packages/pds/tests/account.test.ts @@ -2,9 +2,9 @@ import { once, EventEmitter } from 'events' import AtpAgent, { ComAtprotoServerResetPassword } from '@atproto/api' import { IdResolver } from '@atproto/identity' import * as crypto from '@atproto/crypto' +import { TestNetworkNoAppView } from '@atproto/dev-env' import Mail from 'nodemailer/lib/mailer' import { AppContext, Database } from '../src' -import * as util from './_util' import { ServerMailer } from '../src/mailer' const email = 'alice@test.com' @@ -14,11 +14,10 @@ const passwordAlt = 'test456' const minsToMs = 60 * 1000 describe('account', () => { - let serverUrl: string + let network: TestNetworkNoAppView let ctx: AppContext let repoSigningKey: string let agent: AtpAgent - let close: util.CloseFn let mailer: ServerMailer let db: Database let idResolver: IdResolver @@ -26,20 +25,19 @@ describe('account', () => { let _origSendMail beforeAll(async () => { - const server = await util.runTestServer({ - hostname: 'pds.public.url', - termsOfServiceUrl: 'https://example.com/tos', - privacyPolicyUrl: 'https://example.com/privacy-policy', + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'account', + pds: { + termsOfServiceUrl: 'https://example.com/tos', + privacyPolicyUrl: 'https://example.com/privacy-policy', + }, }) - close = server.close - mailer = server.ctx.mailer - db = server.ctx.db - ctx = server.ctx - serverUrl = server.url - repoSigningKey = server.ctx.repoSigningKey.did() - idResolver = server.ctx.idResolver - agent = new AtpAgent({ service: serverUrl }) + mailer = network.pds.ctx.mailer + db = network.pds.ctx.db + ctx = network.pds.ctx + repoSigningKey = network.pds.ctx.repoSigningKey.did() + idResolver = network.pds.ctx.idResolver + agent = network.pds.getClient() // Catch emails for use in tests _origSendMail = mailer.transporter.sendMail @@ -52,9 +50,7 @@ describe('account', () => { afterAll(async () => { mailer.transporter.sendMail = _origSendMail - if (close) { - await close() - } + await network.close() }) it('serves the accounts system config', async () => { @@ -122,7 +118,7 @@ describe('account', () => { expect(didData.did).toBe(did) expect(didData.handle).toBe(handle) expect(didData.signingKey).toBe(repoSigningKey) - expect(didData.pds).toBe('https://pds.public.url') // Mapped from publicUrl + expect(didData.pds).toBe(network.pds.url) }) it('allows a custom set recovery key', async () => { @@ -154,7 +150,7 @@ describe('account', () => { ctx.cfg.identity.recoveryDidKey ?? '', ctx.plcRotationKey.did(), ], - pds: ctx.cfg.service.publicUrl, + pds: network.pds.url, signer: userKey, }) @@ -245,7 +241,7 @@ describe('account', () => { }, { encoding: 'application/json', - headers: { authorization: util.adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) @@ -259,7 +255,7 @@ describe('account', () => { }, { encoding: 'application/json', - headers: { authorization: util.adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) @@ -275,7 +271,7 @@ describe('account', () => { }, { encoding: 'application/json', - headers: { authorization: util.moderatorAuth() }, + headers: network.pds.adminAuthHeaders('moderator'), }, ) await expect(attemptUpdateMod).rejects.toThrow('Insufficient privileges') @@ -286,7 +282,7 @@ describe('account', () => { }, { encoding: 'application/json', - headers: { authorization: util.triageAuth() }, + headers: network.pds.adminAuthHeaders('triage'), }, ) await expect(attemptUpdateTriage).rejects.toThrow('Insufficient privileges') diff --git a/packages/pds/tests/admin/get-moderation-action.test.ts b/packages/pds/tests/admin/get-moderation-action.test.ts index 54b86984416..11a64799db3 100644 --- a/packages/pds/tests/admin/get-moderation-action.test.ts +++ b/packages/pds/tests/admin/get-moderation-action.test.ts @@ -1,3 +1,4 @@ +import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' import AtpAgent from '@atproto/api' import { FLAG, @@ -7,27 +8,25 @@ import { REASONOTHER, REASONSPAM, } from '../../src/lexicon/types/com/atproto/moderation/defs' -import { runTestServer, forSnapshot, CloseFn, adminAuth } from '../_util' -import { SeedClient } from '../seeds/client' +import { forSnapshot } from '../_util' import basicSeed from '../seeds/basic' describe('pds admin get moderation action view', () => { + let network: TestNetworkNoAppView let agent: AtpAgent - let close: CloseFn let sc: SeedClient beforeAll(async () => { - const server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'views_admin_get_moderation_action', }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) + agent = network.pds.getClient() + sc = network.getSeedClient() await basicSeed(sc) }) afterAll(async () => { - await close() + await network.close() }) beforeAll(async () => { @@ -79,7 +78,7 @@ describe('pds admin get moderation action view', () => { // id 2 because id 1 is in seed client const result = await agent.api.com.atproto.admin.getModerationAction( { id: 2 }, - { headers: { authorization: adminAuth() } }, + { headers: { authorization: network.pds.adminAuth() } }, ) expect(forSnapshot(result.data)).toMatchSnapshot() }) @@ -88,7 +87,7 @@ describe('pds admin get moderation action view', () => { // id 3 because id 1 is in seed client const result = await agent.api.com.atproto.admin.getModerationAction( { id: 3 }, - { headers: { authorization: adminAuth() } }, + { headers: { authorization: network.pds.adminAuth() } }, ) expect(forSnapshot(result.data)).toMatchSnapshot() }) @@ -96,7 +95,7 @@ describe('pds admin get moderation action view', () => { it('fails when moderation action does not exist.', async () => { const promise = agent.api.com.atproto.admin.getModerationAction( { id: 100 }, - { headers: { authorization: adminAuth() } }, + { headers: { authorization: network.pds.adminAuth() } }, ) await expect(promise).rejects.toThrow('Action not found') }) diff --git a/packages/pds/tests/admin/get-moderation-actions.test.ts b/packages/pds/tests/admin/get-moderation-actions.test.ts index 1ad5e066c60..01a934c32e0 100644 --- a/packages/pds/tests/admin/get-moderation-actions.test.ts +++ b/packages/pds/tests/admin/get-moderation-actions.test.ts @@ -1,3 +1,4 @@ +import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' import AtpAgent from '@atproto/api' import { ACKNOWLEDGE, @@ -8,33 +9,25 @@ import { REASONOTHER, REASONSPAM, } from '../../src/lexicon/types/com/atproto/moderation/defs' -import { - runTestServer, - forSnapshot, - CloseFn, - adminAuth, - paginateAll, -} from '../_util' -import { SeedClient } from '../seeds/client' +import { forSnapshot, paginateAll } from '../_util' import basicSeed from '../seeds/basic' describe('pds admin get moderation actions view', () => { + let network: TestNetworkNoAppView let agent: AtpAgent - let close: CloseFn let sc: SeedClient beforeAll(async () => { - const server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'views_admin_get_moderation_actions', }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) + agent = network.pds.getClient() + sc = network.getSeedClient() await basicSeed(sc) }) afterAll(async () => { - await close() + await network.close() }) beforeAll(async () => { @@ -124,7 +117,7 @@ describe('pds admin get moderation actions view', () => { it('gets all moderation actions.', async () => { const result = await agent.api.com.atproto.admin.getModerationActions( {}, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect(forSnapshot(result.data.actions)).toMatchSnapshot() }) @@ -132,7 +125,7 @@ describe('pds admin get moderation actions view', () => { it('gets all moderation actions for a repo.', async () => { const result = await agent.api.com.atproto.admin.getModerationActions( { subject: Object.values(sc.dids)[0] }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect(forSnapshot(result.data.actions)).toMatchSnapshot() }) @@ -140,7 +133,7 @@ describe('pds admin get moderation actions view', () => { it('gets all moderation actions for a record.', async () => { const result = await agent.api.com.atproto.admin.getModerationActions( { subject: Object.values(sc.posts)[0][0].ref.uriStr }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect(forSnapshot(result.data.actions)).toMatchSnapshot() }) @@ -150,7 +143,7 @@ describe('pds admin get moderation actions view', () => { const paginator = async (cursor?: string) => { const res = await agent.api.com.atproto.admin.getModerationActions( { cursor, limit: 3 }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) return res.data } @@ -162,7 +155,7 @@ describe('pds admin get moderation actions view', () => { const full = await agent.api.com.atproto.admin.getModerationActions( {}, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect(full.data.actions.length).toEqual(7) // extra one because of seed client diff --git a/packages/pds/tests/admin/get-moderation-report.test.ts b/packages/pds/tests/admin/get-moderation-report.test.ts index 7d433539b37..714596e352f 100644 --- a/packages/pds/tests/admin/get-moderation-report.test.ts +++ b/packages/pds/tests/admin/get-moderation-report.test.ts @@ -1,3 +1,4 @@ +import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' import AtpAgent from '@atproto/api' import { FLAG, @@ -7,27 +8,25 @@ import { REASONOTHER, REASONSPAM, } from '../../src/lexicon/types/com/atproto/moderation/defs' -import { runTestServer, forSnapshot, CloseFn, adminAuth } from '../_util' -import { SeedClient } from '../seeds/client' +import { forSnapshot } from '../_util' import basicSeed from '../seeds/basic' describe('pds admin get moderation action view', () => { + let network: TestNetworkNoAppView let agent: AtpAgent - let close: CloseFn let sc: SeedClient beforeAll(async () => { - const server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'views_admin_get_moderation_report', }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) + agent = network.pds.getClient() + sc = network.getSeedClient() await basicSeed(sc) }) afterAll(async () => { - await close() + await network.close() }) beforeAll(async () => { @@ -78,7 +77,7 @@ describe('pds admin get moderation action view', () => { it('gets moderation report for a repo.', async () => { const result = await agent.api.com.atproto.admin.getModerationReport( { id: 1 }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect(forSnapshot(result.data)).toMatchSnapshot() }) @@ -86,7 +85,7 @@ describe('pds admin get moderation action view', () => { it('gets moderation report for a record.', async () => { const result = await agent.api.com.atproto.admin.getModerationReport( { id: 2 }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect(forSnapshot(result.data)).toMatchSnapshot() }) @@ -94,7 +93,7 @@ describe('pds admin get moderation action view', () => { it('fails when moderation report does not exist.', async () => { const promise = agent.api.com.atproto.admin.getModerationReport( { id: 100 }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) await expect(promise).rejects.toThrow('Report not found') }) diff --git a/packages/pds/tests/admin/get-moderation-reports.test.ts b/packages/pds/tests/admin/get-moderation-reports.test.ts index 20f1c97f781..aac3560c048 100644 --- a/packages/pds/tests/admin/get-moderation-reports.test.ts +++ b/packages/pds/tests/admin/get-moderation-reports.test.ts @@ -1,3 +1,4 @@ +import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' import AtpAgent from '@atproto/api' import { ACKNOWLEDGE, @@ -8,33 +9,25 @@ import { REASONOTHER, REASONSPAM, } from '../../src/lexicon/types/com/atproto/moderation/defs' -import { - runTestServer, - forSnapshot, - CloseFn, - adminAuth, - paginateAll, -} from '../_util' -import { SeedClient } from '../seeds/client' +import { forSnapshot, paginateAll } from '../_util' import basicSeed from '../seeds/basic' describe('pds admin get moderation reports view', () => { + let network: TestNetworkNoAppView let agent: AtpAgent - let close: CloseFn let sc: SeedClient beforeAll(async () => { - const server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'views_admin_get_moderation_reports', }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) + agent = network.pds.getClient() + sc = network.getSeedClient() await basicSeed(sc) }) afterAll(async () => { - await close() + await network.close() }) beforeAll(async () => { @@ -132,7 +125,7 @@ describe('pds admin get moderation reports view', () => { const allReports = await agent.api.com.atproto.admin.getModerationReports( {}, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) const ignoreSubjects = getDids(allReports).slice(0, 2) @@ -140,7 +133,7 @@ describe('pds admin get moderation reports view', () => { const filteredReportsByDid = await agent.api.com.atproto.admin.getModerationReports( { ignoreSubjects }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) // Validate that when ignored by DID, all reports for that DID is ignored @@ -159,7 +152,7 @@ describe('pds admin get moderation reports view', () => { { ignoreSubjects: ignoredAtUriSubjects, }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) // Validate that when ignored by at uri, only the reports for that at uri is ignored @@ -176,7 +169,7 @@ describe('pds admin get moderation reports view', () => { it('gets all moderation reports.', async () => { const result = await agent.api.com.atproto.admin.getModerationReports( {}, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect(forSnapshot(result.data.reports)).toMatchSnapshot() }) @@ -184,7 +177,7 @@ describe('pds admin get moderation reports view', () => { it('gets all moderation reports for a repo.', async () => { const result = await agent.api.com.atproto.admin.getModerationReports( { subject: Object.values(sc.dids)[0] }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect(forSnapshot(result.data.reports)).toMatchSnapshot() }) @@ -192,7 +185,7 @@ describe('pds admin get moderation reports view', () => { it('gets all moderation reports for a record.', async () => { const result = await agent.api.com.atproto.admin.getModerationReports( { subject: Object.values(sc.posts)[0][0].ref.uriStr }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect(forSnapshot(result.data.reports)).toMatchSnapshot() }) @@ -200,12 +193,12 @@ describe('pds admin get moderation reports view', () => { it('gets all resolved/unresolved moderation reports.', async () => { const resolved = await agent.api.com.atproto.admin.getModerationReports( { resolved: true }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect(forSnapshot(resolved.data.reports)).toMatchSnapshot() const unresolved = await agent.api.com.atproto.admin.getModerationReports( { resolved: false }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect(forSnapshot(unresolved.data.reports)).toMatchSnapshot() }) @@ -221,11 +214,11 @@ describe('pds admin get moderation reports view', () => { ] = await Promise.all([ agent.api.com.atproto.admin.getModerationReports( { reverse: true }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ), agent.api.com.atproto.admin.getModerationReports( {}, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ), ]) @@ -237,7 +230,7 @@ describe('pds admin get moderation reports view', () => { const reportsWithTakedown = await agent.api.com.atproto.admin.getModerationReports( { actionType: TAKEDOWN }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect(forSnapshot(reportsWithTakedown.data.reports)).toMatchSnapshot() }) @@ -248,21 +241,21 @@ describe('pds admin get moderation reports view', () => { const [actionedByAdminOne, actionedByAdminTwo] = await Promise.all([ agent.api.com.atproto.admin.getModerationReports( { actionedBy: adminDidOne }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ), agent.api.com.atproto.admin.getModerationReports( { actionedBy: adminDidTwo }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ), ]) const [fullReportOne, fullReportTwo] = await Promise.all([ agent.api.com.atproto.admin.getModerationReport( { id: actionedByAdminOne.data.reports[0].id }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ), agent.api.com.atproto.admin.getModerationReport( { id: actionedByAdminTwo.data.reports[0].id }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ), ]) @@ -281,7 +274,7 @@ describe('pds admin get moderation reports view', () => { const paginator = async (cursor?: string) => { const res = await agent.api.com.atproto.admin.getModerationReports( { cursor, limit: 3 }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) return res.data } @@ -293,7 +286,7 @@ describe('pds admin get moderation reports view', () => { const full = await agent.api.com.atproto.admin.getModerationReports( {}, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect(full.data.reports.length).toEqual(6) @@ -306,7 +299,7 @@ describe('pds admin get moderation reports view', () => { async (cursor?: string) => { const res = await agent.api.com.atproto.admin.getModerationReports( { cursor, limit: 3, reverse }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) return res.data } @@ -326,7 +319,7 @@ describe('pds admin get moderation reports view', () => { it('filters reports by reporter DID.', async () => { const result = await agent.api.com.atproto.admin.getModerationReports( { reporters: [sc.dids.alice] }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) const reporterDidsFromReports = [ diff --git a/packages/pds/tests/admin/get-record.test.ts b/packages/pds/tests/admin/get-record.test.ts index 6c38419612e..350709971fc 100644 --- a/packages/pds/tests/admin/get-record.test.ts +++ b/packages/pds/tests/admin/get-record.test.ts @@ -1,3 +1,4 @@ +import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' import AtpAgent from '@atproto/api' import { AtUri } from '@atproto/syntax' import { @@ -8,34 +9,25 @@ import { REASONOTHER, REASONSPAM, } from '../../src/lexicon/types/com/atproto/moderation/defs' -import { - runTestServer, - forSnapshot, - CloseFn, - adminAuth, - TestServerInfo, -} from '../_util' -import { SeedClient } from '../seeds/client' +import { forSnapshot } from '../_util' import basicSeed from '../seeds/basic' describe('pds admin get record view', () => { - let server: TestServerInfo + let network: TestNetworkNoAppView let agent: AtpAgent - let close: CloseFn let sc: SeedClient beforeAll(async () => { - server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'views_admin_get_record', }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) + agent = network.pds.getClient() + sc = network.getSeedClient() await basicSeed(sc) }) afterAll(async () => { - await close() + await network.close() }) beforeAll(async () => { @@ -80,7 +72,7 @@ describe('pds admin get record view', () => { it('gets a record by uri, even when taken down.', async () => { const result = await agent.api.com.atproto.admin.getRecord( { uri: sc.posts[sc.dids.alice][0].ref.uriStr }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect(forSnapshot(result.data)).toMatchSnapshot() }) @@ -91,7 +83,7 @@ describe('pds admin get record view', () => { uri: sc.posts[sc.dids.alice][0].ref.uriStr, cid: sc.posts[sc.dids.alice][0].ref.cidStr, }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect(forSnapshot(result.data)).toMatchSnapshot() }) @@ -105,7 +97,7 @@ describe('pds admin get record view', () => { 'badrkey', ).toString(), }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) await expect(promise).rejects.toThrow('Record not found') }) @@ -116,7 +108,7 @@ describe('pds admin get record view', () => { uri: sc.posts[sc.dids.alice][0].ref.uriStr, cid: sc.posts[sc.dids.alice][1].ref.cidStr, // Mismatching cid }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) await expect(promise).rejects.toThrow('Record not found') }) diff --git a/packages/pds/tests/admin/get-repo.test.ts b/packages/pds/tests/admin/get-repo.test.ts index 9cd38ae101f..9467643973e 100644 --- a/packages/pds/tests/admin/get-repo.test.ts +++ b/packages/pds/tests/admin/get-repo.test.ts @@ -1,3 +1,4 @@ +import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' import AtpAgent from '@atproto/api' import { ACKNOWLEDGE, @@ -7,36 +8,25 @@ import { REASONOTHER, REASONSPAM, } from '../../src/lexicon/types/com/atproto/moderation/defs' -import { - runTestServer, - forSnapshot, - CloseFn, - adminAuth, - TestServerInfo, - moderatorAuth, - triageAuth, -} from '../_util' -import { SeedClient } from '../seeds/client' +import { forSnapshot } from '../_util' import basicSeed from '../seeds/basic' describe('pds admin get repo view', () => { - let server: TestServerInfo + let network: TestNetworkNoAppView let agent: AtpAgent - let close: CloseFn let sc: SeedClient beforeAll(async () => { - server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'views_admin_get_repo', }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) + agent = network.pds.getClient() + sc = network.getSeedClient() await basicSeed(sc) }) afterAll(async () => { - await close() + await network.close() }) beforeAll(async () => { @@ -77,7 +67,7 @@ describe('pds admin get repo view', () => { it('gets a repo by did, even when taken down.', async () => { const result = await agent.api.com.atproto.admin.getRepo( { did: sc.dids.alice }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect(forSnapshot(result.data)).toMatchSnapshot() }) @@ -85,15 +75,15 @@ describe('pds admin get repo view', () => { it('does not include account emails for triage mods.', async () => { const { data: admin } = await agent.api.com.atproto.admin.getRepo( { did: sc.dids.bob }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) const { data: moderator } = await agent.api.com.atproto.admin.getRepo( { did: sc.dids.bob }, - { headers: { authorization: moderatorAuth() } }, + { headers: network.pds.adminAuthHeaders('moderator') }, ) const { data: triage } = await agent.api.com.atproto.admin.getRepo( { did: sc.dids.bob }, - { headers: { authorization: triageAuth() } }, + { headers: network.pds.adminAuthHeaders('triage') }, ) expect(admin.email).toEqual('bob@test.com') expect(moderator.email).toEqual('bob@test.com') @@ -104,7 +94,7 @@ describe('pds admin get repo view', () => { it('fails when repo does not exist.', async () => { const promise = agent.api.com.atproto.admin.getRepo( { did: 'did:plc:doesnotexist' }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) await expect(promise).rejects.toThrow('Repo not found') }) diff --git a/packages/pds/tests/admin/invites.test.ts b/packages/pds/tests/admin/invites.test.ts index a64c473783a..4f52400a314 100644 --- a/packages/pds/tests/admin/invites.test.ts +++ b/packages/pds/tests/admin/invites.test.ts @@ -1,30 +1,26 @@ +import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' import AtpAgent from '@atproto/api' -import { - runTestServer, - adminAuth, - moderatorAuth, - TestServerInfo, -} from '../_util' import { randomStr } from '@atproto/crypto' -import { SeedClient } from '../seeds/client' describe('pds admin invite views', () => { + let network: TestNetworkNoAppView let agent: AtpAgent let sc: SeedClient - let server: TestServerInfo beforeAll(async () => { - server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'views_admin_invites', - inviteRequired: true, - inviteInterval: 1, + pds: { + inviteRequired: true, + inviteInterval: 1, + }, }) - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) + agent = network.pds.getClient() + sc = network.getSeedClient() }) afterAll(async () => { - await server.close() + await network.close() }) let alice: string @@ -34,7 +30,7 @@ describe('pds admin invite views', () => { beforeAll(async () => { const adminCode = await agent.api.com.atproto.server.createInviteCode( { useCount: 10 }, - { encoding: 'application/json', headers: { authorization: adminAuth() } }, + { encoding: 'application/json', headers: network.pds.adminAuthHeaders() }, ) await sc.createAccount('alice', { @@ -70,11 +66,11 @@ describe('pds admin invite views', () => { ) await agent.api.com.atproto.server.createInviteCode( { useCount: 5, forAccount: alice }, - { encoding: 'application/json', headers: { authorization: adminAuth() } }, + { encoding: 'application/json', headers: network.pds.adminAuthHeaders() }, ) await agent.api.com.atproto.admin.disableInviteCodes( { codes: [adminCode.data.code], accounts: [bob] }, - { encoding: 'application/json', headers: { authorization: adminAuth() } }, + { encoding: 'application/json', headers: network.pds.adminAuthHeaders() }, ) const useCode = async (code: string) => { @@ -94,7 +90,7 @@ describe('pds admin invite views', () => { it('gets a list of invite codes by recency', async () => { const result = await agent.api.com.atproto.admin.getInviteCodes( {}, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) let lastDate = result.data.codes[0].createdAt for (const code of result.data.codes) { @@ -121,15 +117,15 @@ describe('pds admin invite views', () => { it('paginates by recency', async () => { const full = await agent.api.com.atproto.admin.getInviteCodes( {}, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) const first = await agent.api.com.atproto.admin.getInviteCodes( { limit: 5 }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) const second = await agent.api.com.atproto.admin.getInviteCodes( { cursor: first.data.cursor }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) const combined = [...first.data.codes, ...second.data.codes] expect(combined).toEqual(full.data.codes) @@ -138,7 +134,7 @@ describe('pds admin invite views', () => { it('gets a list of invite codes by usage', async () => { const result = await agent.api.com.atproto.admin.getInviteCodes( { sort: 'usage' }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) let lastUseCount = result.data.codes[0].uses.length for (const code of result.data.codes) { @@ -157,15 +153,15 @@ describe('pds admin invite views', () => { it('paginates by usage', async () => { const full = await agent.api.com.atproto.admin.getInviteCodes( { sort: 'usage' }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) const first = await agent.api.com.atproto.admin.getInviteCodes( { sort: 'usage', limit: 5 }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) const second = await agent.api.com.atproto.admin.getInviteCodes( { sort: 'usage', cursor: first.data.cursor }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) const combined = [...first.data.codes, ...second.data.codes] expect(combined).toEqual(full.data.codes) @@ -174,7 +170,7 @@ describe('pds admin invite views', () => { it('filters admin.searchRepos by invitedBy', async () => { const searchView = await agent.api.com.atproto.admin.searchRepos( { invitedBy: alice }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect(searchView.data.repos.length).toBe(2) expect(searchView.data.repos[0].invitedBy?.available).toBe(1) @@ -186,7 +182,7 @@ describe('pds admin invite views', () => { it('hydrates invites into admin.getRepo', async () => { const aliceView = await agent.api.com.atproto.admin.getRepo( { did: alice }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect(aliceView.data.invitedBy?.available).toBe(10) expect(aliceView.data.invitedBy?.uses.length).toBe(3) @@ -199,7 +195,7 @@ describe('pds admin invite views', () => { { codes: ['x'], accounts: [alice] }, { encoding: 'application/json', - headers: { authorization: moderatorAuth() }, + headers: network.pds.adminAuthHeaders('moderator'), }, ) await expect(attemptDisableInvites).rejects.toThrow( @@ -212,7 +208,7 @@ describe('pds admin invite views', () => { { useCount: 5, forAccount: alice }, { encoding: 'application/json', - headers: { authorization: moderatorAuth() }, + headers: network.pds.adminAuthHeaders('moderator'), }, ) await expect(attemptCreateInvite).rejects.toThrow('Insufficient privileges') @@ -222,12 +218,12 @@ describe('pds admin invite views', () => { const reasonForDisabling = 'User is selling invites' await agent.api.com.atproto.admin.disableAccountInvites( { account: carol, note: reasonForDisabling }, - { encoding: 'application/json', headers: { authorization: adminAuth() } }, + { encoding: 'application/json', headers: network.pds.adminAuthHeaders() }, ) const repoRes = await agent.api.com.atproto.admin.getRepo( { did: carol }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect(repoRes.data.invitesDisabled).toBe(true) expect(repoRes.data.inviteNote).toBe(reasonForDisabling) @@ -244,31 +240,31 @@ describe('pds admin invite views', () => { const reasonForDisabling = 'User is selling invites' await agent.api.com.atproto.admin.enableAccountInvites( { account: carol, note: reasonForEnabling }, - { encoding: 'application/json', headers: { authorization: adminAuth() } }, + { encoding: 'application/json', headers: network.pds.adminAuthHeaders() }, ) const afterEnable = await agent.api.com.atproto.admin.getRepo( { did: carol }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect(afterEnable.data.invitesDisabled).toBe(false) expect(afterEnable.data.inviteNote).toBe(reasonForEnabling) await agent.api.com.atproto.admin.disableAccountInvites( { account: carol, note: reasonForDisabling }, - { encoding: 'application/json', headers: { authorization: adminAuth() } }, + { encoding: 'application/json', headers: network.pds.adminAuthHeaders() }, ) const afterDisable = await agent.api.com.atproto.admin.getRepo( { did: carol }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect(afterDisable.data.invitesDisabled).toBe(true) expect(afterDisable.data.inviteNote).toBe(reasonForDisabling) }) it('creates codes in the background but disables them', async () => { - const res = await server.ctx.db.db + const res = await network.pds.ctx.db.db .selectFrom('invite_code') .where('forUser', '=', carol) .selectAll() @@ -282,7 +278,7 @@ describe('pds admin invite views', () => { { account: alice }, { encoding: 'application/json', - headers: { authorization: moderatorAuth() }, + headers: network.pds.adminAuthHeaders('moderator'), }, ) await expect(attempt).rejects.toThrow('Insufficient privileges') @@ -291,12 +287,12 @@ describe('pds admin invite views', () => { it('re-enables an accounts invites', async () => { await agent.api.com.atproto.admin.enableAccountInvites( { account: carol }, - { encoding: 'application/json', headers: { authorization: adminAuth() } }, + { encoding: 'application/json', headers: network.pds.adminAuthHeaders() }, ) const repoRes = await agent.api.com.atproto.admin.getRepo( { did: carol }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect(repoRes.data.invitesDisabled).toBe(false) @@ -312,7 +308,7 @@ describe('pds admin invite views', () => { { account: alice }, { encoding: 'application/json', - headers: { authorization: moderatorAuth() }, + headers: network.pds.adminAuthHeaders('moderator'), }, ) await expect(attempt).rejects.toThrow('Insufficient privileges') diff --git a/packages/pds/tests/admin/moderation.test.ts b/packages/pds/tests/admin/moderation.test.ts index 2e06cea5ed4..c65812adfed 100644 --- a/packages/pds/tests/admin/moderation.test.ts +++ b/packages/pds/tests/admin/moderation.test.ts @@ -1,17 +1,14 @@ +import { + TestNetworkNoAppView, + ImageRef, + RecordRef, + SeedClient, +} from '@atproto/dev-env' import AtpAgent from '@atproto/api' import { AtUri } from '@atproto/syntax' import { BlobNotFoundError } from '@atproto/repo' -import { - adminAuth, - CloseFn, - forSnapshot, - moderatorAuth, - runTestServer, - TestServerInfo, - triageAuth, -} from '../_util' +import { forSnapshot } from '../_util' import { PeriodicModerationActionReversal } from '../../src/db/periodic-moderation-action-reversal' -import { ImageRef, RecordRef, SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' import { ACKNOWLEDGE, @@ -25,23 +22,21 @@ import { } from '../../src/lexicon/types/com/atproto/moderation/defs' describe('moderation', () => { - 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: 'moderation', }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) + agent = network.pds.getClient() + sc = network.getSeedClient() await basicSeed(sc) }) afterAll(async () => { - await close() + await network.close() }) describe('reporting', () => { @@ -210,7 +205,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) const { data: actionResolvedReports } = @@ -222,7 +217,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) @@ -237,7 +232,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) }) @@ -285,7 +280,7 @@ describe('moderation', () => { await agent.api.com.atproto.server.requestAccountDelete(undefined, { headers: sc.getHeaders(deleteme.did), }) - const { token: deletionToken } = await server.ctx.db.db + const { token: deletionToken } = await network.pds.ctx.db.db .selectFrom('email_token') .where('purpose', '=', 'delete_account') .where('did', '=', deleteme.did) @@ -296,7 +291,7 @@ describe('moderation', () => { password: 'password', token: deletionToken, }) - await server.processAll() + await network.processAll() // Take action on deleted content const { data: action } = await agent.api.com.atproto.admin.takeModerationAction( @@ -312,7 +307,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) await agent.api.com.atproto.admin.resolveModerationReports( @@ -323,29 +318,29 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) // Check report and action details const { data: repoDeletionActionDetail } = await agent.api.com.atproto.admin.getModerationAction( { id: action.id - 1 }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) const { data: recordActionDetail } = await agent.api.com.atproto.admin.getModerationAction( { id: action.id }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) const { data: reportADetail } = await agent.api.com.atproto.admin.getModerationReport( { id: reportA.id }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) const { data: reportBDetail } = await agent.api.com.atproto.admin.getModerationReport( { id: reportB.id }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) expect( forSnapshot({ @@ -364,7 +359,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) }) @@ -397,7 +392,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) @@ -409,7 +404,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) @@ -426,7 +421,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) }) @@ -463,7 +458,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) @@ -475,7 +470,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) @@ -492,7 +487,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) }) @@ -514,7 +509,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: triageAuth() }, + headers: network.pds.adminAuthHeaders('triage'), }, ) expect(action1).toEqual( @@ -541,7 +536,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: triageAuth() }, + headers: network.pds.adminAuthHeaders('triage'), }, ) expect(action2).toEqual( @@ -563,7 +558,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: triageAuth() }, + headers: network.pds.adminAuthHeaders('triage'), }, ) await agent.api.com.atproto.admin.reverseModerationAction( @@ -574,7 +569,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: triageAuth() }, + headers: network.pds.adminAuthHeaders('triage'), }, ) }) @@ -595,7 +590,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) const flagPromise = agent.api.com.atproto.admin.takeModerationAction( @@ -611,7 +606,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) await expect(flagPromise).rejects.toThrow( @@ -627,7 +622,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) const { data: flag } = @@ -644,7 +639,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) @@ -657,7 +652,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) }) @@ -676,7 +671,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) const flagPromise = agent.api.com.atproto.admin.takeModerationAction( @@ -691,7 +686,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) await expect(flagPromise).rejects.toThrow( @@ -707,7 +702,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) const { data: flag } = @@ -723,7 +718,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) @@ -736,7 +731,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) }) @@ -760,7 +755,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) const flagPromise = agent.api.com.atproto.admin.takeModerationAction( @@ -777,7 +772,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) await expect(flagPromise).rejects.toThrow( @@ -792,7 +787,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) const { data: flag } = @@ -810,7 +805,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) @@ -823,7 +818,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) }) @@ -842,7 +837,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: moderatorAuth() }, + headers: network.pds.adminAuthHeaders('moderator'), }, ) // cleanup @@ -854,7 +849,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) }) @@ -876,18 +871,20 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: moderatorAuth() }, + headers: network.pds.adminAuthHeaders('moderator'), }, ) // In the actual app, this will be instantiated and run on server startup - const periodicReversal = new PeriodicModerationActionReversal(server.ctx) + const periodicReversal = new PeriodicModerationActionReversal( + network.pds.ctx, + ) await periodicReversal.findAndRevertDueActions() const { data: reversedAction } = await agent.api.com.atproto.admin.getModerationAction( { id: action.id }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) // Verify that the automatic reversal is attributed to the original moderator of the temporary action @@ -912,7 +909,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: triageAuth() }, + headers: network.pds.adminAuthHeaders('triage'), }, ) await expect(attemptTakedownTriage).rejects.toThrow( @@ -943,14 +940,14 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) actionId = takeAction.data.id }) it('removes blob from the store', async () => { - const tryGetBytes = server.ctx.blobstore.getBytes(blob.image.ref) + const tryGetBytes = network.pds.ctx.blobstore.getBytes(blob.image.ref) await expect(tryGetBytes).rejects.toThrow(BlobNotFoundError) }) @@ -982,7 +979,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) diff --git a/packages/pds/tests/admin/repo-search.test.ts b/packages/pds/tests/admin/repo-search.test.ts index e3fcdef2d80..b95dde6063d 100644 --- a/packages/pds/tests/admin/repo-search.test.ts +++ b/packages/pds/tests/admin/repo-search.test.ts @@ -1,28 +1,27 @@ +import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' import AtpAgent from '@atproto/api' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' -import { runTestServer, CloseFn, paginateAll, adminAuth } from '../_util' -import { SeedClient } from '../seeds/client' +import { paginateAll } from '../_util' import usersBulkSeed from '../seeds/users-bulk' describe('pds admin repo search view', () => { + let network: TestNetworkNoAppView let agent: AtpAgent - let close: CloseFn let sc: SeedClient let headers: { [s: string]: string } beforeAll(async () => { - const server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'views_admin_repo_search', }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) + agent = network.pds.getClient() + sc = network.getSeedClient() await usersBulkSeed(sc) - headers = { authorization: adminAuth() } + headers = network.pds.adminAuthHeaders() }) afterAll(async () => { - await close() + await network.close() }) beforeAll(async () => { diff --git a/packages/pds/tests/app-passwords.test.ts b/packages/pds/tests/app-passwords.test.ts index c67b335bef7..c8e1309dda8 100644 --- a/packages/pds/tests/app-passwords.test.ts +++ b/packages/pds/tests/app-passwords.test.ts @@ -1,20 +1,18 @@ +import { TestNetworkNoAppView } from '@atproto/dev-env' import AtpAgent from '@atproto/api' import * as jwt from 'jsonwebtoken' -import { CloseFn, runTestServer, TestServerInfo } from './_util' describe('app_passwords', () => { - let server: TestServerInfo + let network: TestNetworkNoAppView let accntAgent: AtpAgent let appAgent: AtpAgent - let close: CloseFn beforeAll(async () => { - server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'app_passwords', }) - accntAgent = new AtpAgent({ service: server.url }) - appAgent = new AtpAgent({ service: server.url }) - close = server.close + accntAgent = network.pds.getClient() + appAgent = network.pds.getClient() await accntAgent.createAccount({ handle: 'alice.test', @@ -24,7 +22,7 @@ describe('app_passwords', () => { }) afterAll(async () => { - await close() + await network.close() }) let appPass: string @@ -128,7 +126,7 @@ describe('app_passwords', () => { }) it('no longer allows session creation after revocation', async () => { - const newAgent = new AtpAgent({ service: server.url }) + const newAgent = network.pds.getClient() const attempt = newAgent.login({ identifier: 'alice.test', password: appPass, diff --git a/packages/pds/tests/auth.test.ts b/packages/pds/tests/auth.test.ts index ae78f3d5619..d94eebf17e1 100644 --- a/packages/pds/tests/auth.test.ts +++ b/packages/pds/tests/auth.test.ts @@ -1,26 +1,23 @@ -import AtpAgent from '@atproto/api' import * as jwt from 'jsonwebtoken' +import AtpAgent from '@atproto/api' +import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' import * as CreateSession from '@atproto/api/src/client/types/com/atproto/server/createSession' import * as RefreshSession from '@atproto/api/src/client/types/com/atproto/server/refreshSession' -import { SeedClient } from './seeds/client' -import { adminAuth, CloseFn, runTestServer, TestServerInfo } from './_util' describe('auth', () => { - let server: TestServerInfo + let network: TestNetworkNoAppView let agent: AtpAgent - let close: CloseFn beforeAll(async () => { - server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'auth', }) - agent = new AtpAgent({ service: server.url }) - close = server.close + agent = network.pds.getClient() }) afterAll(async () => { - await close() + await network.close() }) const createAccount = async (info) => { @@ -173,7 +170,7 @@ describe('auth', () => { }) it('refresh token is revoked after grace period completes.', async () => { - const { db } = server.ctx + const { db } = network.pds.ctx const account = await createAccount({ handle: 'evan.test', email: 'evan@test.com', @@ -228,7 +225,7 @@ describe('auth', () => { }) it('expired refresh token cannot be used to refresh a session.', async () => { - const { auth } = server.ctx + const { auth } = network.pds.ctx const account = await createAccount({ handle: 'holga.test', email: 'holga@test.com', @@ -258,7 +255,7 @@ describe('auth', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: { authorization: network.pds.adminAuth() }, }, ) await expect( @@ -284,7 +281,7 @@ describe('auth', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: { authorization: network.pds.adminAuth() }, }, ) await expect(refreshSession(account.refreshJwt)).rejects.toThrow( diff --git a/packages/pds/tests/blob-deletes.test.ts b/packages/pds/tests/blob-deletes.test.ts index 8ec97139d63..bf7f36c256c 100644 --- a/packages/pds/tests/blob-deletes.test.ts +++ b/packages/pds/tests/blob-deletes.test.ts @@ -1,12 +1,11 @@ +import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' import AtpAgent, { BlobRef } from '@atproto/api' -import { runTestServer, TestServerInfo } from './_util' import { Database } from '../src' import DiskBlobStore from '../src/storage/disk-blobstore' import { ids } from '../src/lexicon/lexicons' -import { SeedClient } from './seeds/client' describe('blob deletes', () => { - let server: TestServerInfo + let network: TestNetworkNoAppView let agent: AtpAgent let sc: SeedClient @@ -17,13 +16,13 @@ describe('blob deletes', () => { let bob: string beforeAll(async () => { - server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'blob_deletes', }) - blobstore = server.ctx.blobstore as DiskBlobStore - db = server.ctx.db - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) + blobstore = network.pds.ctx.blobstore as DiskBlobStore + db = network.pds.ctx.db + agent = network.pds.getClient() + sc = network.getSeedClient() await sc.createAccount('alice', { email: 'alice@test.com', handle: 'alice.test', @@ -39,7 +38,7 @@ describe('blob deletes', () => { }) afterAll(async () => { - await server.close() + await network.close() }) const getDbBlobsForDid = (did: string) => { @@ -58,7 +57,7 @@ describe('blob deletes', () => { ) const post = await sc.post(alice, 'test', undefined, [img]) await sc.deletePost(alice, post.ref.uri) - await server.processAll() + await network.processAll() const dbBlobs = await getDbBlobsForDid(alice) expect(dbBlobs.length).toBe(0) @@ -80,7 +79,7 @@ describe('blob deletes', () => { ) await updateProfile(sc, alice, img.image, img.image) await updateProfile(sc, alice, img2.image, img2.image) - await server.processAll() + await network.processAll() const dbBlobs = await getDbBlobsForDid(alice) expect(dbBlobs.length).toBe(1) @@ -109,7 +108,7 @@ describe('blob deletes', () => { ) await updateProfile(sc, alice, img.image, img.image) await updateProfile(sc, alice, img.image, img2.image) - await server.processAll() + await network.processAll() const dbBlobs = await getDbBlobsForDid(alice) expect(dbBlobs.length).toBe(2) @@ -160,7 +159,7 @@ describe('blob deletes', () => { }, { encoding: 'application/json', headers: sc.getHeaders(alice) }, ) - await server.processAll() + await network.processAll() const dbBlobs = await getDbBlobsForDid(alice) expect(dbBlobs.length).toBe(1) diff --git a/packages/pds/tests/create-post.test.ts b/packages/pds/tests/create-post.test.ts index 3a96d0f15a7..81d813b9aeb 100644 --- a/packages/pds/tests/create-post.test.ts +++ b/packages/pds/tests/create-post.test.ts @@ -1,30 +1,29 @@ +import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' import AtpAgent, { AppBskyFeedPost, AtUri, RichText, AppBskyRichtextFacet, } 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 network: TestNetworkNoAppView let agent: AtpAgent let sc: SeedClient beforeAll(async () => { - server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'views_posts', }) - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) + agent = network.pds.getClient() + sc = network.getSeedClient() await basicSeed(sc) - await server.processAll() + await network.processAll() }) afterAll(async () => { - await server.close() + await network.close() }) it('allows for creating posts with tags', async () => { diff --git a/packages/pds/tests/crud.test.ts b/packages/pds/tests/crud.test.ts index 00dc3a623fa..c0902e2db29 100644 --- a/packages/pds/tests/crud.test.ts +++ b/packages/pds/tests/crud.test.ts @@ -1,6 +1,8 @@ import fs from 'fs/promises' import { AtUri } from '@atproto/syntax' import AtpAgent from '@atproto/api' +import { BlobRef } from '@atproto/lexicon' +import { TestNetworkNoAppView } from '@atproto/dev-env' import * as createRecord from '@atproto/api/src/client/types/com/atproto/repo/createRecord' import * as putRecord from '@atproto/api/src/client/types/com/atproto/repo/putRecord' import * as deleteRecord from '@atproto/api/src/client/types/com/atproto/repo/deleteRecord' @@ -9,10 +11,9 @@ import { cidForCbor, TID, ui8ToArrayBuffer } from '@atproto/common' import { BlobNotFoundError } from '@atproto/repo' import { defaultFetchHandler } from '@atproto/xrpc' import * as Post from '../src/lexicon/types/app/bsky/feed/post' -import { adminAuth, CloseFn, paginateAll, runTestServer } from './_util' +import { paginateAll } from './_util' import AppContext from '../src/context' import { TAKEDOWN } from '../src/lexicon/types/com/atproto/admin/defs' -import { BlobRef } from '@atproto/lexicon' import { ids } from '../src/lexicon/lexicons' const alice = { @@ -29,25 +30,24 @@ const bob = { } describe('crud operations', () => { + let network: TestNetworkNoAppView let ctx: AppContext let agent: AtpAgent let aliceAgent: AtpAgent let bobAgent: AtpAgent - let close: CloseFn beforeAll(async () => { - const server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'crud', }) - ctx = server.ctx - close = server.close - agent = new AtpAgent({ service: server.url }) - aliceAgent = new AtpAgent({ service: server.url }) - bobAgent = new AtpAgent({ service: server.url }) + ctx = network.pds.ctx + agent = network.pds.getClient() + aliceAgent = network.pds.getClient() + bobAgent = network.pds.getClient() }) afterAll(async () => { - await close() + await network.close() }) it('registers users', async () => { @@ -1168,7 +1168,7 @@ describe('crud operations', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: { authorization: network.pds.adminAuth() }, }, ) @@ -1191,7 +1191,7 @@ describe('crud operations', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: { authorization: network.pds.adminAuth() }, }, ) }) @@ -1213,7 +1213,7 @@ describe('crud operations', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: { authorization: network.pds.adminAuth() }, }, ) @@ -1231,7 +1231,7 @@ describe('crud operations', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: { authorization: network.pds.adminAuth() }, }, ) }) diff --git a/packages/pds/tests/db.test.ts b/packages/pds/tests/db.test.ts index 6e4192cfac8..1a2a42f0930 100644 --- a/packages/pds/tests/db.test.ts +++ b/packages/pds/tests/db.test.ts @@ -1,26 +1,23 @@ import { sql } from 'kysely' import { once } from 'events' +import { TestNetworkNoAppView } from '@atproto/dev-env' import { createDeferrable, wait } from '@atproto/common' import { Database } from '../src' import { Leader, appMigration } from '../src/db/leader' -import { runTestServer, CloseFn } from './_util' describe('db', () => { - let close: CloseFn + let network: TestNetworkNoAppView let db: Database beforeAll(async () => { - const server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'db', }) - close = server.close - db = server.ctx.db + db = network.pds.ctx.db }) afterAll(async () => { - if (close) { - await close() - } + await network.close() }) describe('transaction()', () => { diff --git a/packages/pds/tests/email-confirmation.test.ts b/packages/pds/tests/email-confirmation.test.ts index fc3c4caadcd..d6d4213986f 100644 --- a/packages/pds/tests/email-confirmation.test.ts +++ b/packages/pds/tests/email-confirmation.test.ts @@ -1,10 +1,9 @@ import { once, EventEmitter } from 'events' import Mail from 'nodemailer/lib/mailer' import AtpAgent from '@atproto/api' -import { SeedClient } from './seeds/client' +import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' import userSeed from './seeds/users' import { ServerMailer } from '../src/mailer' -import { TestNetworkNoAppView } from '@atproto/dev-env' import { ComAtprotoServerConfirmEmail, ComAtprotoServerUpdateEmail, @@ -27,7 +26,7 @@ describe('email confirmation', () => { }) mailer = network.pds.ctx.mailer agent = network.pds.getClient() - sc = new SeedClient(agent) + sc = network.getSeedClient() await userSeed(sc) alice = sc.accounts[sc.dids.alice] diff --git a/packages/pds/tests/file-uploads.test.ts b/packages/pds/tests/file-uploads.test.ts index 097b797103e..07b4c6ebb55 100644 --- a/packages/pds/tests/file-uploads.test.ts +++ b/packages/pds/tests/file-uploads.test.ts @@ -1,13 +1,13 @@ import fs from 'fs/promises' import { gzipSync } from 'zlib' import AtpAgent from '@atproto/api' -import { CloseFn, runTestServer, TestServerInfo } from './_util' -import { Database, ServerConfig } from '../src' +import { Database } from '../src' import DiskBlobStore from '../src/storage/disk-blobstore' import * as uint8arrays from 'uint8arrays' import { randomBytes } from '@atproto/crypto' import { BlobRef } from '@atproto/lexicon' import { ids } from '../src/lexicon/lexicons' +import { TestNetworkNoAppView } from '@atproto/dev-env' const alice = { email: 'alice@test.com', @@ -23,30 +23,24 @@ const bob = { } describe('file uploads', () => { - let server: TestServerInfo + let network: TestNetworkNoAppView let aliceAgent: AtpAgent let bobAgent: AtpAgent let blobstore: DiskBlobStore let db: Database - let cfg: ServerConfig - let serverUrl: string - let close: CloseFn beforeAll(async () => { - server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'file_uploads', }) - blobstore = server.ctx.blobstore as DiskBlobStore - db = server.ctx.db - close = server.close - aliceAgent = new AtpAgent({ service: server.url }) - bobAgent = new AtpAgent({ service: server.url }) - cfg = server.ctx.cfg - serverUrl = server.url + blobstore = network.pds.ctx.blobstore as DiskBlobStore + db = network.pds.ctx.db + aliceAgent = network.pds.getClient() + bobAgent = network.pds.getClient() }) afterAll(async () => { - await close() + await network.close() }) it('registers users', async () => { @@ -69,24 +63,27 @@ describe('file uploads', () => { it('handles client abort', async () => { const abortController = new AbortController() - const _putTemp = server.ctx.blobstore.putTemp - server.ctx.blobstore.putTemp = function (...args) { + const _putTemp = network.pds.ctx.blobstore.putTemp + network.pds.ctx.blobstore.putTemp = function (...args) { // Abort just as processing blob in packages/pds/src/services/repo/blobs.ts process.nextTick(() => abortController.abort()) return _putTemp.call(this, ...args) } - const response = fetch(`${server.url}/xrpc/com.atproto.repo.uploadBlob`, { - method: 'post', - body: Buffer.alloc(5000000), // Enough bytes to get some chunking going on - signal: abortController.signal, - headers: { - 'content-type': 'image/jpeg', - authorization: `Bearer ${aliceAgent.session?.accessJwt}`, + const response = fetch( + `${network.pds.url}/xrpc/com.atproto.repo.uploadBlob`, + { + method: 'post', + body: Buffer.alloc(5000000), // Enough bytes to get some chunking going on + signal: abortController.signal, + headers: { + 'content-type': 'image/jpeg', + authorization: `Bearer ${aliceAgent.session?.accessJwt}`, + }, }, - }) + ) await expect(response).rejects.toThrow('operation was aborted') // Cleanup - server.ctx.blobstore.putTemp = _putTemp + network.pds.ctx.blobstore.putTemp = _putTemp // This test would fail from an uncaught exception: this grace period gives time for that to surface await new Promise((res) => setTimeout(res, 10)) }) diff --git a/packages/pds/tests/handles.test.ts b/packages/pds/tests/handles.test.ts index e9b03e3deb9..7c6833bdb78 100644 --- a/packages/pds/tests/handles.test.ts +++ b/packages/pds/tests/handles.test.ts @@ -1,8 +1,7 @@ +import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' import AtpAgent from '@atproto/api' import { IdResolver } from '@atproto/identity' -import { SeedClient } from './seeds/client' import basicSeed from './seeds/basic' -import * as util from './_util' import { AppContext } from '../src' // outside of suite so they can be used in mock @@ -24,8 +23,8 @@ jest.mock('dns/promises', () => { }) describe('handles', () => { + let network: TestNetworkNoAppView let agent: AtpAgent - let close: util.CloseFn let sc: SeedClient let ctx: AppContext let idResolver: IdResolver @@ -33,21 +32,20 @@ describe('handles', () => { const newHandle = 'alice2.test' beforeAll(async () => { - const server = await util.runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'handles', }) - ctx = server.ctx + ctx = network.pds.ctx idResolver = new IdResolver({ plcUrl: ctx.cfg.identity.plcUrl }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) + agent = network.pds.getClient() + sc = network.getSeedClient() await basicSeed(sc) alice = sc.dids.alice bob = sc.dids.bob }) afterAll(async () => { - await close() + await network.close() }) const getHandleFromDb = async (did: string): Promise => { @@ -213,7 +211,7 @@ describe('handles', () => { handle: 'bob-alt.test', }, { - headers: { authorization: util.adminAuth() }, + headers: network.pds.adminAuthHeaders(), encoding: 'application/json', }, ) @@ -229,7 +227,7 @@ describe('handles', () => { handle: 'dril.test', }, { - headers: { authorization: util.adminAuth() }, + headers: network.pds.adminAuthHeaders(), encoding: 'application/json', }, ) @@ -261,7 +259,7 @@ describe('handles', () => { handle: 'bob-alt.test', }, { - headers: { authorization: util.moderatorAuth() }, + headers: network.pds.adminAuthHeaders('moderator'), encoding: 'application/json', }, ) @@ -272,7 +270,7 @@ describe('handles', () => { handle: 'bob-alt.test', }, { - headers: { authorization: util.triageAuth() }, + headers: network.pds.adminAuthHeaders('triage'), encoding: 'application/json', }, ) diff --git a/packages/pds/tests/invite-codes.test.ts b/packages/pds/tests/invite-codes.test.ts index 31892cfeedd..f406b77cc3b 100644 --- a/packages/pds/tests/invite-codes.test.ts +++ b/packages/pds/tests/invite-codes.test.ts @@ -1,32 +1,31 @@ import AtpAgent, { ComAtprotoServerCreateAccount } from '@atproto/api' import * as crypto from '@atproto/crypto' +import { TestNetworkNoAppView } from '@atproto/dev-env' import { AppContext } from '../src' -import * as util from './_util' import { DAY } from '@atproto/common' import { genInvCodes } from '../src/api/com/atproto/server/util' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' describe('account', () => { - let serverUrl: string + let network: TestNetworkNoAppView let ctx: AppContext let agent: AtpAgent - let close: util.CloseFn beforeAll(async () => { - const server = await util.runTestServer({ - inviteRequired: true, - inviteInterval: DAY, - inviteEpoch: Date.now() - 3 * DAY, + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'invite_codes', + pds: { + inviteRequired: true, + inviteInterval: DAY, + inviteEpoch: Date.now() - 3 * DAY, + }, }) - close = server.close - ctx = server.ctx - serverUrl = server.url - agent = new AtpAgent({ service: serverUrl }) + ctx = network.pds.ctx + agent = network.pds.getClient() }) afterAll(async () => { - await close() + await network.close() }) it('describes the fact that invites are required', async () => { @@ -35,7 +34,7 @@ describe('account', () => { }) it('succeeds with a valid code', async () => { - const code = await createInviteCode(agent, 1) + const code = await createInviteCode(network, agent, 1) await createAccountWithInvite(agent, code) }) @@ -47,9 +46,9 @@ describe('account', () => { }) it('fails on invite code from takendown account', async () => { - const account = await makeLoggedInAccount(agent) + const account = await makeLoggedInAccount(network, agent) // assign an invite code to the user - const code = await createInviteCode(agent, 1, account.did) + const code = await createInviteCode(network, agent, 1, account.did) // takedown the user's account const { data: takedownAction } = await agent.api.com.atproto.admin.takeModerationAction( @@ -64,7 +63,7 @@ describe('account', () => { }, { encoding: 'application/json', - headers: { authorization: util.adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) // attempt to create account with the previously generated invite code @@ -82,7 +81,7 @@ describe('account', () => { }, { encoding: 'application/json', - headers: { authorization: util.adminAuth() }, + headers: network.pds.adminAuthHeaders(), }, ) // attempt to create account with the previously generated invite code @@ -90,7 +89,7 @@ describe('account', () => { }) it('fails on used up invite code', async () => { - const code = await createInviteCode(agent, 2) + const code = await createInviteCode(network, agent, 2) await createAccountsWithInvite(agent, code, 2) const promise = createAccountWithInvite(agent, code) await expect(promise).rejects.toThrow( @@ -99,7 +98,7 @@ describe('account', () => { }) it('handles racing invite code uses', async () => { - const inviteCode = await createInviteCode(agent, 1) + const inviteCode = await createInviteCode(network, agent, 1) const COUNT = 10 let successes = 0 @@ -122,7 +121,7 @@ describe('account', () => { }) it('allow users to get available user invites', async () => { - const account = await makeLoggedInAccount(agent) + const account = await makeLoggedInAccount(network, agent) // no codes available yet const res1 = @@ -151,7 +150,7 @@ describe('account', () => { }) it('admin gifted codes to not impact a users available codes', async () => { - const account = await makeLoggedInAccount(agent) + const account = await makeLoggedInAccount(network, agent) // again, pretend account was made 2 days ago const twoDaysAgo = new Date(Date.now() - 2 * DAY).toISOString() @@ -161,9 +160,9 @@ describe('account', () => { .where('did', '=', account.did) .execute() - await createInviteCode(agent, 1, account.did) - await createInviteCode(agent, 1, account.did) - await createInviteCode(agent, 1, account.did) + await createInviteCode(network, agent, 1, account.did) + await createInviteCode(network, agent, 1, account.did) + await createInviteCode(network, agent, 1, account.did) const res = await account.agent.api.com.atproto.server.getAccountInviteCodes() @@ -181,7 +180,7 @@ describe('account', () => { }) it('creates invites based on epoch', async () => { - const account = await makeLoggedInAccount(agent) + const account = await makeLoggedInAccount(network, agent) // first, pretend account was made 2 days ago & get those two codes const twoDaysAgo = new Date(Date.now() - 2 * DAY).toISOString() @@ -249,9 +248,9 @@ describe('account', () => { }) it('prevents use of disabled codes', async () => { - const first = await createInviteCode(agent, 1) - const account = await makeLoggedInAccount(agent) - const second = await createInviteCode(agent, 1, account.did) + const first = await createInviteCode(network, agent, 1) + const account = await makeLoggedInAccount(network, agent) + const second = await createInviteCode(network, agent, 1, account.did) // disabled first by code & second by did await agent.api.com.atproto.admin.disableInviteCodes( @@ -260,7 +259,7 @@ describe('account', () => { accounts: [account.did], }, { - headers: { authorization: util.adminAuth() }, + headers: network.pds.adminAuthHeaders(), encoding: 'application/json', }, ) @@ -279,7 +278,7 @@ describe('account', () => { accounts: ['admin'], }, { - headers: { authorization: util.adminAuth() }, + headers: network.pds.adminAuthHeaders(), encoding: 'application/json', }, ) @@ -295,7 +294,7 @@ describe('account', () => { forAccounts: accounts, }, { - headers: { authorization: util.adminAuth() }, + headers: network.pds.adminAuthHeaders(), encoding: 'application/json', }, ) @@ -321,6 +320,7 @@ describe('account', () => { }) const createInviteCode = async ( + network: TestNetworkNoAppView, agent: AtpAgent, uses: number, forAccount?: string, @@ -328,7 +328,7 @@ const createInviteCode = async ( const res = await agent.api.com.atproto.server.createInviteCode( { useCount: uses, forAccount }, { - headers: { authorization: util.adminAuth() }, + headers: network.pds.adminAuthHeaders(), encoding: 'application/json', }, ) @@ -360,9 +360,10 @@ const createAccountsWithInvite = async ( } const makeLoggedInAccount = async ( + network: TestNetworkNoAppView, agent: AtpAgent, ): Promise<{ did: string; agent: AtpAgent }> => { - const code = await createInviteCode(agent, 1) + const code = await createInviteCode(network, agent, 1) const account = await createAccountWithInvite(agent, code) const did = account.did const loggedInAgent = new AtpAgent({ service: agent.service.toString() }) diff --git a/packages/pds/tests/preferences.test.ts b/packages/pds/tests/preferences.test.ts index 0194e967575..77dc256f85f 100644 --- a/packages/pds/tests/preferences.test.ts +++ b/packages/pds/tests/preferences.test.ts @@ -1,26 +1,23 @@ +import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' import AtpAgent from '@atproto/api' -import { CloseFn, runTestServer, TestServerInfo } from './_util' -import { SeedClient } from './seeds/client' import usersSeed from './seeds/users' describe('user preferences', () => { - 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: 'preferences', }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) + agent = network.pds.getClient() + sc = network.getSeedClient() await usersSeed(sc) }) afterAll(async () => { - await close() + await network.close() }) it('requires auth to set or put preferences.', async () => { @@ -45,7 +42,7 @@ describe('user preferences', () => { }) it('only gets preferences in app.bsky namespace.', async () => { - const { db, services } = server.ctx + const { db, services } = network.pds.ctx await db.transaction(async (tx) => { await services .account(tx) @@ -101,7 +98,7 @@ describe('user preferences', () => { ], }) // Ensure other prefs were not clobbered - const { db, services } = server.ctx + const { db, services } = network.pds.ctx const otherPrefs = await services .account(db) .getPreferences(sc.dids.alice, 'com.atproto') diff --git a/packages/pds/tests/proxied/admin.test.ts b/packages/pds/tests/proxied/admin.test.ts index a2948d2b40e..23c801cd6b2 100644 --- a/packages/pds/tests/proxied/admin.test.ts +++ b/packages/pds/tests/proxied/admin.test.ts @@ -1,6 +1,5 @@ import AtpAgent from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' -import { SeedClient } from '../seeds/client' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import basicSeed from '../seeds/basic' import { REASONOTHER, @@ -29,7 +28,7 @@ describe('proxies admin requests', () => { }, }) agent = network.pds.getClient() - sc = new SeedClient(agent) + sc = network.getSeedClient() const { data: invite } = await agent.api.com.atproto.server.createInviteCode( { useCount: 10 }, diff --git a/packages/pds/tests/proxied/feedgen.test.ts b/packages/pds/tests/proxied/feedgen.test.ts index 305b12fc230..142d1235497 100644 --- a/packages/pds/tests/proxied/feedgen.test.ts +++ b/packages/pds/tests/proxied/feedgen.test.ts @@ -1,7 +1,6 @@ import { makeAlgos } from '@atproto/bsky' import AtpAgent, { AtUri, FeedNS } from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' -import { SeedClient } from '../seeds/client' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import basicSeed from '../seeds/basic' import { forSnapshot } from '../_util' @@ -23,7 +22,7 @@ describe('feedgen proxy view', () => { bsky: { algos: makeAlgos(feedUri.host) }, }) agent = network.pds.getClient() - sc = new SeedClient(agent) + sc = network.getSeedClient() await basicSeed(sc) // publish feed const feed = await agent.api.app.bsky.feed.generator.create( diff --git a/packages/pds/tests/proxied/notif.test.ts b/packages/pds/tests/proxied/notif.test.ts index 106620d4bcd..fb3de2b8fe7 100644 --- a/packages/pds/tests/proxied/notif.test.ts +++ b/packages/pds/tests/proxied/notif.test.ts @@ -3,9 +3,8 @@ import http from 'http' import { AddressInfo } from 'net' import express from 'express' import AtpAgent from '@atproto/api' -import { TestNetworkNoAppView } from '@atproto/dev-env' +import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' import { verifyJwt } from '@atproto/xrpc-server' -import { SeedClient } from '../seeds/client' import usersSeed from '../seeds/users' import { createServer } from '../../src/lexicon' @@ -24,7 +23,7 @@ describe('notif service proxy', () => { network.pds.server.app.get const plc = network.plc.getClient() agent = network.pds.getClient() - sc = new SeedClient(agent) + sc = network.getSeedClient() await usersSeed(sc) await network.processAll() // piggybacking existing plc did, turn it into a notif service diff --git a/packages/pds/tests/proxied/procedures.test.ts b/packages/pds/tests/proxied/procedures.test.ts index 8fee0b35a5c..00dd02863ce 100644 --- a/packages/pds/tests/proxied/procedures.test.ts +++ b/packages/pds/tests/proxied/procedures.test.ts @@ -1,6 +1,5 @@ import AtpAgent from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' -import { SeedClient } from '../seeds/client' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import basicSeed from '../seeds/basic' describe('proxies appview procedures', () => { @@ -17,7 +16,7 @@ describe('proxies appview procedures', () => { dbPostgresSchema: 'proxy_procedures', }) agent = network.pds.getClient() - sc = new SeedClient(agent) + sc = network.getSeedClient() await basicSeed(sc) await network.processAll() alice = sc.dids.alice diff --git a/packages/pds/tests/proxied/read-after-write.test.ts b/packages/pds/tests/proxied/read-after-write.test.ts index 06df91fa53f..34f1e4b71dd 100644 --- a/packages/pds/tests/proxied/read-after-write.test.ts +++ b/packages/pds/tests/proxied/read-after-write.test.ts @@ -1,7 +1,6 @@ import util from 'util' import AtpAgent from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' -import { RecordRef, SeedClient } from '../seeds/client' +import { TestNetwork, SeedClient, RecordRef } from '@atproto/dev-env' import basicSeed from '../seeds/basic' import { ThreadViewPost } from '../../src/lexicon/types/app/bsky/feed/defs' import { View as RecordEmbedView } from '../../src/lexicon/types/app/bsky/embed/record' @@ -19,7 +18,7 @@ describe('proxy read after write', () => { dbPostgresSchema: 'proxy_read_after_write', }) agent = network.pds.getClient() - sc = new SeedClient(agent) + sc = network.getSeedClient() await basicSeed(sc) await network.processAll() alice = sc.dids.alice diff --git a/packages/pds/tests/proxied/views.test.ts b/packages/pds/tests/proxied/views.test.ts index 38b486a1d05..13fa41174b4 100644 --- a/packages/pds/tests/proxied/views.test.ts +++ b/packages/pds/tests/proxied/views.test.ts @@ -1,6 +1,5 @@ import AtpAgent, { AtUri } from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' -import { SeedClient } from '../seeds/client' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import basicSeed from '../seeds/basic' import { forSnapshot } from '../_util' @@ -19,7 +18,7 @@ describe('proxies view requests', () => { dbPostgresSchema: 'proxy_views', }) agent = network.pds.getClient() - sc = new SeedClient(agent) + sc = network.getSeedClient() await basicSeed(sc) alice = sc.dids.alice bob = sc.dids.bob diff --git a/packages/pds/tests/races.test.ts b/packages/pds/tests/races.test.ts index 7f276e61147..220e9c252c8 100644 --- a/packages/pds/tests/races.test.ts +++ b/packages/pds/tests/races.test.ts @@ -1,25 +1,24 @@ import AtpAgent from '@atproto/api' -import { CloseFn, runTestServer } from './_util' +import { wait } from '@atproto/common' +import { TestNetworkNoAppView } from '@atproto/dev-env' +import { CommitData, readCarWithRoot, verifyRepo } from '@atproto/repo' import AppContext from '../src/context' import { PreparedWrite, prepareCreate } from '../src/repo' -import { wait } from '@atproto/common' import SqlRepoStorage from '../src/sql-repo-storage' -import { CommitData, readCarWithRoot, verifyRepo } from '@atproto/repo' import { ConcurrentWriteError } from '../src/services/repo' describe('crud operations', () => { + let network: TestNetworkNoAppView let ctx: AppContext let agent: AtpAgent let did: string - let close: CloseFn beforeAll(async () => { - const server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'races', }) - ctx = server.ctx - close = server.close - agent = new AtpAgent({ service: server.url }) + ctx = network.pds.ctx + agent = network.pds.getClient() await agent.createAccount({ email: 'alice@test.com', handle: 'alice.test', @@ -29,7 +28,7 @@ describe('crud operations', () => { }) afterAll(async () => { - await close() + await network.close() }) const formatWrite = async () => { diff --git a/packages/pds/tests/rate-limits.test.ts b/packages/pds/tests/rate-limits.test.ts index 6f7cd77cbb8..3e8703126f3 100644 --- a/packages/pds/tests/rate-limits.test.ts +++ b/packages/pds/tests/rate-limits.test.ts @@ -1,32 +1,33 @@ -import { runTestServer, TestServerInfo } from './_util' -import { SeedClient } from './seeds/client' -import userSeed from './seeds/basic' import { AtpAgent } from '@atproto/api' import { randomStr } from '@atproto/crypto' +import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' +import userSeed from './seeds/basic' describe('rate limits', () => { - let server: TestServerInfo + let network: TestNetworkNoAppView let agent: AtpAgent let sc: SeedClient let alice: string let bob: string beforeAll(async () => { - server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'rate_limits', - redisScratchAddress: process.env.REDIS_HOST, - redisScratchPassword: process.env.REDIS_PASSWORD, - rateLimitsEnabled: true, + pds: { + redisScratchAddress: process.env.REDIS_HOST, + redisScratchPassword: process.env.REDIS_PASSWORD, + rateLimitsEnabled: true, + }, }) - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) + agent = network.pds.getClient() + sc = network.getSeedClient() await userSeed(sc) alice = sc.dids.alice bob = sc.dids.bob }) afterAll(async () => { - await server.close() + await network.close() }) it('rate limits by ip', async () => { diff --git a/packages/pds/tests/seeds/basic.ts b/packages/pds/tests/seeds/basic.ts index cf3bd83bbc3..3d045fc9239 100644 --- a/packages/pds/tests/seeds/basic.ts +++ b/packages/pds/tests/seeds/basic.ts @@ -1,7 +1,6 @@ +import { SeedClient } from '@atproto/dev-env' import { ids } from '../../src/lexicon/lexicons' import { FLAG } from '../../src/lexicon/types/com/atproto/admin/defs' -import { adminAuth } from '../_util' -import { SeedClient } from './client' import usersSeed from './users' export default async (sc: SeedClient, invite?: { code: string }) => { @@ -142,7 +141,7 @@ export default async (sc: SeedClient, invite?: { code: string }) => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: sc.adminAuthHeaders(), }, ) diff --git a/packages/pds/tests/seeds/follows.ts b/packages/pds/tests/seeds/follows.ts index f15156dbff5..1abe555ff00 100644 --- a/packages/pds/tests/seeds/follows.ts +++ b/packages/pds/tests/seeds/follows.ts @@ -1,4 +1,4 @@ -import { SeedClient } from './client' +import { SeedClient } from '@atproto/dev-env' export default async (sc: SeedClient) => { await sc.createAccount('alice', users.alice) diff --git a/packages/pds/tests/seeds/likes.ts b/packages/pds/tests/seeds/likes.ts index 27eeba09c40..c1671652a5e 100644 --- a/packages/pds/tests/seeds/likes.ts +++ b/packages/pds/tests/seeds/likes.ts @@ -1,5 +1,5 @@ +import { SeedClient } from '@atproto/dev-env' import basicSeed from './basic' -import { SeedClient } from './client' export default async (sc: SeedClient) => { await basicSeed(sc) diff --git a/packages/pds/tests/seeds/reposts.ts b/packages/pds/tests/seeds/reposts.ts index 8de9b8ec655..9bb444ec8f2 100644 --- a/packages/pds/tests/seeds/reposts.ts +++ b/packages/pds/tests/seeds/reposts.ts @@ -1,5 +1,5 @@ +import { SeedClient } from '@atproto/dev-env' import basicSeed from './basic' -import { SeedClient } from './client' export default async (sc: SeedClient) => { await basicSeed(sc) diff --git a/packages/pds/tests/seeds/thread.ts b/packages/pds/tests/seeds/thread.ts index 921736e919e..747e2fdfd6e 100644 --- a/packages/pds/tests/seeds/thread.ts +++ b/packages/pds/tests/seeds/thread.ts @@ -1,4 +1,4 @@ -import { RecordRef, SeedClient } from './client' +import { RecordRef, SeedClient } from '@atproto/dev-env' export default async (sc: SeedClient, did, threads: Item[]) => { const refByItemId: Record = {} diff --git a/packages/pds/tests/seeds/users-bulk.ts b/packages/pds/tests/seeds/users-bulk.ts index 5cc6813753c..ec4e4b5a6f7 100644 --- a/packages/pds/tests/seeds/users-bulk.ts +++ b/packages/pds/tests/seeds/users-bulk.ts @@ -1,5 +1,5 @@ import { chunkArray } from '@atproto/common' -import { SeedClient } from './client' +import { SeedClient } from '@atproto/dev-env' export default async (sc: SeedClient, max = Infinity) => { // @TODO when these are run in parallel, seem to get an intermittent diff --git a/packages/pds/tests/seeds/users.ts b/packages/pds/tests/seeds/users.ts index 2ef9a74864f..6f20bf613bb 100644 --- a/packages/pds/tests/seeds/users.ts +++ b/packages/pds/tests/seeds/users.ts @@ -1,4 +1,4 @@ -import { SeedClient } from './client' +import { SeedClient } from '@atproto/dev-env' export default async (sc: SeedClient, invite?: { code: string }) => { await sc.createAccount('alice', { ...users.alice, inviteCode: invite?.code }) diff --git a/packages/pds/tests/sequencer.test.ts b/packages/pds/tests/sequencer.test.ts index c41cedafb58..d48ba1797d6 100644 --- a/packages/pds/tests/sequencer.test.ts +++ b/packages/pds/tests/sequencer.test.ts @@ -1,15 +1,13 @@ -import AtpAgent from '@atproto/api' +import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' import { randomStr } from '@atproto/crypto' import { cborEncode, readFromGenerator, wait } from '@atproto/common' import { Sequencer, SeqEvt } from '../src/sequencer' import Outbox from '../src/sequencer/outbox' import { Database } from '../src' -import { SeedClient } from './seeds/client' import userSeed from './seeds/users' -import { TestServerInfo, runTestServer } from './_util' describe('sequencer', () => { - let server: TestServerInfo + let network: TestNetworkNoAppView let db: Database let sequencer: Sequencer let sc: SeedClient @@ -20,13 +18,12 @@ describe('sequencer', () => { let lastSeen: number beforeAll(async () => { - server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'sequencer', }) - db = server.ctx.db - sequencer = server.ctx.sequencer - const agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) + db = network.pds.ctx.db + sequencer = network.pds.ctx.sequencer + sc = network.getSeedClient() await userSeed(sc) alice = sc.dids.alice bob = sc.dids.bob @@ -35,7 +32,7 @@ describe('sequencer', () => { }) afterAll(async () => { - await server.close() + await network.close() }) const randomPost = async (by: string) => sc.post(by, randomStr(8, 'base32')) @@ -81,7 +78,7 @@ describe('sequencer', () => { const caughtUp = (outbox: Outbox): (() => Promise) => { return async () => { - const leaderCaughtUp = await server.ctx.sequencerLeader?.isCaughtUp() + const leaderCaughtUp = await network.pds.ctx.sequencerLeader?.isCaughtUp() if (!leaderCaughtUp) return false const lastEvt = await outbox.sequencer.curr() if (!lastEvt) return true diff --git a/packages/pds/tests/server.test.ts b/packages/pds/tests/server.test.ts index 86714e1bb6d..23298a7d731 100644 --- a/packages/pds/tests/server.test.ts +++ b/packages/pds/tests/server.test.ts @@ -1,12 +1,11 @@ import { AddressInfo } from 'net' import express from 'express' import axios, { AxiosError } from 'axios' +import { TestNetwork, SeedClient } from '@atproto/dev-env' import AtpAgent, { AtUri } from '@atproto/api' import { handler as errorHandler } from '../src/error' -import { SeedClient } from './seeds/client' import basicSeed from './seeds/basic' import { Database } from '../src' -import { TestNetwork } from '@atproto/dev-env' import { randomStr } from '@atproto/crypto' describe('server', () => { @@ -25,7 +24,7 @@ describe('server', () => { }) db = network.pds.ctx.db agent = network.pds.getClient() - sc = new SeedClient(agent) + sc = network.getSeedClient() await basicSeed(sc) alice = sc.dids.alice }) diff --git a/packages/pds/tests/sql-repo-storage.test.ts b/packages/pds/tests/sql-repo-storage.test.ts index c19a8b41805..ea63cf07e06 100644 --- a/packages/pds/tests/sql-repo-storage.test.ts +++ b/packages/pds/tests/sql-repo-storage.test.ts @@ -1,25 +1,24 @@ +import { TestNetworkNoAppView } from '@atproto/dev-env' import { range, dataToCborBlock, TID } from '@atproto/common' import { CidSet, def } from '@atproto/repo' import BlockMap from '@atproto/repo/src/block-map' +import { CID } from 'multiformats/cid' import { Database } from '../src' import SqlRepoStorage from '../src/sql-repo-storage' -import { CloseFn, runTestServer } from './_util' -import { CID } from 'multiformats/cid' describe('sql repo storage', () => { + let network: TestNetworkNoAppView let db: Database - let close: CloseFn beforeAll(async () => { - const server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'sql_repo_storage', }) - close = server.close - db = server.ctx.db + db = network.pds.ctx.db }) afterAll(async () => { - await close() + await network.close() }) it('puts and gets blocks.', async () => { @@ -28,7 +27,7 @@ describe('sql repo storage', () => { const cid = await db.transaction(async (dbTxn) => { const storage = new SqlRepoStorage(dbTxn, did) const block = await dataToCborBlock({ my: 'block' }) - await storage.putBlock(block.cid, block.bytes) + await storage.putBlock(block.cid, block.bytes, TID.nextStr()) return block.cid }) @@ -44,14 +43,14 @@ describe('sql repo storage', () => { const cidA = await db.transaction(async (dbTxn) => { const storage = new SqlRepoStorage(dbTxn, did) const block = await dataToCborBlock({ my: 'block' }) - await storage.putBlock(block.cid, block.bytes) + await storage.putBlock(block.cid, block.bytes, TID.nextStr()) return block.cid }) const cidB = await db.transaction(async (dbTxn) => { const storage = new SqlRepoStorage(dbTxn, did) const block = await dataToCborBlock({ my: 'block' }) - await storage.putBlock(block.cid, block.bytes) + await storage.putBlock(block.cid, block.bytes, TID.nextStr()) return block.cid }) @@ -87,13 +86,15 @@ describe('sql repo storage', () => { cid: commits[0].cid, rev: TID.nextStr(), prev: null, + since: null, newBlocks: blocks0, removedCids: new CidSet(), }) await storage.applyCommit({ cid: commits[1].cid, - prev: commits[0].cid, rev: TID.nextStr(), + prev: commits[0].cid, + since: null, newBlocks: blocks1, removedCids: toRemove, }) diff --git a/packages/pds/tests/sync/list.test.ts b/packages/pds/tests/sync/list.test.ts index cbab7126ad3..cd6716e7a11 100644 --- a/packages/pds/tests/sync/list.test.ts +++ b/packages/pds/tests/sync/list.test.ts @@ -1,25 +1,23 @@ +import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' import AtpAgent from '@atproto/api' -import { CloseFn, runTestServer } from '../_util' -import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' describe('sync listing', () => { + let network: TestNetworkNoAppView let agent: AtpAgent let sc: SeedClient - let close: CloseFn beforeAll(async () => { - const server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'sync_list', }) - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) + agent = network.pds.getClient() + sc = network.getSeedClient() await basicSeed(sc) }) afterAll(async () => { - await close() + await network.close() }) it('lists hosted repos in order of creation', async () => { diff --git a/packages/pds/tests/sync/subscribe-repos.test.ts b/packages/pds/tests/sync/subscribe-repos.test.ts index e9e008ae91d..58745b7fe1e 100644 --- a/packages/pds/tests/sync/subscribe-repos.test.ts +++ b/packages/pds/tests/sync/subscribe-repos.test.ts @@ -1,3 +1,4 @@ +import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' import AtpAgent from '@atproto/api' import { cborDecode, @@ -17,14 +18,13 @@ import { Tombstone as TombstoneEvt, } from '../../src/lexicon/types/com/atproto/sync/subscribeRepos' import { AppContext, Database } from '../../src' -import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' -import { CloseFn, runTestServer } from '../_util' import { CID } from 'multiformats/cid' describe('repo subscribe repos', () => { let serverHost: string + let network: TestNetworkNoAppView let db: Database let ctx: AppContext @@ -35,19 +35,18 @@ describe('repo subscribe repos', () => { let carol: string let dan: string - let close: CloseFn - beforeAll(async () => { - const server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'repo_subscribe_repos', - repoBackfillLimitMs: HOUR, + pds: { + repoBackfillLimitMs: HOUR, + }, }) - serverHost = server.url.replace('http://', '') - ctx = server.ctx - db = server.ctx.db - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) + serverHost = network.pds.url.replace('http://', '') + ctx = network.pds.ctx + db = network.pds.ctx.db + agent = network.pds.getClient() + sc = network.getSeedClient() await basicSeed(sc) alice = sc.dids.alice bob = sc.dids.bob @@ -56,7 +55,7 @@ describe('repo subscribe repos', () => { }) afterAll(async () => { - await close() + await network.close() }) const getRepo = async (did: string): Promise => { diff --git a/packages/pds/tests/sync/sync.test.ts b/packages/pds/tests/sync/sync.test.ts index 27311cbf81d..424ebc86337 100644 --- a/packages/pds/tests/sync/sync.test.ts +++ b/packages/pds/tests/sync/sync.test.ts @@ -1,3 +1,4 @@ +import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' import AtpAgent from '@atproto/api' import { TID } from '@atproto/common' import { randomStr } from '@atproto/crypto' @@ -7,10 +8,9 @@ import { AtUri } from '@atproto/syntax' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' import { CID } from 'multiformats/cid' import { AppContext } from '../../src' -import { adminAuth, CloseFn, runTestServer } from '../_util' -import { SeedClient } from '../seeds/client' describe('repo sync', () => { + let network: TestNetworkNoAppView let agent: AtpAgent let sc: SeedClient let did: string @@ -21,16 +21,13 @@ describe('repo sync', () => { let currRoot: CID | undefined let ctx: AppContext - let close: CloseFn - beforeAll(async () => { - const server = await runTestServer({ + network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'repo_sync', }) - ctx = server.ctx - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) + ctx = network.pds.ctx + agent = network.pds.getClient() + sc = network.getSeedClient() await sc.createAccount('alice', { email: 'alice@test.com', handle: 'alice.test', @@ -41,7 +38,7 @@ describe('repo sync', () => { }) afterAll(async () => { - await close() + await network.close() }) it('creates and syncs some records', async () => { @@ -229,7 +226,7 @@ describe('repo sync', () => { await expect(tryGetRepoOwner).resolves.toBeDefined() const tryGetRepoAdmin = agent.api.com.atproto.sync.getRepo( { did }, - { headers: { authorization: adminAuth() } }, + { headers: network.pds.adminAuthHeaders() }, ) await expect(tryGetRepoAdmin).resolves.toBeDefined() }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 309e59c502a..0e22af0c4e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -343,6 +343,9 @@ importers: '@atproto/identity': specifier: workspace:^ version: link:../identity + '@atproto/lexicon': + specifier: workspace:^ + version: link:../lexicon '@atproto/pds': specifier: workspace:^ version: link:../pds @@ -373,6 +376,9 @@ importers: get-port: specifier: ^6.1.2 version: 6.1.2 + multiformats: + specifier: ^9.9.0 + version: 9.9.0 sharp: specifier: ^0.31.2 version: 0.31.2 @@ -613,6 +619,9 @@ importers: axios: specifier: ^0.27.2 version: 0.27.2 + ws: + specifier: ^8.12.0 + version: 8.12.0 packages/repo: dependencies: @@ -11200,7 +11209,6 @@ packages: optional: true utf-8-validate: optional: true - dev: false /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} From c7285a8e9fcd39d673a08c6f114918ce1bd42171 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 28 Sep 2023 19:48:48 -0500 Subject: [PATCH 098/105] build pds correctly --- packages/pds/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/pds/package.json b/packages/pds/package.json index 4d966941f8a..4c22a61133b 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -24,6 +24,7 @@ "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", "test": "../dev-infra/with-test-redis-and-db.sh jest", + "update-main-to-dist": "node ../../update-main-to-dist.js packages/pds", "bench": "../dev-infra/with-test-redis-and-db.sh jest --config jest.bench.config.js", "test:sqlite": "jest --testPathIgnorePatterns /tests/proxied/*", "test:log": "tail -50 test.log | pino-pretty", From 7506d5dcdfcbc8fe9cf47b8ae83d0a51795acbab Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 28 Sep 2023 19:58:40 -0500 Subject: [PATCH 099/105] fix entry point --- services/pds/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/pds/index.js b/services/pds/index.js index 4c7477f6f14..d8d7126676a 100644 --- a/services/pds/index.js +++ b/services/pds/index.js @@ -32,7 +32,7 @@ const main = async () => { // If the PDS is configured to proxy moderation, this will be running on appview instead of pds. // Also don't run this on the sequencer leader, which may not be configured regarding moderation proxying at all. const periodicModerationActionReversal = - pds.cfg.bskyAppView.proxyModeration || pds.ctx.cfg.sequencerLeaderEnabled + pds.ctx.cfg.bskyAppView.proxyModeration || pds.ctx.cfg.sequencerLeaderEnabled ? null : new PeriodicModerationActionReversal(pds.ctx) const periodicModerationActionReversalRunning = From 3efaa1dd05e9dd6273b3e0a069195207c2195e2c Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 28 Sep 2023 21:03:06 -0500 Subject: [PATCH 100/105] default logging to false (for now) --- packages/common/src/logger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/src/logger.ts b/packages/common/src/logger.ts index a302c6ed3c5..857d32ee9f2 100644 --- a/packages/common/src/logger.ts +++ b/packages/common/src/logger.ts @@ -7,7 +7,7 @@ const enabledSystems = (process.env.LOG_SYSTEMS || '') const enabledEnv = process.env.LOG_ENABLED const enabled = - enabledEnv !== 'false' && enabledEnv !== 'f' && enabledEnv !== '0' + enabledEnv === 'true' || enabledEnv === 't' || enabledEnv === '1' const level = process.env.LOG_LEVEL || 'info' From d3e194f9bf8935799b52663ff2f6d1a3aa1bd7be Mon Sep 17 00:00:00 2001 From: dholms Date: Fri, 29 Sep 2023 14:27:56 -0500 Subject: [PATCH 101/105] format service entry --- services/pds/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/pds/index.js b/services/pds/index.js index d8d7126676a..112d63edf90 100644 --- a/services/pds/index.js +++ b/services/pds/index.js @@ -32,7 +32,8 @@ const main = async () => { // If the PDS is configured to proxy moderation, this will be running on appview instead of pds. // Also don't run this on the sequencer leader, which may not be configured regarding moderation proxying at all. const periodicModerationActionReversal = - pds.ctx.cfg.bskyAppView.proxyModeration || pds.ctx.cfg.sequencerLeaderEnabled + pds.ctx.cfg.bskyAppView.proxyModeration || + pds.ctx.cfg.sequencerLeaderEnabled ? null : new PeriodicModerationActionReversal(pds.ctx) const periodicModerationActionReversalRunning = From f2d06c487d7bcfc4ddf28bf4925ed3d2dca4f805 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Fri, 29 Sep 2023 20:38:44 -0400 Subject: [PATCH 102/105] Switch takedown ids back to ints on pds distribution (#1694) * switch takedown ids back to ints, consistent with live pds * tidy/fix migration * update migration for sqlite --- .../20230929T213219699Z-takedown-id-as-int.ts | 47 +++++++++++++++++++ packages/pds/src/db/migrations/index.ts | 1 + packages/pds/src/db/tables/record.ts | 3 +- packages/pds/src/db/tables/repo-blob.ts | 3 +- packages/pds/src/db/tables/repo-root.ts | 3 +- packages/pds/src/db/util.ts | 2 +- packages/pds/src/services/moderation/index.ts | 6 +-- packages/pds/src/services/moderation/views.ts | 2 +- packages/pds/src/services/record/index.ts | 2 +- packages/pds/tests/account-deletion.test.ts | 1 - 10 files changed, 57 insertions(+), 13 deletions(-) create mode 100644 packages/pds/src/db/migrations/20230929T213219699Z-takedown-id-as-int.ts diff --git a/packages/pds/src/db/migrations/20230929T213219699Z-takedown-id-as-int.ts b/packages/pds/src/db/migrations/20230929T213219699Z-takedown-id-as-int.ts new file mode 100644 index 00000000000..8cacc599c60 --- /dev/null +++ b/packages/pds/src/db/migrations/20230929T213219699Z-takedown-id-as-int.ts @@ -0,0 +1,47 @@ +import { Kysely, sql } from 'kysely' +import { Dialect } from '..' + +export async function up(db: Kysely, dialect: Dialect): Promise { + if (dialect === 'pg') { + await sql` + alter table "repo_root" alter column "takedownId" type integer using "takedownId"::integer; + alter table "repo_blob" alter column "takedownId" type integer using "takedownId"::integer; + alter table "record" alter column "takedownId" type integer using "takedownId"::integer; + `.execute(db) + } else { + await sql`alter table "repo_root" drop column "takedownId"`.execute(db) + await sql`alter table "repo_root" add column "takedownId" integer`.execute( + db, + ) + await sql`alter table "repo_blob" drop column "takedownId"`.execute(db) + await sql`alter table "repo_blob" add column "takedownId" integer`.execute( + db, + ) + await sql`alter table "record" drop column "takedownId"`.execute(db) + await sql`alter table "record" add column "takedownId" integer`.execute(db) + } +} + +export async function down( + db: Kysely, + dialect: Dialect, +): Promise { + if (dialect === 'pg') { + await sql` + alter table "repo_root" alter column "takedownId" type varchar; + alter table "repo_blob" alter column "takedownId" type varchar; + alter table "record" alter column "takedownId" type varchar; + `.execute(db) + } else { + await sql`alter table "repo_root" drop column "takedownId"`.execute(db) + await sql`alter table "repo_root" add column "takedownId" varchar`.execute( + db, + ) + await sql`alter table "repo_blob" drop column "takedownId"`.execute(db) + await sql`alter table "repo_blob" add column "takedownId" varchar`.execute( + db, + ) + await sql`alter table "record" drop column "takedownId"`.execute(db) + await sql`alter table "record" add column "takedownId" varchar`.execute(db) + } +} diff --git a/packages/pds/src/db/migrations/index.ts b/packages/pds/src/db/migrations/index.ts index 2c7b34b9f47..9aead0d7012 100644 --- a/packages/pds/src/db/migrations/index.ts +++ b/packages/pds/src/db/migrations/index.ts @@ -5,3 +5,4 @@ export * as _20230613T164932261Z from './20230613T164932261Z-init' export * as _20230914T014727199Z from './20230914T014727199Z-repo-v3' export * as _20230926T195532354Z from './20230926T195532354Z-email-tokens' +export * as _20230929T213219699Z from './20230929T213219699Z-takedown-id-as-int' diff --git a/packages/pds/src/db/tables/record.ts b/packages/pds/src/db/tables/record.ts index fbde68b2219..03f1008ef0f 100644 --- a/packages/pds/src/db/tables/record.ts +++ b/packages/pds/src/db/tables/record.ts @@ -7,8 +7,7 @@ export interface Record { rkey: string repoRev: string | null indexedAt: string - // opaque identifier, though currently tends to reference a moderation_action - takedownId: string | null + takedownId: number | null } export const tableName = 'record' diff --git a/packages/pds/src/db/tables/repo-blob.ts b/packages/pds/src/db/tables/repo-blob.ts index 67aa072454f..a1fed0877e5 100644 --- a/packages/pds/src/db/tables/repo-blob.ts +++ b/packages/pds/src/db/tables/repo-blob.ts @@ -3,8 +3,7 @@ export interface RepoBlob { recordUri: string repoRev: string | null did: string - // opaque identifier, though currently tends to reference a moderation_action - takedownId: string | null + takedownId: number | null } export const tableName = 'repo_blob' diff --git a/packages/pds/src/db/tables/repo-root.ts b/packages/pds/src/db/tables/repo-root.ts index b68c6c37bea..6b6c921f380 100644 --- a/packages/pds/src/db/tables/repo-root.ts +++ b/packages/pds/src/db/tables/repo-root.ts @@ -4,8 +4,7 @@ export interface RepoRoot { root: string rev: string | null indexedAt: string - // opaque identifier, though currently tends to reference a moderation_action - takedownId: string | null + takedownId: number | null } export const tableName = 'repo_root' diff --git a/packages/pds/src/db/util.ts b/packages/pds/src/db/util.ts index 4fcd93a1d44..696ac7dee8b 100644 --- a/packages/pds/src/db/util.ts +++ b/packages/pds/src/db/util.ts @@ -23,7 +23,7 @@ export const notSoftDeletedClause = (alias: DbRef) => { return sql`${alias}."takedownId" is null` } -export const softDeleted = (repoOrRecord: { takedownId: string | null }) => { +export const softDeleted = (repoOrRecord: { takedownId: number | null }) => { return repoOrRecord.takedownId !== null } diff --git a/packages/pds/src/services/moderation/index.ts b/packages/pds/src/services/moderation/index.ts index 622fee2a11c..9e46332cf33 100644 --- a/packages/pds/src/services/moderation/index.ts +++ b/packages/pds/src/services/moderation/index.ts @@ -437,7 +437,7 @@ export class ModerationService { async takedownRepo(info: { takedownId: number; did: string }) { await this.db.db .updateTable('repo_root') - .set({ takedownId: String(info.takedownId) }) + .set({ takedownId: info.takedownId }) .where('did', '=', info.did) .where('takedownId', 'is', null) .executeTakeFirst() @@ -459,14 +459,14 @@ export class ModerationService { this.db.assertTransaction() await this.db.db .updateTable('record') - .set({ takedownId: String(info.takedownId) }) + .set({ takedownId: info.takedownId }) .where('uri', '=', info.uri.toString()) .where('takedownId', 'is', null) .executeTakeFirst() if (info.blobCids?.length) { await this.db.db .updateTable('repo_blob') - .set({ takedownId: String(info.takedownId) }) + .set({ takedownId: info.takedownId }) .where('recordUri', '=', info.uri.toString()) .where( 'cid', diff --git a/packages/pds/src/services/moderation/views.ts b/packages/pds/src/services/moderation/views.ts index b38b9fa987e..e8d89620d73 100644 --- a/packages/pds/src/services/moderation/views.ts +++ b/packages/pds/src/services/moderation/views.ts @@ -616,7 +616,7 @@ type RecordResult = { cid: string value: object indexedAt: string - takedownId: string | null + takedownId: number | null } type SubjectResult = Pick< diff --git a/packages/pds/src/services/record/index.ts b/packages/pds/src/services/record/index.ts index 5ddc9b79a09..1914d1b8c61 100644 --- a/packages/pds/src/services/record/index.ts +++ b/packages/pds/src/services/record/index.ts @@ -164,7 +164,7 @@ export class RecordService { cid: string value: object indexedAt: string - takedownId: string | null + takedownId: number | null } | null> { const { ref } = this.db.db.dynamic let builder = this.db.db diff --git a/packages/pds/tests/account-deletion.test.ts b/packages/pds/tests/account-deletion.test.ts index 54f1de60c70..12bdad8875a 100644 --- a/packages/pds/tests/account-deletion.test.ts +++ b/packages/pds/tests/account-deletion.test.ts @@ -62,7 +62,6 @@ describe('account deletion', () => { const getMailFrom = async (promise): Promise => { const result = await Promise.all([once(mailCatcher, 'mail'), promise]) - console.log(result) return result[0][0] } From 7e5920f37053afb1b3f7c8fda26b73f16d0dfb3c Mon Sep 17 00:00:00 2001 From: dholms Date: Sun, 1 Oct 2023 14:09:09 -0500 Subject: [PATCH 103/105] export moderation action reversal --- packages/pds/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index 42544eba492..cc9e1555895 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -28,6 +28,7 @@ import compression from './util/compression' export * from './config' export { Database } from './db' +export { PeriodicModerationActionReversal } from './db/periodic-moderation-action-reversal' export { DiskBlobStore, MemoryBlobStore } from './storage' export { AppContext } from './context' export { httpLogger } from './logger' From 4fef68b47bc266003d9531c7d0528a0be8c3f887 Mon Sep 17 00:00:00 2001 From: dholms Date: Sun, 1 Oct 2023 14:16:46 -0500 Subject: [PATCH 104/105] takedown tests --- packages/bsky/tests/auto-moderator/takedowns.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bsky/tests/auto-moderator/takedowns.test.ts b/packages/bsky/tests/auto-moderator/takedowns.test.ts index 733c7a87baf..6c7b0669b77 100644 --- a/packages/bsky/tests/auto-moderator/takedowns.test.ts +++ b/packages/bsky/tests/auto-moderator/takedowns.test.ts @@ -98,7 +98,7 @@ describe('takedowner', () => { .where('uri', '=', post.ref.uriStr) .select('takedownId') .executeTakeFirst() - expect(recordPds?.takedownId).toEqual(modAction.id.toString()) + expect(recordPds?.takedownId).toEqual(modAction.id) expect(testInvalidator.invalidated.length).toBe(1) expect(testInvalidator.invalidated[0].subject).toBe( @@ -140,7 +140,7 @@ describe('takedowner', () => { .where('uri', '=', res.data.uri) .select('takedownId') .executeTakeFirst() - expect(recordPds?.takedownId).toEqual(modAction.id.toString()) + expect(recordPds?.takedownId).toEqual(modAction.id) expect(testInvalidator.invalidated.length).toBe(2) expect(testInvalidator.invalidated[1].subject).toBe( From fc193acee09e6c1a6f1221126bbc73f5062a0f3c Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 2 Oct 2023 13:15:27 -0500 Subject: [PATCH 105/105] dont build branch --- .github/workflows/build-and-push-pds-aws.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build-and-push-pds-aws.yaml b/.github/workflows/build-and-push-pds-aws.yaml index 8d132401d21..097f782d88e 100644 --- a/.github/workflows/build-and-push-pds-aws.yaml +++ b/.github/workflows/build-and-push-pds-aws.yaml @@ -3,7 +3,6 @@ on: push: branches: - main - - simplify-pds env: REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}