diff --git a/.eslintignore b/.eslintignore index fc66834cbc0..ec0195a5f8c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ packages/api/src/client packages/bsky/src/lexicon packages/pds/src/lexicon +packages/ozone/src/lexicon diff --git a/.github/workflows/build-and-push-bsky-aws.yaml b/.github/workflows/build-and-push-bsky-aws.yaml index 9df469c0615..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 - - appeal-report 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-ozone-aws.yaml b/.github/workflows/build-and-push-ozone-aws.yaml new file mode 100644 index 00000000000..53f95c5b731 --- /dev/null +++ b/.github/workflows/build-and-push-ozone-aws.yaml @@ -0,0 +1,54 @@ +name: build-and-push-ozone-aws +on: + push: + branches: + - main +env: + REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} + USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }} + PASSWORD: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_PASSWORD }} + IMAGE_NAME: ozone + +jobs: + ozone-container-aws: + if: github.repository == 'bluesky-social/atproto' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v2 + + - name: Log into registry ${{ env.REGISTRY }} + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ env.USERNAME}} + password: ${{ env.PASSWORD }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=sha,enable=true,priority=100,prefix=,suffix=,format=long + + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v4 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + file: ./services/ozone/Dockerfile + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/build-and-push-ozone-ghcr.yaml b/.github/workflows/build-and-push-ozone-ghcr.yaml new file mode 100644 index 00000000000..ab37093963d --- /dev/null +++ b/.github/workflows/build-and-push-ozone-ghcr.yaml @@ -0,0 +1,56 @@ +name: build-and-push-ozone-ghcr +on: + push: + branches: + - main +env: + REGISTRY: ghcr.io + USERNAME: ${{ github.actor }} + PASSWORD: ${{ secrets.GITHUB_TOKEN }} + + # github.repository as / + IMAGE_NAME: ${{ github.repository }} + +jobs: + ozone-container-ghcr: + if: github.repository == 'bluesky-social/atproto' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v2 + + - name: Log into registry ${{ env.REGISTRY }} + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ env.USERNAME }} + password: ${{ env.PASSWORD }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=sha,enable=true,priority=100,prefix=ozone:,suffix=,format=long + + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v4 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + file: ./services/ozone/Dockerfile + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Makefile b/Makefile index 0236fafe069..2ec47b2892e 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,7 @@ 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 + cd packages/ozone; pnpm run codegen # clean up codegen output pnpm format diff --git a/lexicons/com/atproto/admin/defs.json b/lexicons/com/atproto/admin/defs.json index 23448b7ac8d..55a8b32be53 100644 --- a/lexicons/com/atproto/admin/defs.json +++ b/lexicons/com/atproto/admin/defs.json @@ -294,6 +294,7 @@ "did": { "type": "string", "format": "did" }, "handle": { "type": "string", "format": "handle" }, "email": { "type": "string" }, + "relatedRecords": { "type": "array", "items": { "type": "unknown" } }, "indexedAt": { "type": "string", "format": "datetime" }, "invitedBy": { "type": "ref", diff --git a/lexicons/com/atproto/admin/getAccountInfos.json b/lexicons/com/atproto/admin/getAccountInfos.json new file mode 100644 index 00000000000..45d97e08bd6 --- /dev/null +++ b/lexicons/com/atproto/admin/getAccountInfos.json @@ -0,0 +1,36 @@ +{ + "lexicon": 1, + "id": "com.atproto.admin.getAccountInfos", + "defs": { + "main": { + "type": "query", + "description": "Get details about some accounts.", + "parameters": { + "type": "params", + "required": ["dids"], + "properties": { + "dids": { + "type": "array", + "items": { "type": "string", "format": "did" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["infos"], + "properties": { + "infos": { + "type": "array", + "items": { + "type": "ref", + "ref": "com.atproto.admin.defs#accountView" + } + } + } + } + } + } + } +} diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index df55181aef0..fb56cd251a0 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -14,6 +14,7 @@ import * as ComAtprotoAdminDisableInviteCodes from './types/com/atproto/admin/di import * as ComAtprotoAdminEmitModerationEvent from './types/com/atproto/admin/emitModerationEvent' import * as ComAtprotoAdminEnableAccountInvites from './types/com/atproto/admin/enableAccountInvites' import * as ComAtprotoAdminGetAccountInfo from './types/com/atproto/admin/getAccountInfo' +import * as ComAtprotoAdminGetAccountInfos from './types/com/atproto/admin/getAccountInfos' import * as ComAtprotoAdminGetInviteCodes from './types/com/atproto/admin/getInviteCodes' import * as ComAtprotoAdminGetModerationEvent from './types/com/atproto/admin/getModerationEvent' import * as ComAtprotoAdminGetRecord from './types/com/atproto/admin/getRecord' @@ -153,6 +154,7 @@ export * as ComAtprotoAdminDisableInviteCodes from './types/com/atproto/admin/di export * as ComAtprotoAdminEmitModerationEvent from './types/com/atproto/admin/emitModerationEvent' export * as ComAtprotoAdminEnableAccountInvites from './types/com/atproto/admin/enableAccountInvites' export * as ComAtprotoAdminGetAccountInfo from './types/com/atproto/admin/getAccountInfo' +export * as ComAtprotoAdminGetAccountInfos from './types/com/atproto/admin/getAccountInfos' export * as ComAtprotoAdminGetInviteCodes from './types/com/atproto/admin/getInviteCodes' export * as ComAtprotoAdminGetModerationEvent from './types/com/atproto/admin/getModerationEvent' export * as ComAtprotoAdminGetRecord from './types/com/atproto/admin/getRecord' @@ -441,6 +443,17 @@ export class AdminNS { }) } + getAccountInfos( + params?: ComAtprotoAdminGetAccountInfos.QueryParams, + opts?: ComAtprotoAdminGetAccountInfos.CallOptions, + ): Promise { + return this._service.xrpc + .call('com.atproto.admin.getAccountInfos', params, undefined, opts) + .catch((e) => { + throw ComAtprotoAdminGetAccountInfos.toKnownErr(e) + }) + } + getInviteCodes( params?: ComAtprotoAdminGetInviteCodes.QueryParams, opts?: ComAtprotoAdminGetInviteCodes.CallOptions, diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index c0e7e51fddc..258d297c69e 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -436,6 +436,12 @@ export const schemaDict = { email: { type: 'string', }, + relatedRecords: { + type: 'array', + items: { + type: 'unknown', + }, + }, indexedAt: { type: 'string', format: 'datetime', @@ -1046,6 +1052,45 @@ export const schemaDict = { }, }, }, + ComAtprotoAdminGetAccountInfos: { + lexicon: 1, + id: 'com.atproto.admin.getAccountInfos', + defs: { + main: { + type: 'query', + description: 'Get details about some accounts.', + parameters: { + type: 'params', + required: ['dids'], + properties: { + dids: { + type: 'array', + items: { + type: 'string', + format: 'did', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['infos'], + properties: { + infos: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#accountView', + }, + }, + }, + }, + }, + }, + }, + }, ComAtprotoAdminGetInviteCodes: { lexicon: 1, id: 'com.atproto.admin.getInviteCodes', @@ -7875,6 +7920,7 @@ export const ids = { ComAtprotoAdminEmitModerationEvent: 'com.atproto.admin.emitModerationEvent', ComAtprotoAdminEnableAccountInvites: 'com.atproto.admin.enableAccountInvites', ComAtprotoAdminGetAccountInfo: 'com.atproto.admin.getAccountInfo', + ComAtprotoAdminGetAccountInfos: 'com.atproto.admin.getAccountInfos', ComAtprotoAdminGetInviteCodes: 'com.atproto.admin.getInviteCodes', ComAtprotoAdminGetModerationEvent: 'com.atproto.admin.getModerationEvent', ComAtprotoAdminGetRecord: 'com.atproto.admin.getRecord', diff --git a/packages/api/src/client/types/com/atproto/admin/defs.ts b/packages/api/src/client/types/com/atproto/admin/defs.ts index d4b35ae8056..aea27e86905 100644 --- a/packages/api/src/client/types/com/atproto/admin/defs.ts +++ b/packages/api/src/client/types/com/atproto/admin/defs.ts @@ -255,6 +255,7 @@ export interface AccountView { did: string handle: string email?: string + relatedRecords?: {}[] indexedAt: string invitedBy?: ComAtprotoServerDefs.InviteCode invites?: ComAtprotoServerDefs.InviteCode[] diff --git a/packages/api/src/client/types/com/atproto/admin/getAccountInfos.ts b/packages/api/src/client/types/com/atproto/admin/getAccountInfos.ts new file mode 100644 index 00000000000..353f3150854 --- /dev/null +++ b/packages/api/src/client/types/com/atproto/admin/getAccountInfos.ts @@ -0,0 +1,36 @@ +/** + * 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 ComAtprotoAdminDefs from './defs' + +export interface QueryParams { + dids: string[] +} + +export type InputSchema = undefined + +export interface OutputSchema { + infos: ComAtprotoAdminDefs.AccountView[] + [k: string]: unknown +} + +export interface CallOptions { + headers?: Headers +} + +export interface Response { + success: boolean + headers: Headers + data: OutputSchema +} + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/bsky/src/api/app/bsky/actor/getProfile.ts b/packages/bsky/src/api/app/bsky/actor/getProfile.ts index 0dacf02bcf5..47c2f8f8ca7 100644 --- a/packages/bsky/src/api/app/bsky/actor/getProfile.ts +++ b/packages/bsky/src/api/app/bsky/actor/getProfile.ts @@ -16,18 +16,16 @@ import { ModerationService } from '../../../../services/moderation' export default function (server: Server, ctx: AppContext) { const getProfile = createPipeline(skeleton, hydration, noRules, presentation) server.app.bsky.actor.getProfile({ - auth: ctx.authOptionalAccessOrRoleVerifier, + auth: ctx.authVerifier.optionalStandardOrRole, handler: async ({ auth, params, res }) => { const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) const modService = ctx.services.moderation(ctx.db.getPrimary()) - const viewer = 'did' in auth.credentials ? auth.credentials.did : null - const canViewTakendownProfile = - auth.credentials.type === 'role' && auth.credentials.triage + const { viewer, canViewTakedowns } = ctx.authVerifier.parseCreds(auth) const [result, repoRev] = await Promise.allSettled([ getProfile( - { ...params, viewer, canViewTakendownProfile }, + { ...params, viewer, canViewTakedowns }, { db, actorService, modService }, ), actorService.getRepoRev(viewer), @@ -52,15 +50,14 @@ const skeleton = async ( params: Params, ctx: Context, ): Promise => { - const { actorService, modService } = ctx - const { canViewTakendownProfile } = params + const { actorService } = ctx + const { canViewTakedowns } = params const actor = await actorService.getActor(params.actor, true) if (!actor) { throw new InvalidRequestError('Profile not found') } - if (!canViewTakendownProfile && softDeleted(actor)) { - const isSuspended = await modService.isSubjectSuspended(actor.did) - if (isSuspended) { + if (!canViewTakedowns && softDeleted(actor)) { + if (actor.takedownRef?.includes('SUSPEND')) { throw new InvalidRequestError( 'Account has been temporarily suspended', 'AccountTakedown', @@ -78,10 +75,10 @@ const skeleton = async ( const hydration = async (state: SkeletonState, ctx: Context) => { const { actorService } = ctx const { params, actor } = state - const { viewer, canViewTakendownProfile } = params + const { viewer, canViewTakedowns } = params const hydration = await actorService.views.profileDetailHydration( [actor.did], - { viewer, includeSoftDeleted: canViewTakendownProfile }, + { viewer, includeSoftDeleted: canViewTakedowns }, ) return { ...state, ...hydration } } @@ -110,7 +107,7 @@ type Context = { type Params = QueryParams & { viewer: string | null - canViewTakendownProfile: boolean + canViewTakedowns: boolean } type SkeletonState = { params: Params; actor: Actor } diff --git a/packages/bsky/src/api/app/bsky/actor/getProfiles.ts b/packages/bsky/src/api/app/bsky/actor/getProfiles.ts index f2e0eb3fd50..21ca13949d2 100644 --- a/packages/bsky/src/api/app/bsky/actor/getProfiles.ts +++ b/packages/bsky/src/api/app/bsky/actor/getProfiles.ts @@ -13,11 +13,11 @@ import { createPipeline, noRules } from '../../../../pipeline' export default function (server: Server, ctx: AppContext) { const getProfile = createPipeline(skeleton, hydration, noRules, presentation) server.app.bsky.actor.getProfiles({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ auth, params, res }) => { const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const [result, repoRev] = await Promise.all([ getProfile({ ...params, viewer }, { db, actorService }), diff --git a/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts b/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts index f68ba68eb66..df580521af9 100644 --- a/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts +++ b/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts @@ -17,12 +17,12 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.actor.getSuggestions({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) const graphService = ctx.services.graph(db) - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const result = await getSuggestions( { ...params, viewer }, diff --git a/packages/bsky/src/api/app/bsky/actor/searchActors.ts b/packages/bsky/src/api/app/bsky/actor/searchActors.ts index 66e934ac0b3..bcc30a6bd66 100644 --- a/packages/bsky/src/api/app/bsky/actor/searchActors.ts +++ b/packages/bsky/src/api/app/bsky/actor/searchActors.ts @@ -1,18 +1,13 @@ -import { sql } from 'kysely' import AppContext from '../../../../context' import { Server } from '../../../../lexicon' -import { - cleanQuery, - getUserSearchQuery, - SearchKeyset, -} from '../../../../services/util/search' +import { cleanQuery } from '../../../../services/util/search' export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.searchActors({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ auth, params }) => { const { cursor, limit } = params - const requester = auth.credentials.did + const requester = auth.credentials.iss const rawQuery = params.q ?? params.term const query = cleanQuery(rawQuery || '') const db = ctx.db.getReplica('search') @@ -29,15 +24,11 @@ export default function (server: Server, ctx: AppContext) { results = res.data.actors.map((a) => a.did) resCursor = res.data.cursor } else { - const res = query - ? await getUserSearchQuery(db, { query, limit, cursor }) - .select('distance') - .selectAll('actor') - .execute() - : [] - results = res.map((a) => a.did) - const keyset = new SearchKeyset(sql``, sql``) - resCursor = keyset.packFromResult(res) + const res = await ctx.services + .actor(ctx.db.getReplica('search')) + .getSearchResults({ query, limit, cursor }) + results = res.results.map((a) => a.did) + resCursor = res.cursor } const actors = await ctx.services diff --git a/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts b/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts index da612edcc87..6a3167fd2d0 100644 --- a/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts @@ -7,10 +7,10 @@ import { export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.searchActorsTypeahead({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { const { limit } = params - const requester = auth.credentials.did + const requester = auth.credentials.iss const rawQuery = params.q ?? params.term const query = cleanQuery(rawQuery || '') const db = ctx.db.getReplica('search') diff --git a/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts b/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts index 7a28e4efe67..bc4ecd7caac 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts @@ -6,10 +6,10 @@ import { TimeCidKeyset, paginate } from '../../../../db/pagination' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getActorFeeds({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ auth, params }) => { const { actor, limit, cursor } = params - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) diff --git a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts index 36e36b0100b..151e9086ca9 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts @@ -23,9 +23,9 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.feed.getActorLikes({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth, res }) => { - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) const feedService = ctx.services.feed(db) diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index 342f371f18d..f2163cd251b 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -23,14 +23,13 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.feed.getAuthorFeed({ - auth: ctx.authOptionalAccessOrRoleVerifier, + auth: ctx.authVerifier.optionalStandardOrRole, handler: async ({ params, auth, res }) => { const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) const feedService = ctx.services.feed(db) const graphService = ctx.services.graph(db) - const viewer = - auth.credentials.type === 'access' ? auth.credentials.did : null + const { viewer } = ctx.authVerifier.parseCreds(auth) const [result, repoRev] = await Promise.all([ getAuthorFeed( diff --git a/packages/bsky/src/api/app/bsky/feed/getFeed.ts b/packages/bsky/src/api/app/bsky/feed/getFeed.ts index a09258c3163..5100ec0f5ab 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeed.ts @@ -33,11 +33,11 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.feed.getFeed({ - auth: ctx.authOptionalVerifierAnyAudience, + auth: ctx.authVerifier.standardOptionalAnyAud, handler: async ({ params, auth, req }) => { const db = ctx.db.getReplica() const feedService = ctx.services.feed(db) - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const { timerSkele, timerHydr, ...result } = await getFeed( { ...params, viewer }, diff --git a/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts b/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts index 14a5688db0d..125af1db9b9 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts @@ -9,10 +9,10 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getFeedGenerator({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { const { feed } = params - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const db = ctx.db.getReplica() const feedService = ctx.services.feed(db) diff --git a/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts b/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts index 7b571ab09f6..ed6df5760cb 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts @@ -14,10 +14,10 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.feed.getFeedGenerators({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { const { feeds } = params - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const db = ctx.db.getReplica() const feedService = ctx.services.feed(db) const actorService = ctx.services.actor(db) diff --git a/packages/bsky/src/api/app/bsky/feed/getFeedSkeleton.ts b/packages/bsky/src/api/app/bsky/feed/getFeedSkeleton.ts index 5d65044f86f..4ab22c3a0b1 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeedSkeleton.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeedSkeleton.ts @@ -5,10 +5,10 @@ import { toSkeletonItem } from '../../../../feed-gen/types' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getFeedSkeleton({ - auth: ctx.authVerifierAnyAudience, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { const { feed } = params - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const localAlgo = ctx.algos[feed] if (!localAlgo) { diff --git a/packages/bsky/src/api/app/bsky/feed/getLikes.ts b/packages/bsky/src/api/app/bsky/feed/getLikes.ts index 893617f6bb0..8df916f29c9 100644 --- a/packages/bsky/src/api/app/bsky/feed/getLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getLikes.ts @@ -13,12 +13,12 @@ import { createPipeline } from '../../../../pipeline' export default function (server: Server, ctx: AppContext) { const getLikes = createPipeline(skeleton, hydration, noBlocks, presentation) server.app.bsky.feed.getLikes({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) const graphService = ctx.services.graph(db) - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const result = await getLikes( { ...params, viewer }, diff --git a/packages/bsky/src/api/app/bsky/feed/getListFeed.ts b/packages/bsky/src/api/app/bsky/feed/getListFeed.ts index fd3f0360ef3..8af7764a6b7 100644 --- a/packages/bsky/src/api/app/bsky/feed/getListFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getListFeed.ts @@ -22,9 +22,9 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.feed.getListFeed({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth, res }) => { - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) const feedService = ctx.services.feed(db) diff --git a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts index edf339513bc..18d9d3124d0 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts @@ -31,9 +31,9 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.feed.getPostThread({ - auth: ctx.authOptionalAccessOrRoleVerifier, + auth: ctx.authVerifier.optionalStandardOrRole, handler: async ({ params, auth, res }) => { - const viewer = 'did' in auth.credentials ? auth.credentials.did : null + const { viewer } = ctx.authVerifier.parseCreds(auth) const db = ctx.db.getReplica('thread') const feedService = ctx.services.feed(db) const actorService = ctx.services.actor(db) diff --git a/packages/bsky/src/api/app/bsky/feed/getPosts.ts b/packages/bsky/src/api/app/bsky/feed/getPosts.ts index 5ec4807accb..9db7cf0a252 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPosts.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPosts.ts @@ -14,12 +14,12 @@ import { ActorService } from '../../../../services/actor' export default function (server: Server, ctx: AppContext) { const getPosts = createPipeline(skeleton, hydration, noBlocks, presentation) server.app.bsky.feed.getPosts({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { const db = ctx.db.getReplica() const feedService = ctx.services.feed(db) const actorService = ctx.services.actor(db) - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const results = await getPosts( { ...params, viewer }, diff --git a/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts b/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts index 5ca5c452b63..e84bb745b42 100644 --- a/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts +++ b/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts @@ -18,12 +18,12 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.feed.getRepostedBy({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) const graphService = ctx.services.graph(db) - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const result = await getRepostedBy( { ...params, viewer }, diff --git a/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts b/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts index 35fac829039..b72a191c9aa 100644 --- a/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts +++ b/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts @@ -4,9 +4,9 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getSuggestedFeeds({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ auth }) => { - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const db = ctx.db.getReplica() const feedService = ctx.services.feed(db) diff --git a/packages/bsky/src/api/app/bsky/feed/getTimeline.ts b/packages/bsky/src/api/app/bsky/feed/getTimeline.ts index 7207f9b6584..3b6fbe70a33 100644 --- a/packages/bsky/src/api/app/bsky/feed/getTimeline.ts +++ b/packages/bsky/src/api/app/bsky/feed/getTimeline.ts @@ -22,9 +22,9 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.feed.getTimeline({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ params, auth, res }) => { - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const db = ctx.db.getReplica('timeline') const feedService = ctx.services.feed(db) const actorService = ctx.services.actor(db) diff --git a/packages/bsky/src/api/app/bsky/feed/searchPosts.ts b/packages/bsky/src/api/app/bsky/feed/searchPosts.ts index db143fc5b8c..9598c6ff88c 100644 --- a/packages/bsky/src/api/app/bsky/feed/searchPosts.ts +++ b/packages/bsky/src/api/app/bsky/feed/searchPosts.ts @@ -21,9 +21,9 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.feed.searchPosts({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ auth, params }) => { - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const db = ctx.db.getReplica('search') const feedService = ctx.services.feed(db) const actorService = ctx.services.actor(db) diff --git a/packages/bsky/src/api/app/bsky/graph/getBlocks.ts b/packages/bsky/src/api/app/bsky/graph/getBlocks.ts index 66b809d70ce..518fd2d62ec 100644 --- a/packages/bsky/src/api/app/bsky/graph/getBlocks.ts +++ b/packages/bsky/src/api/app/bsky/graph/getBlocks.ts @@ -5,10 +5,10 @@ import { notSoftDeletedClause } from '../../../../db/util' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getBlocks({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ params, auth }) => { const { limit, cursor } = params - const requester = auth.credentials.did + const requester = auth.credentials.iss const db = ctx.db.getReplica() const { ref } = db.db.dynamic diff --git a/packages/bsky/src/api/app/bsky/graph/getFollowers.ts b/packages/bsky/src/api/app/bsky/graph/getFollowers.ts index 1382c1f87c7..9fb199c7563 100644 --- a/packages/bsky/src/api/app/bsky/graph/getFollowers.ts +++ b/packages/bsky/src/api/app/bsky/graph/getFollowers.ts @@ -19,17 +19,15 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.graph.getFollowers({ - auth: ctx.authOptionalAccessOrRoleVerifier, + auth: ctx.authVerifier.optionalStandardOrRole, handler: async ({ params, auth }) => { const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) const graphService = ctx.services.graph(db) - const viewer = 'did' in auth.credentials ? auth.credentials.did : null - const canViewTakendownProfile = - auth.credentials.type === 'role' && auth.credentials.triage + const { viewer, canViewTakedowns } = ctx.authVerifier.parseCreds(auth) const result = await getFollowers( - { ...params, viewer, canViewTakendownProfile }, + { ...params, viewer, canViewTakedowns }, { db, actorService, graphService }, ) @@ -46,10 +44,10 @@ const skeleton = async ( ctx: Context, ): Promise => { const { db, actorService } = ctx - const { limit, cursor, actor, canViewTakendownProfile } = params + const { limit, cursor, actor, canViewTakedowns } = params const { ref } = db.db.dynamic - const subject = await actorService.getActor(actor, canViewTakendownProfile) + const subject = await actorService.getActor(actor, canViewTakedowns) if (!subject) { throw new InvalidRequestError(`Actor not found: ${actor}`) } @@ -58,7 +56,7 @@ const skeleton = async ( .selectFrom('follow') .where('follow.subjectDid', '=', subject.did) .innerJoin('actor as creator', 'creator.did', 'follow.creator') - .if(!canViewTakendownProfile, (qb) => + .if(!canViewTakedowns, (qb) => qb.where(notSoftDeletedClause(ref('creator'))), ) .selectAll('creator') @@ -130,7 +128,7 @@ type Context = { type Params = QueryParams & { viewer: string | null - canViewTakendownProfile: boolean + canViewTakedowns: boolean } type SkeletonState = { diff --git a/packages/bsky/src/api/app/bsky/graph/getFollows.ts b/packages/bsky/src/api/app/bsky/graph/getFollows.ts index 34b5d72a605..2195824b696 100644 --- a/packages/bsky/src/api/app/bsky/graph/getFollows.ts +++ b/packages/bsky/src/api/app/bsky/graph/getFollows.ts @@ -19,17 +19,15 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.graph.getFollows({ - auth: ctx.authOptionalAccessOrRoleVerifier, + auth: ctx.authVerifier.optionalStandardOrRole, handler: async ({ params, auth }) => { const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) const graphService = ctx.services.graph(db) - const viewer = 'did' in auth.credentials ? auth.credentials.did : null - const canViewTakendownProfile = - auth.credentials.type === 'role' && auth.credentials.triage + const { viewer, canViewTakedowns } = ctx.authVerifier.parseCreds(auth) const result = await getFollows( - { ...params, viewer, canViewTakendownProfile }, + { ...params, viewer, canViewTakedowns }, { db, actorService, graphService }, ) @@ -46,10 +44,10 @@ const skeleton = async ( ctx: Context, ): Promise => { const { db, actorService } = ctx - const { limit, cursor, actor, canViewTakendownProfile } = params + const { limit, cursor, actor, canViewTakedowns } = params const { ref } = db.db.dynamic - const creator = await actorService.getActor(actor, canViewTakendownProfile) + const creator = await actorService.getActor(actor, canViewTakedowns) if (!creator) { throw new InvalidRequestError(`Actor not found: ${actor}`) } @@ -58,7 +56,7 @@ const skeleton = async ( .selectFrom('follow') .where('follow.creator', '=', creator.did) .innerJoin('actor as subject', 'subject.did', 'follow.subjectDid') - .if(!canViewTakendownProfile, (qb) => + .if(!canViewTakedowns, (qb) => qb.where(notSoftDeletedClause(ref('subject'))), ) .selectAll('subject') @@ -131,7 +129,7 @@ type Context = { type Params = QueryParams & { viewer: string | null - canViewTakendownProfile: boolean + canViewTakedowns: boolean } type SkeletonState = { diff --git a/packages/bsky/src/api/app/bsky/graph/getList.ts b/packages/bsky/src/api/app/bsky/graph/getList.ts index 82a70848cd9..08d3f725663 100644 --- a/packages/bsky/src/api/app/bsky/graph/getList.ts +++ b/packages/bsky/src/api/app/bsky/graph/getList.ts @@ -13,12 +13,12 @@ import { createPipeline, noRules } from '../../../../pipeline' export default function (server: Server, ctx: AppContext) { const getList = createPipeline(skeleton, hydration, noRules, presentation) server.app.bsky.graph.getList({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { const db = ctx.db.getReplica() const graphService = ctx.services.graph(db) const actorService = ctx.services.actor(db) - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const result = await getList( { ...params, viewer }, diff --git a/packages/bsky/src/api/app/bsky/graph/getListBlocks.ts b/packages/bsky/src/api/app/bsky/graph/getListBlocks.ts index 03fd3496f97..b5a6e97986d 100644 --- a/packages/bsky/src/api/app/bsky/graph/getListBlocks.ts +++ b/packages/bsky/src/api/app/bsky/graph/getListBlocks.ts @@ -17,12 +17,12 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.graph.getListBlocks({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ params, auth }) => { const db = ctx.db.getReplica() const graphService = ctx.services.graph(db) const actorService = ctx.services.actor(db) - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const result = await getListBlocks( { ...params, viewer }, diff --git a/packages/bsky/src/api/app/bsky/graph/getListMutes.ts b/packages/bsky/src/api/app/bsky/graph/getListMutes.ts index ab0ac77f47c..f5f14844e32 100644 --- a/packages/bsky/src/api/app/bsky/graph/getListMutes.ts +++ b/packages/bsky/src/api/app/bsky/graph/getListMutes.ts @@ -5,10 +5,10 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getListMutes({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ params, auth }) => { const { limit, cursor } = params - const requester = auth.credentials.did + const requester = auth.credentials.iss const db = ctx.db.getReplica() const { ref } = db.db.dynamic diff --git a/packages/bsky/src/api/app/bsky/graph/getLists.ts b/packages/bsky/src/api/app/bsky/graph/getLists.ts index 73deb51900b..888963b3fa3 100644 --- a/packages/bsky/src/api/app/bsky/graph/getLists.ts +++ b/packages/bsky/src/api/app/bsky/graph/getLists.ts @@ -6,10 +6,10 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getLists({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ params, auth }) => { const { actor, limit, cursor } = params - const requester = auth.credentials.did + const requester = auth.credentials.iss const db = ctx.db.getReplica() const { ref } = db.db.dynamic diff --git a/packages/bsky/src/api/app/bsky/graph/getMutes.ts b/packages/bsky/src/api/app/bsky/graph/getMutes.ts index e69803d144a..2481e8de240 100644 --- a/packages/bsky/src/api/app/bsky/graph/getMutes.ts +++ b/packages/bsky/src/api/app/bsky/graph/getMutes.ts @@ -5,10 +5,10 @@ import { notSoftDeletedClause } from '../../../../db/util' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getMutes({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ params, auth }) => { const { limit, cursor } = params - const requester = auth.credentials.did + const requester = auth.credentials.iss const db = ctx.db.getReplica() const { ref } = db.db.dynamic diff --git a/packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts b/packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts index eddf0cd5fd6..3aec8ded48e 100644 --- a/packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts +++ b/packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts @@ -9,10 +9,10 @@ const RESULT_LENGTH = 10 export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getSuggestedFollowsByActor({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ auth, params }) => { const { actor } = params - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) diff --git a/packages/bsky/src/api/app/bsky/graph/muteActor.ts b/packages/bsky/src/api/app/bsky/graph/muteActor.ts index 50a3723db6e..acf72bdd2eb 100644 --- a/packages/bsky/src/api/app/bsky/graph/muteActor.ts +++ b/packages/bsky/src/api/app/bsky/graph/muteActor.ts @@ -4,10 +4,10 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.muteActor({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ auth, input }) => { const { actor } = input.body - const requester = auth.credentials.did + const requester = auth.credentials.iss const db = ctx.db.getPrimary() const subjectDid = await ctx.services.actor(db).getActorDid(actor) diff --git a/packages/bsky/src/api/app/bsky/graph/muteActorList.ts b/packages/bsky/src/api/app/bsky/graph/muteActorList.ts index b6b29796c5c..d732c3cd89f 100644 --- a/packages/bsky/src/api/app/bsky/graph/muteActorList.ts +++ b/packages/bsky/src/api/app/bsky/graph/muteActorList.ts @@ -6,10 +6,10 @@ import { AtUri } from '@atproto/syntax' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.muteActorList({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ auth, input }) => { const { list } = input.body - const requester = auth.credentials.did + const requester = auth.credentials.iss const db = ctx.db.getPrimary() diff --git a/packages/bsky/src/api/app/bsky/graph/unmuteActor.ts b/packages/bsky/src/api/app/bsky/graph/unmuteActor.ts index 11af919126f..5308aef4f47 100644 --- a/packages/bsky/src/api/app/bsky/graph/unmuteActor.ts +++ b/packages/bsky/src/api/app/bsky/graph/unmuteActor.ts @@ -4,10 +4,10 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.unmuteActor({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ auth, input }) => { const { actor } = input.body - const requester = auth.credentials.did + const requester = auth.credentials.iss const db = ctx.db.getPrimary() const subjectDid = await ctx.services.actor(db).getActorDid(actor) diff --git a/packages/bsky/src/api/app/bsky/graph/unmuteActorList.ts b/packages/bsky/src/api/app/bsky/graph/unmuteActorList.ts index 8b97530c216..059fa5605d9 100644 --- a/packages/bsky/src/api/app/bsky/graph/unmuteActorList.ts +++ b/packages/bsky/src/api/app/bsky/graph/unmuteActorList.ts @@ -3,10 +3,10 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.unmuteActorList({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ auth, input }) => { const { list } = input.body - const requester = auth.credentials.did + const requester = auth.credentials.iss const db = ctx.db.getPrimary() await ctx.services.graph(db).unmuteActorList({ diff --git a/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts b/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts index c23d7683abe..71391457902 100644 --- a/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts +++ b/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts @@ -6,9 +6,9 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.app.bsky.notification.getUnreadCount({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ auth, params }) => { - const requester = auth.credentials.did + const requester = auth.credentials.iss if (params.seenAt) { throw new InvalidRequestError('The seenAt parameter is unsupported') } diff --git a/packages/bsky/src/api/app/bsky/notification/listNotifications.ts b/packages/bsky/src/api/app/bsky/notification/listNotifications.ts index 672e8c0997a..c0de1925120 100644 --- a/packages/bsky/src/api/app/bsky/notification/listNotifications.ts +++ b/packages/bsky/src/api/app/bsky/notification/listNotifications.ts @@ -20,13 +20,13 @@ export default function (server: Server, ctx: AppContext) { presentation, ) server.app.bsky.notification.listNotifications({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ params, auth }) => { const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) const graphService = ctx.services.graph(db) const labelService = ctx.services.label(db) - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const result = await listNotifications( { ...params, viewer }, diff --git a/packages/bsky/src/api/app/bsky/notification/registerPush.ts b/packages/bsky/src/api/app/bsky/notification/registerPush.ts index be7d373bcd4..9645cd76c83 100644 --- a/packages/bsky/src/api/app/bsky/notification/registerPush.ts +++ b/packages/bsky/src/api/app/bsky/notification/registerPush.ts @@ -5,13 +5,11 @@ import { Platform } from '../../../../notifications' export default function (server: Server, ctx: AppContext) { server.app.bsky.notification.registerPush({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ auth, input }) => { const { token, platform, serviceDid, appId } = input.body - const { - credentials: { did }, - } = auth - if (serviceDid !== auth.artifacts.aud) { + const did = auth.credentials.iss + if (serviceDid !== auth.credentials.aud) { throw new InvalidRequestError('Invalid serviceDid.') } const { notifServer } = ctx diff --git a/packages/bsky/src/api/app/bsky/notification/updateSeen.ts b/packages/bsky/src/api/app/bsky/notification/updateSeen.ts index b7c705c0889..4b8b614fbad 100644 --- a/packages/bsky/src/api/app/bsky/notification/updateSeen.ts +++ b/packages/bsky/src/api/app/bsky/notification/updateSeen.ts @@ -5,10 +5,10 @@ import { excluded } from '../../../../db/util' export default function (server: Server, ctx: AppContext) { server.app.bsky.notification.updateSeen({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ input, auth }) => { const { seenAt } = input.body - const viewer = auth.credentials.did + const viewer = auth.credentials.iss let parsed: string try { diff --git a/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts index e135d2cb7c1..b8456d111a4 100644 --- a/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -8,10 +8,10 @@ import { GeneratorView } from '../../../../lexicon/types/app/bsky/feed/defs' // THIS IS A TEMPORARY UNSPECCED ROUTE export default function (server: Server, ctx: AppContext) { server.app.bsky.unspecced.getPopularFeedGenerators({ - auth: ctx.authOptionalVerifier, + auth: ctx.authVerifier.standardOptional, handler: async ({ auth, params }) => { const { limit, cursor, query } = params - const requester = auth.credentials.did + const requester = auth.credentials.iss const db = ctx.db.getReplica() const { ref } = db.db.dynamic const feedService = ctx.services.feed(db) diff --git a/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts b/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts index 821eeda655f..f45b657af1e 100644 --- a/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts +++ b/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts @@ -6,11 +6,11 @@ import { toSkeletonItem } from '../../../../feed-gen/types' // THIS IS A TEMPORARY UNSPECCED ROUTE export default function (server: Server, ctx: AppContext) { server.app.bsky.unspecced.getTimelineSkeleton({ - auth: ctx.authVerifier, + auth: ctx.authVerifier.standard, handler: async ({ auth, params }) => { const db = ctx.db.getReplica('timeline') const feedService = ctx.services.feed(db) - const viewer = auth.credentials.did + const viewer = auth.credentials.iss const result = await skeleton({ ...params, viewer }, { db, feedService }) diff --git a/packages/bsky/src/api/blob-resolver.ts b/packages/bsky/src/api/blob-resolver.ts index 7eb245eedd5..c307152c43a 100644 --- a/packages/bsky/src/api/blob-resolver.ts +++ b/packages/bsky/src/api/blob-resolver.ts @@ -10,7 +10,6 @@ import AppContext from '../context' import { httpLogger as log } from '../logger' import { retryHttp } from '../util/retry' import { Database } from '../db' -import { sql } from 'kysely' // Resolve and verify blob from its origin host @@ -88,10 +87,10 @@ export async function resolveBlob( const [{ pds }, takedown] = await Promise.all([ idResolver.did.resolveAtprotoData(did), // @TODO cache did info db.db - .selectFrom('moderation_subject_status') - .select('id') - .where('blobCids', '@>', sql`CAST(${JSON.stringify([cidStr])} AS JSONB)`) - .where('takendown', 'is', true) + .selectFrom('blob_takedown') + .select('takedownRef') + .where('did', '=', did) + .where('cid', '=', cid.toString()) .executeTakeFirst(), ]) if (takedown) { diff --git a/packages/bsky/src/api/com/atproto/admin/emitModerationEvent.ts b/packages/bsky/src/api/com/atproto/admin/emitModerationEvent.ts deleted file mode 100644 index 8b007f64ca1..00000000000 --- a/packages/bsky/src/api/com/atproto/admin/emitModerationEvent.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/syntax' -import { - AuthRequiredError, - InvalidRequestError, - UpstreamFailureError, -} from '@atproto/xrpc-server' -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' -import { getSubject } from '../moderation/util' -import { - isModEventLabel, - isModEventReverseTakedown, - isModEventTakedown, -} from '../../../../lexicon/types/com/atproto/admin/defs' -import { TakedownSubjects } from '../../../../services/moderation' -import { retryHttp } from '../../../../util/retry' - -export default function (server: Server, ctx: AppContext) { - server.com.atproto.admin.emitModerationEvent({ - auth: ctx.roleVerifier, - handler: async ({ input, auth }) => { - const access = auth.credentials - const db = ctx.db.getPrimary() - const moderationService = ctx.services.moderation(db) - const { subject, createdBy, subjectBlobCids, event } = input.body - const isTakedownEvent = isModEventTakedown(event) - const isReverseTakedownEvent = isModEventReverseTakedown(event) - const isLabelEvent = isModEventLabel(event) - - // apply access rules - - // if less than moderator access then can not takedown an account - if (!access.moderator && isTakedownEvent && 'did' in subject) { - throw new AuthRequiredError( - 'Must be a full moderator to perform an account takedown', - ) - } - // if less than moderator access then can only take ack and escalation actions - if (!access.moderator && (isTakedownEvent || isReverseTakedownEvent)) { - throw new AuthRequiredError( - 'Must be a full moderator to take this type of action', - ) - } - // if less than moderator access then can not apply labels - if (!access.moderator && isLabelEvent) { - throw new AuthRequiredError('Must be a full moderator to label content') - } - - if (isLabelEvent) { - validateLabels([ - ...(event.createLabelVals ?? []), - ...(event.negateLabelVals ?? []), - ]) - } - - const subjectInfo = getSubject(subject) - - if (isTakedownEvent || isReverseTakedownEvent) { - const isSubjectTakendown = await moderationService.isSubjectTakendown( - subjectInfo, - ) - - if (isSubjectTakendown && isTakedownEvent) { - throw new InvalidRequestError(`Subject is already taken down`) - } - - if (!isSubjectTakendown && isReverseTakedownEvent) { - throw new InvalidRequestError(`Subject is not taken down`) - } - } - - const { result: moderationEvent, takenDown } = await db.transaction( - async (dbTxn) => { - const moderationTxn = ctx.services.moderation(dbTxn) - const labelTxn = ctx.services.label(dbTxn) - - const result = await moderationTxn.logEvent({ - event, - subject: subjectInfo, - subjectBlobCids: - subjectBlobCids?.map((cid) => CID.parse(cid)) ?? [], - createdBy, - }) - - let takenDown: TakedownSubjects | undefined - - if ( - result.subjectType === 'com.atproto.admin.defs#repoRef' && - result.subjectDid - ) { - // No credentials to revoke on appview - if (isTakedownEvent) { - takenDown = await moderationTxn.takedownRepo({ - takedownId: result.id, - did: result.subjectDid, - }) - } - - if (isReverseTakedownEvent) { - await moderationTxn.reverseTakedownRepo({ - did: result.subjectDid, - }) - takenDown = { - subjects: [ - { - $type: 'com.atproto.admin.defs#repoRef', - did: result.subjectDid, - }, - ], - did: result.subjectDid, - } - } - } - - if ( - result.subjectType === 'com.atproto.repo.strongRef' && - result.subjectUri - ) { - const blobCids = subjectBlobCids?.map((cid) => CID.parse(cid)) ?? [] - if (isTakedownEvent) { - takenDown = await moderationTxn.takedownRecord({ - takedownId: result.id, - uri: new AtUri(result.subjectUri), - // TODO: I think this will always be available for strongRefs? - cid: CID.parse(result.subjectCid as string), - blobCids, - }) - } - - if (isReverseTakedownEvent) { - await moderationTxn.reverseTakedownRecord({ - uri: new AtUri(result.subjectUri), - }) - takenDown = { - did: result.subjectDid, - subjects: [ - { - $type: 'com.atproto.repo.strongRef', - uri: result.subjectUri, - cid: result.subjectCid ?? '', - }, - ...blobCids.map((cid) => ({ - $type: 'com.atproto.admin.defs#repoBlobRef', - did: result.subjectDid, - cid: cid.toString(), - recordUri: result.subjectUri, - })), - ], - } - } - } - - if (isLabelEvent) { - await labelTxn.formatAndCreate( - ctx.cfg.labelerDid, - result.subjectUri ?? result.subjectDid, - result.subjectCid, - { - create: result.createLabelVals?.length - ? result.createLabelVals.split(' ') - : undefined, - negate: result.negateLabelVals?.length - ? result.negateLabelVals.split(' ') - : undefined, - }, - ) - } - - return { result, takenDown } - }, - ) - - if (takenDown && ctx.moderationPushAgent) { - const { did, subjects } = takenDown - if (did && subjects.length > 0) { - const agent = ctx.moderationPushAgent - const results = await Promise.allSettled( - subjects.map((subject) => - retryHttp(() => - agent.api.com.atproto.admin.updateSubjectStatus({ - subject, - takedown: isTakedownEvent - ? { - applied: true, - ref: moderationEvent.id.toString(), - } - : { - applied: false, - }, - }), - ), - ), - ) - const hadFailure = results.some((r) => r.status === 'rejected') - if (hadFailure) { - throw new UpstreamFailureError('failed to apply action on PDS') - } - } - } - - return { - encoding: 'application/json', - body: await moderationService.views.event(moderationEvent), - } - }, - }) -} - -const validateLabels = (labels: string[]) => { - for (const label of labels) { - for (const char of badChars) { - if (label.includes(char)) { - throw new InvalidRequestError(`Invalid label: ${label}`) - } - } - } -} - -const badChars = [' ', ',', ';', `'`, `"`] diff --git a/packages/bsky/src/api/com/atproto/admin/getAccountInfos.ts b/packages/bsky/src/api/com/atproto/admin/getAccountInfos.ts new file mode 100644 index 00000000000..9ef66c94c9b --- /dev/null +++ b/packages/bsky/src/api/com/atproto/admin/getAccountInfos.ts @@ -0,0 +1,42 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { Actor } from '../../../../db/tables/actor' +import { mapDefined } from '@atproto/common' +import { INVALID_HANDLE } from '@atproto/syntax' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.admin.getAccountInfos({ + auth: ctx.authVerifier.roleOrAdminService, + handler: async ({ params }) => { + const { dids } = params + const db = ctx.db.getPrimary() + const actorService = ctx.services.actor(db) + const [actors, profiles] = await Promise.all([ + actorService.getActors(dids, true), + actorService.getProfileRecords(dids, true), + ]) + const actorByDid = actors.reduce((acc, cur) => { + return acc.set(cur.did, cur) + }, new Map()) + + const infos = mapDefined(dids, (did) => { + const info = actorByDid.get(did) + if (!info) return + const profile = profiles.get(did) + return { + did, + handle: info.handle ?? INVALID_HANDLE, + relatedRecords: profile ? [profile] : undefined, + indexedAt: info.indexedAt, + } + }) + + return { + encoding: 'application/json', + body: { + infos, + }, + } + }, + }) +} diff --git a/packages/bsky/src/api/com/atproto/admin/getModerationEvent.ts b/packages/bsky/src/api/com/atproto/admin/getModerationEvent.ts deleted file mode 100644 index 347a450c727..00000000000 --- a/packages/bsky/src/api/com/atproto/admin/getModerationEvent.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.com.atproto.admin.getModerationEvent({ - auth: ctx.roleVerifier, - handler: async ({ params }) => { - const { id } = params - const db = ctx.db.getPrimary() - const moderationService = ctx.services.moderation(db) - const event = await moderationService.getEventOrThrow(id) - const eventDetail = await moderationService.views.eventDetail(event) - return { - encoding: 'application/json', - body: eventDetail, - } - }, - }) -} diff --git a/packages/bsky/src/api/com/atproto/admin/getSubjectStatus.ts b/packages/bsky/src/api/com/atproto/admin/getSubjectStatus.ts new file mode 100644 index 00000000000..8ac237240f9 --- /dev/null +++ b/packages/bsky/src/api/com/atproto/admin/getSubjectStatus.ts @@ -0,0 +1,73 @@ +import { InvalidRequestError } from '@atproto/xrpc-server' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { OutputSchema } from '../../../../lexicon/types/com/atproto/admin/getSubjectStatus' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.admin.getSubjectStatus({ + auth: ctx.authVerifier.roleOrAdminService, + handler: async ({ params }) => { + const { did, uri, blob } = params + const modService = ctx.services.moderation(ctx.db.getPrimary()) + let body: OutputSchema | null = null + if (blob) { + if (!did) { + throw new InvalidRequestError( + 'Must provide a did to request blob state', + ) + } + const takedown = await modService.getBlobTakedownRef(did, blob) + if (takedown) { + body = { + subject: { + $type: 'com.atproto.admin.defs#repoBlobRef', + did: did, + cid: blob, + }, + takedown, + } + } + } else if (uri) { + const [takedown, cidRes] = await Promise.all([ + modService.getRecordTakedownRef(uri), + ctx.db + .getPrimary() + .db.selectFrom('record') + .where('uri', '=', uri) + .select('cid') + .executeTakeFirst(), + ]) + if (cidRes && takedown) { + body = { + subject: { + $type: 'com.atproto.repo.strongRef', + uri, + cid: cidRes.cid, + }, + takedown, + } + } + } else if (did) { + const takedown = await modService.getRepoTakedownRef(did) + if (takedown) { + body = { + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: did, + }, + takedown, + } + } + } else { + throw new InvalidRequestError('No provided subject') + } + if (body === null) { + throw new InvalidRequestError('Subject not found', 'NotFound') + } + return { + encoding: 'application/json', + body, + } + }, + }) +} diff --git a/packages/bsky/src/api/com/atproto/admin/searchRepos.ts b/packages/bsky/src/api/com/atproto/admin/searchRepos.ts deleted file mode 100644 index ef580f30d67..00000000000 --- a/packages/bsky/src/api/com/atproto/admin/searchRepos.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.com.atproto.admin.searchRepos({ - auth: ctx.roleVerifier, - handler: async ({ params }) => { - const db = ctx.db.getPrimary() - const moderationService = ctx.services.moderation(db) - const { limit, cursor } = params - // prefer new 'q' query param over deprecated 'term' - const query = params.q ?? params.term - - const { results, cursor: resCursor } = await ctx.services - .actor(db) - .getSearchResults({ query, limit, cursor, includeSoftDeleted: true }) - - return { - encoding: 'application/json', - body: { - cursor: resCursor, - repos: await moderationService.views.repo(results), - }, - } - }, - }) -} diff --git a/packages/bsky/src/api/com/atproto/admin/updateSubjectStatus.ts b/packages/bsky/src/api/com/atproto/admin/updateSubjectStatus.ts new file mode 100644 index 00000000000..a7875280137 --- /dev/null +++ b/packages/bsky/src/api/com/atproto/admin/updateSubjectStatus.ts @@ -0,0 +1,74 @@ +import { AtUri } from '@atproto/syntax' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { + isRepoRef, + isRepoBlobRef, +} from '../../../../lexicon/types/com/atproto/admin/defs' +import { isMain as isStrongRef } from '../../../../lexicon/types/com/atproto/repo/strongRef' +import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' +import { CID } from 'multiformats/cid' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.admin.updateSubjectStatus({ + auth: ctx.authVerifier.roleOrAdminService, + handler: async ({ input, auth }) => { + const { canPerformTakedown } = ctx.authVerifier.parseCreds(auth) + if (!canPerformTakedown) { + throw new AuthRequiredError( + 'Must be a full moderator to update subject state', + ) + } + + const modService = ctx.services.moderation(ctx.db.getPrimary()) + + const { subject, takedown } = input.body + if (takedown) { + if (isRepoRef(subject)) { + const did = subject.did + if (takedown.applied) { + await modService.takedownRepo({ + takedownRef: takedown.ref ?? new Date().toISOString(), + did, + }) + } else { + await modService.reverseTakedownRepo({ did }) + } + } else if (isStrongRef(subject)) { + const uri = new AtUri(subject.uri) + const cid = CID.parse(subject.cid) + if (takedown.applied) { + await modService.takedownRecord({ + takedownRef: takedown.ref ?? new Date().toISOString(), + uri, + cid, + }) + } else { + await modService.reverseTakedownRecord({ uri }) + } + } else if (isRepoBlobRef(subject)) { + const { did, cid } = subject + if (takedown.applied) { + await modService.takedownBlob({ + takedownRef: takedown.ref ?? new Date().toISOString(), + did, + cid, + }) + } else { + await modService.reverseTakedownBlob({ did, cid }) + } + } else { + throw new InvalidRequestError('Invalid subject') + } + } + + return { + encoding: 'application/json', + body: { + subject, + takedown, + }, + } + }, + }) +} diff --git a/packages/bsky/src/api/com/atproto/moderation/createReport.ts b/packages/bsky/src/api/com/atproto/moderation/createReport.ts deleted file mode 100644 index 4a98d0629d4..00000000000 --- a/packages/bsky/src/api/com/atproto/moderation/createReport.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { AuthRequiredError, ForbiddenError } from '@atproto/xrpc-server' -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' -import { getReasonType, getSubject } from './util' -import { softDeleted } from '../../../../db/util' -import { REASONAPPEAL } from '../../../../lexicon/types/com/atproto/moderation/defs' - -export default function (server: Server, ctx: AppContext) { - server.com.atproto.moderation.createReport({ - // @TODO anonymous reports w/ optional auth are a temporary measure - auth: ctx.authOptionalVerifier, - handler: async ({ input, auth }) => { - const { reasonType, reason, subject } = input.body - const requester = auth.credentials.did - - const db = ctx.db.getPrimary() - - if (requester) { - // Don't accept reports from users that are fully taken-down - const actor = await ctx.services.actor(db).getActor(requester, true) - if (actor && softDeleted(actor)) { - throw new AuthRequiredError() - } - } - - const reportReasonType = getReasonType(reasonType) - const reportSubject = getSubject(subject) - const subjectDid = - 'did' in reportSubject ? reportSubject.did : reportSubject.uri.host - - // If the report is an appeal, the requester must be the author of the subject - if (reasonType === REASONAPPEAL && requester !== subjectDid) { - throw new ForbiddenError('You cannot appeal this report') - } - - const report = await db.transaction(async (dbTxn) => { - const moderationTxn = ctx.services.moderation(dbTxn) - return moderationTxn.report({ - reasonType: reportReasonType, - reason, - subject: reportSubject, - reportedBy: requester || ctx.cfg.serverDid, - }) - }) - - const moderationService = ctx.services.moderation(db) - return { - encoding: 'application/json', - body: moderationService.views.reportPublic(report), - } - }, - }) -} diff --git a/packages/bsky/src/api/index.ts b/packages/bsky/src/api/index.ts index da21b582019..cd99f0ad4dd 100644 --- a/packages/bsky/src/api/index.ts +++ b/packages/bsky/src/api/index.ts @@ -40,16 +40,11 @@ import updateSeen from './app/bsky/notification/updateSeen' import registerPush from './app/bsky/notification/registerPush' import getPopularFeedGenerators from './app/bsky/unspecced/getPopularFeedGenerators' import getTimelineSkeleton from './app/bsky/unspecced/getTimelineSkeleton' -import createReport from './com/atproto/moderation/createReport' -import emitModerationEvent from './com/atproto/admin/emitModerationEvent' -import searchRepos from './com/atproto/admin/searchRepos' -import adminGetRecord from './com/atproto/admin/getRecord' -import getRepo from './com/atproto/admin/getRepo' -import queryModerationStatuses from './com/atproto/admin/queryModerationStatuses' +import getSubjectStatus from './com/atproto/admin/getSubjectStatus' +import updateSubjectStatus from './com/atproto/admin/updateSubjectStatus' +import getAccountInfos from './com/atproto/admin/getAccountInfos' import resolveHandle from './com/atproto/identity/resolveHandle' import getRecord from './com/atproto/repo/getRecord' -import queryModerationEvents from './com/atproto/admin/queryModerationEvents' -import getModerationEvent from './com/atproto/admin/getModerationEvent' import fetchLabels from './com/atproto/temp/fetchLabels' export * as health from './health' @@ -101,14 +96,9 @@ export default function (server: Server, ctx: AppContext) { getPopularFeedGenerators(server, ctx) getTimelineSkeleton(server, ctx) // com.atproto - createReport(server, ctx) - emitModerationEvent(server, ctx) - searchRepos(server, ctx) - adminGetRecord(server, ctx) - getRepo(server, ctx) - getModerationEvent(server, ctx) - queryModerationEvents(server, ctx) - queryModerationStatuses(server, ctx) + getSubjectStatus(server, ctx) + updateSubjectStatus(server, ctx) + getAccountInfos(server, ctx) resolveHandle(server, ctx) getRecord(server, ctx) fetchLabels(server, ctx) diff --git a/packages/bsky/src/auth-verifier.ts b/packages/bsky/src/auth-verifier.ts new file mode 100644 index 00000000000..5a2bf753072 --- /dev/null +++ b/packages/bsky/src/auth-verifier.ts @@ -0,0 +1,275 @@ +import { + AuthRequiredError, + verifyJwt as verifyServiceJwt, +} from '@atproto/xrpc-server' +import { IdResolver } from '@atproto/identity' +import * as ui8 from 'uint8arrays' +import express from 'express' + +type ReqCtx = { + req: express.Request +} + +export enum RoleStatus { + Valid, + Invalid, + Missing, +} + +type NullOutput = { + credentials: { + type: 'null' + iss: null + } +} + +type StandardOutput = { + credentials: { + type: 'standard' + aud: string + iss: string + } +} + +type RoleOutput = { + credentials: { + type: 'role' + admin: boolean + moderator: boolean + triage: boolean + } +} + +type AdminServiceOutput = { + credentials: { + type: 'admin_service' + aud: string + iss: string + } +} + +export type AuthVerifierOpts = { + ownDid: string + adminDid: string + adminPass: string + moderatorPass: string + triagePass: string +} + +export class AuthVerifier { + private _adminPass: string + private _moderatorPass: string + private _triagePass: string + public ownDid: string + public adminDid: string + + constructor(public idResolver: IdResolver, opts: AuthVerifierOpts) { + this._adminPass = opts.adminPass + this._moderatorPass = opts.moderatorPass + this._triagePass = opts.triagePass + this.ownDid = opts.ownDid + this.adminDid = opts.adminDid + } + + // verifiers (arrow fns to preserve scope) + + standard = async (ctx: ReqCtx): Promise => { + const { iss, aud } = await this.verifyServiceJwt(ctx, { + aud: this.ownDid, + iss: null, + }) + return { credentials: { type: 'standard', iss, aud } } + } + + standardOptional = async ( + ctx: ReqCtx, + ): Promise => { + if (isBearerToken(ctx.req)) { + return this.standard(ctx) + } + return this.nullCreds() + } + + standardOptionalAnyAud = async ( + ctx: ReqCtx, + ): Promise => { + if (!isBearerToken(ctx.req)) { + return this.nullCreds() + } + const { iss, aud } = await this.verifyServiceJwt(ctx, { + aud: null, + iss: null, + }) + return { credentials: { type: 'standard', iss, aud } } + } + + role = (ctx: ReqCtx): RoleOutput => { + const creds = this.parseRoleCreds(ctx.req) + if (creds.status !== RoleStatus.Valid) { + throw new AuthRequiredError() + } + return { + credentials: { + ...creds, + type: 'role', + }, + } + } + + standardOrRole = async ( + ctx: ReqCtx, + ): Promise => { + if (isBearerToken(ctx.req)) { + return this.standard(ctx) + } else { + return this.role(ctx) + } + } + + optionalStandardOrRole = async ( + ctx: ReqCtx, + ): Promise => { + if (isBearerToken(ctx.req)) { + return await this.standard(ctx) + } else { + const creds = this.parseRoleCreds(ctx.req) + if (creds.status === RoleStatus.Valid) { + return { + credentials: { + ...creds, + type: 'role', + }, + } + } else if (creds.status === RoleStatus.Missing) { + return this.nullCreds() + } else { + throw new AuthRequiredError() + } + } + } + + adminService = async (reqCtx: ReqCtx): Promise => { + const { iss, aud } = await this.verifyServiceJwt(reqCtx, { + aud: this.ownDid, + iss: [this.adminDid], + }) + return { credentials: { type: 'admin_service', aud, iss } } + } + + roleOrAdminService = async ( + reqCtx: ReqCtx, + ): Promise => { + if (isBearerToken(reqCtx.req)) { + return this.adminService(reqCtx) + } else { + return this.role(reqCtx) + } + } + + parseRoleCreds(req: express.Request) { + const parsed = parseBasicAuth(req.headers.authorization || '') + const { Missing, Valid, Invalid } = RoleStatus + if (!parsed) { + return { status: Missing, admin: false, moderator: false, triage: false } + } + const { username, password } = parsed + 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._triagePass) { + return { status: Valid, admin: false, moderator: false, triage: true } + } + return { status: Invalid, admin: false, moderator: false, triage: false } + } + + async verifyServiceJwt( + reqCtx: ReqCtx, + opts: { aud: string | null; iss: string[] | null }, + ) { + const getSigningKey = async ( + did: string, + forceRefresh: boolean, + ): Promise => { + if (opts.iss !== null && !opts.iss.includes(did)) { + throw new AuthRequiredError('Untrusted issuer', 'UntrustedIss') + } + return this.idResolver.did.resolveAtprotoKey(did, forceRefresh) + } + + const jwtStr = bearerTokenFromReq(reqCtx.req) + if (!jwtStr) { + throw new AuthRequiredError('missing jwt', 'MissingJwt') + } + const payload = await verifyServiceJwt(jwtStr, opts.aud, getSigningKey) + return { iss: payload.iss, aud: payload.aud } + } + + nullCreds(): NullOutput { + return { + credentials: { + type: 'null', + iss: null, + }, + } + } + + parseCreds( + creds: StandardOutput | RoleOutput | AdminServiceOutput | NullOutput, + ) { + const viewer = + creds.credentials.type === 'standard' ? creds.credentials.iss : null + const canViewTakedowns = + (creds.credentials.type === 'role' && creds.credentials.triage) || + creds.credentials.type === 'admin_service' + const canPerformTakedown = + (creds.credentials.type === 'role' && creds.credentials.moderator) || + creds.credentials.type === 'admin_service' + return { + viewer, + canViewTakedowns, + canPerformTakedown, + } + } +} + +// HELPERS +// --------- + +const BEARER = 'Bearer ' +const BASIC = 'Basic ' + +const isBearerToken = (req: express.Request): boolean => { + return req.headers.authorization?.startsWith(BEARER) ?? false +} + +const bearerTokenFromReq = (req: express.Request) => { + const header = req.headers.authorization || '' + if (!header.startsWith(BEARER)) return null + return header.slice(BEARER.length).trim() +} + +export const parseBasicAuth = ( + token: string, +): { username: string; password: string } | null => { + if (!token.startsWith(BASIC)) return null + const b64 = token.slice(BASIC.length) + let parsed: string[] + try { + parsed = ui8.toString(ui8.fromString(b64, 'base64pad'), 'utf8').split(':') + } catch (err) { + return null + } + const [username, password] = parsed + if (!username || !password) return null + return { username, password } +} + +export const buildBasicAuth = (username: string, password: string): string => { + return ( + BASIC + + ui8.toString(ui8.fromString(`${username}:${password}`, 'utf8'), 'base64pad') + ) +} diff --git a/packages/bsky/src/auto-moderator/index.ts b/packages/bsky/src/auto-moderator/index.ts index eac9e392156..dc246f0a3d4 100644 --- a/packages/bsky/src/auto-moderator/index.ts +++ b/packages/bsky/src/auto-moderator/index.ts @@ -6,16 +6,12 @@ import { PrimaryDatabase } from '../db' import { IdResolver } from '@atproto/identity' import { BackgroundQueue } from '../background' import { IndexerConfig } from '../indexer/config' -import { buildBasicAuth } from '../auth' +import { buildBasicAuth } from '../auth-verifier' import { CID } from 'multiformats/cid' -import { LabelService } from '../services/label' -import { ModerationService } from '../services/moderation' import { ImageFlagger } from './abyss' import { HiveLabeler, ImgLabeler } from './hive' import { KeywordLabeler, TextLabeler } from './keyword' import { ids } from '../lexicon/lexicons' -import { ImageUriBuilder } from '../image/uri' -import { ImageInvalidator } from '../image/invalidator' import { Abyss } from './abyss' import { FuzzyMatcher, TextFlagger } from './fuzzy-matcher' import { @@ -24,43 +20,21 @@ import { } from '../lexicon/types/com/atproto/moderation/defs' export class AutoModerator { - public pushAgent?: AtpAgent + public pushAgent: AtpAgent public imageFlagger?: ImageFlagger public textFlagger?: TextFlagger public imgLabeler?: ImgLabeler public textLabeler?: TextLabeler - services: { - label: (db: PrimaryDatabase) => LabelService - moderation?: (db: PrimaryDatabase) => ModerationService - } - constructor( public ctx: { db: PrimaryDatabase idResolver: IdResolver cfg: IndexerConfig backgroundQueue: BackgroundQueue - imgUriBuilder?: ImageUriBuilder - imgInvalidator?: ImageInvalidator }, ) { - const { imgUriBuilder, imgInvalidator } = ctx const { hiveApiKey, abyssEndpoint, abyssPassword } = ctx.cfg - this.services = { - label: LabelService.creator(null), - } - if (imgUriBuilder && imgInvalidator) { - this.services.moderation = ModerationService.creator( - imgUriBuilder, - imgInvalidator, - ) - } else { - log.error( - { imgUriBuilder, imgInvalidator }, - 'moderation service not properly configured', - ) - } this.imgLabeler = hiveApiKey ? new HiveLabeler(hiveApiKey, ctx) : undefined this.textLabeler = new KeywordLabeler(ctx.cfg.labelerKeywords) if (abyssEndpoint && abyssPassword) { @@ -79,14 +53,12 @@ export class AutoModerator { ) } - if (ctx.cfg.moderationPushUrl) { - const url = new URL(ctx.cfg.moderationPushUrl) - this.pushAgent = new AtpAgent({ service: url.origin }) - this.pushAgent.api.setHeader( - 'authorization', - buildBasicAuth(url.username, url.password), - ) - } + const url = new URL(ctx.cfg.moderationPushUrl) + this.pushAgent = new AtpAgent({ service: url.origin }) + this.pushAgent.api.setHeader( + 'authorization', + buildBasicAuth(url.username, url.password), + ) } processRecord(uri: AtUri, cid: CID, obj: unknown) { @@ -133,7 +105,7 @@ export class AutoModerator { ...imgs.map((cid) => this.imgLabeler?.labelImg(uri.host, cid)), ]) const labels = dedupe(allLabels.flat()) - await this.storeLabels(uri, recordCid, labels) + await this.pushLabels(uri, recordCid, labels) } async flagRecordText(uri: AtUri, cid: CID, text: string[]) { @@ -156,22 +128,22 @@ export class AutoModerator { if (!this.textFlagger) return const matches = this.textFlagger.getMatches(text) if (matches.length < 1) return - await this.ctx.db.transaction(async (dbTxn) => { - if (!this.services.moderation) { - log.error( - { subject, text, matches }, - 'no moderation service setup to flag record text', - ) - return - } - return this.services.moderation(dbTxn).report({ - reasonType: REASONOTHER, - reason: `Automatically flagged for possible slurs: ${matches.join( - ', ', - )}`, - subject, - reportedBy: this.ctx.cfg.labelerDid, - }) + const formattedSubject = + 'did' in subject + ? { + $type: 'com.atproto.admin.defs#repoRef', + did: subject.did, + } + : { + $type: 'com.atproto.repo.strongRef', + uri: subject.uri.toString(), + cid: subject.cid.toString(), + } + await this.pushAgent.api.com.atproto.moderation.createReport({ + reasonType: REASONOTHER, + reason: `Automatically flagged for possible slurs: ${matches.join(', ')}`, + subject: formattedSubject, + reportedBy: this.ctx.cfg.serverDid, }) } @@ -226,93 +198,49 @@ export class AutoModerator { '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, - }) - }) - } + await this.pushAgent.com.atproto.moderation.createReport({ + reportedBy: this.ctx.cfg.serverDid, + reasonType: REASONVIOLATION, + subject: { + $type: 'com.atproto.repo.strongRef', + uri: uri.toString(), + cid: recordCid.toString(), + }, + reason: reportReason, + }) - if (this.pushAgent) { - await this.pushAgent.com.atproto.admin.emitModerationEvent({ - event: { - $type: 'com.atproto.admin.defs#modEventTakedown', - comment: takedownReason, - }, - subject: { - $type: 'com.atproto.repo.strongRef', - uri: uri.toString(), - cid: recordCid.toString(), - }, - subjectBlobCids: takedownCids.map((c) => c.toString()), - createdBy: this.ctx.cfg.labelerDid, - }) - } else { - await this.ctx.db.transaction(async (dbTxn) => { - if (!this.services.moderation) { - throw new Error('no mod push agent or uri invalidator setup') - } - const modSrvc = this.services.moderation(dbTxn) - const action = await modSrvc.logEvent({ - event: { - $type: 'com.atproto.admin.defs#modEventTakedown', - comment: takedownReason, - }, - subject: { uri, cid: recordCid }, - subjectBlobCids: takedownCids, - createdBy: this.ctx.cfg.labelerDid, - }) - await modSrvc.takedownRecord({ - takedownId: action.id, - uri: uri, - cid: recordCid, - blobCids: takedownCids, - }) - }) - } + await this.pushAgent.com.atproto.admin.emitModerationEvent({ + event: { + $type: 'com.atproto.admin.defs#modEventTakedown', + comment: takedownReason, + }, + subject: { + $type: 'com.atproto.repo.strongRef', + uri: uri.toString(), + cid: recordCid.toString(), + }, + subjectBlobCids: takedownCids.map((c) => c.toString()), + createdBy: this.ctx.cfg.serverDid, + }) } - async storeLabels(uri: AtUri, cid: CID, labels: string[]): Promise { + async pushLabels(uri: AtUri, cid: CID, labels: string[]): Promise { if (labels.length < 1) return - // Given that moderation service is available, log the labeling event for historical purposes - if (this.services.moderation) { - await this.ctx.db.transaction(async (dbTxn) => { - if (!this.services.moderation) return - const modSrvc = this.services.moderation(dbTxn) - await modSrvc.logEvent({ - event: { - $type: 'com.atproto.admin.defs#modEventLabel', - createLabelVals: labels, - negateLabelVals: [], - comment: '[AutoModerator]: Applying labels', - }, - subject: { uri, cid }, - createdBy: this.ctx.cfg.labelerDid, - }) - }) - } - - const labelSrvc = this.services.label(this.ctx.db) - await labelSrvc.formatAndCreate( - this.ctx.cfg.labelerDid, - uri.toString(), - cid.toString(), - { create: labels }, - ) + await this.pushAgent.com.atproto.admin.emitModerationEvent({ + event: { + $type: 'com.atproto.admin.defs#modEventLabel', + comment: '[AutoModerator]: Applying labels', + createLabelVals: labels, + negateLabelVals: [], + }, + subject: { + $type: 'com.atproto.repo.strongRef', + uri: uri.toString(), + cid: cid.toString(), + }, + createdBy: this.ctx.cfg.serverDid, + }) } async processAll() { diff --git a/packages/bsky/src/config.ts b/packages/bsky/src/config.ts index 04134e69e21..faa1ac7953d 100644 --- a/packages/bsky/src/config.ts +++ b/packages/bsky/src/config.ts @@ -31,11 +31,10 @@ export interface ServerConfigValues { imgUriEndpoint?: string blobCacheLocation?: string searchEndpoint?: string - labelerDid: string adminPassword: string - moderatorPassword?: string - triagePassword?: string - moderationPushUrl?: string + moderatorPassword: string + triagePassword: string + modServiceDid: string rateLimitsEnabled: boolean rateLimitBypassKey?: string rateLimitBypassIps?: string[] @@ -110,14 +109,17 @@ export class ServerConfig { ) const dbPostgresSchema = process.env.DB_POSTGRES_SCHEMA assert(dbPrimaryPostgresUrl) - const adminPassword = process.env.ADMIN_PASSWORD || 'admin' + const adminPassword = process.env.ADMIN_PASSWORD || undefined + assert(adminPassword) const moderatorPassword = process.env.MODERATOR_PASSWORD || undefined + assert(moderatorPassword) const triagePassword = process.env.TRIAGE_PASSWORD || undefined - const labelerDid = process.env.LABELER_DID || 'did:example:labeler' - const moderationPushUrl = - overrides?.moderationPushUrl || - process.env.MODERATION_PUSH_URL || + assert(triagePassword) + const modServiceDid = + overrides?.modServiceDid || + process.env.MODERATION_SERVICE_DID || undefined + assert(modServiceDid) const rateLimitsEnabled = process.env.RATE_LIMITS_ENABLED === 'true' const rateLimitBypassKey = process.env.RATE_LIMIT_BYPASS_KEY const rateLimitBypassIps = process.env.RATE_LIMIT_BYPASS_IPS @@ -150,11 +152,10 @@ export class ServerConfig { imgUriEndpoint, blobCacheLocation, searchEndpoint, - labelerDid, adminPassword, moderatorPassword, triagePassword, - moderationPushUrl, + modServiceDid, rateLimitsEnabled, rateLimitBypassKey, rateLimitBypassIps, @@ -267,10 +268,6 @@ export class ServerConfig { return this.cfg.searchEndpoint } - get labelerDid() { - return this.cfg.labelerDid - } - get adminPassword() { return this.cfg.adminPassword } @@ -283,8 +280,8 @@ export class ServerConfig { return this.cfg.triagePassword } - get moderationPushUrl() { - return this.cfg.moderationPushUrl + get modServiceDid() { + return this.cfg.modServiceDid } get rateLimitsEnabled() { diff --git a/packages/bsky/src/context.ts b/packages/bsky/src/context.ts index 8c8db6b2a3c..9a3eb222cdf 100644 --- a/packages/bsky/src/context.ts +++ b/packages/bsky/src/context.ts @@ -7,15 +7,14 @@ import { DatabaseCoordinator } from './db' import { ServerConfig } from './config' import { ImageUriBuilder } from './image/uri' import { Services } from './services' -import * as auth from './auth' import DidRedisCache from './did-cache' import { BackgroundQueue } from './background' import { MountedAlgos } from './feed-gen/types' import { NotificationServer } from './notifications' import { Redis } from './redis' +import { AuthVerifier } from './auth-verifier' export class AppContext { - public moderationPushAgent: AtpAgent | undefined constructor( private opts: { db: DatabaseCoordinator @@ -30,17 +29,9 @@ export class AppContext { searchAgent?: AtpAgent algos: MountedAlgos notifServer: NotificationServer + authVerifier: AuthVerifier }, - ) { - if (opts.cfg.moderationPushUrl) { - const url = new URL(opts.cfg.moderationPushUrl) - this.moderationPushAgent = new AtpAgent({ service: url.origin }) - this.moderationPushAgent.api.setHeader( - 'authorization', - auth.buildBasicAuth(url.username, url.password), - ) - } - } + ) {} get db(): DatabaseCoordinator { return this.opts.db @@ -86,30 +77,8 @@ export class AppContext { return this.opts.searchAgent } - get authVerifier() { - return auth.authVerifier(this.idResolver, { aud: this.cfg.serverDid }) - } - - get authVerifierAnyAudience() { - return auth.authVerifier(this.idResolver, { aud: null }) - } - - get authOptionalVerifierAnyAudience() { - return auth.authOptionalVerifier(this.idResolver, { aud: null }) - } - - get authOptionalVerifier() { - return auth.authOptionalVerifier(this.idResolver, { - aud: this.cfg.serverDid, - }) - } - - get authOptionalAccessOrRoleVerifier() { - return auth.authOptionalAccessOrRoleVerifier(this.idResolver, this.cfg) - } - - get roleVerifier() { - return auth.roleVerifier(this.cfg) + get authVerifier(): AuthVerifier { + return this.opts.authVerifier } async serviceAuthJwt(aud: string) { diff --git a/packages/bsky/src/db/database-schema.ts b/packages/bsky/src/db/database-schema.ts index 70ac6495c9b..3dba50d39ea 100644 --- a/packages/bsky/src/db/database-schema.ts +++ b/packages/bsky/src/db/database-schema.ts @@ -30,6 +30,7 @@ import * as algo from './tables/algo' import * as viewParam from './tables/view-param' import * as suggestedFollow from './tables/suggested-follow' import * as suggestedFeed from './tables/suggested-feed' +import * as blobTakedown from './tables/blob-takedown' export type DatabaseSchemaType = duplicateRecord.PartialDB & profile.PartialDB & @@ -61,7 +62,8 @@ export type DatabaseSchemaType = duplicateRecord.PartialDB & algo.PartialDB & viewParam.PartialDB & suggestedFollow.PartialDB & - suggestedFeed.PartialDB + suggestedFeed.PartialDB & + blobTakedown.PartialDB export type DatabaseSchema = Kysely diff --git a/packages/bsky/src/db/migrations/20231213T181744386Z-moderation-subject-appeal.ts b/packages/bsky/src/db/migrations/20231213T181744386Z-moderation-subject-appeal.ts deleted file mode 100644 index 95662737a63..00000000000 --- a/packages/bsky/src/db/migrations/20231213T181744386Z-moderation-subject-appeal.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .alterTable('moderation_subject_status') - .addColumn('lastAppealedAt', 'varchar') - .execute() - await db.schema - .alterTable('moderation_subject_status') - .addColumn('appealed', 'boolean') - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema - .alterTable('moderation_subject_status') - .dropColumn('lastAppealedAt') - .execute() - await db.schema - .alterTable('moderation_subject_status') - .dropColumn('appealed') - .execute() -} diff --git a/packages/bsky/src/db/migrations/20231220T225126090Z-blob-takedowns.ts b/packages/bsky/src/db/migrations/20231220T225126090Z-blob-takedowns.ts new file mode 100644 index 00000000000..ce8d03cae54 --- /dev/null +++ b/packages/bsky/src/db/migrations/20231220T225126090Z-blob-takedowns.ts @@ -0,0 +1,66 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .createTable('blob_takedown') + .addColumn('did', 'varchar', (col) => col.notNull()) + .addColumn('cid', 'varchar', (col) => col.notNull()) + .addColumn('takedownRef', 'varchar', (col) => col.notNull()) + .addPrimaryKeyConstraint('blob_takedown_pkey', ['did', 'cid']) + .execute() + + await db.schema + .alterTable('actor') + .dropConstraint('actor_takedown_id_fkey') + .execute() + await db.schema.alterTable('actor').dropColumn('takedownId').execute() + await db.schema + .alterTable('actor') + .addColumn('takedownRef', 'varchar') + .execute() + + await db.schema + .alterTable('record') + .dropConstraint('record_takedown_id_fkey') + .execute() + await db.schema.alterTable('record').dropColumn('takedownId').execute() + await db.schema + .alterTable('record') + .addColumn('takedownRef', 'varchar') + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('blob_takedown').execute() + + await db.schema.alterTable('actor').dropColumn('takedownRef').execute() + await db.schema + .alterTable('actor') + .addColumn('takedownId', 'integer') + .execute() + + await db.schema + .alterTable('actor') + .addForeignKeyConstraint( + 'actor_takedown_id_fkey', + ['takedownId'], + 'moderation_event', + ['id'], + ) + .execute() + + await db.schema.alterTable('record').dropColumn('takedownRef').execute() + await db.schema + .alterTable('record') + .addColumn('takedownId', 'integer') + .execute() + await db.schema + .alterTable('record') + .addForeignKeyConstraint( + 'record_takedown_id_fkey', + ['takedownId'], + 'moderation_event', + ['id'], + ) + .execute() +} diff --git a/packages/bsky/src/db/migrations/index.ts b/packages/bsky/src/db/migrations/index.ts index ea14e775383..76272566514 100644 --- a/packages/bsky/src/db/migrations/index.ts +++ b/packages/bsky/src/db/migrations/index.ts @@ -32,4 +32,4 @@ export * as _20230920T213858047Z from './20230920T213858047Z-add-tags-to-post' export * as _20230929T192920807Z from './20230929T192920807Z-record-cursor-indexes' export * as _20231003T202833377Z from './20231003T202833377Z-create-moderation-subject-status' export * as _20231205T000257238Z from './20231205T000257238Z-remove-did-cache' -export * as _20231213T181744386Z from './20231213T181744386Z-moderation-subject-appeal' +export * as _20231220T225126090Z from './20231220T225126090Z-blob-takedowns' diff --git a/packages/bsky/src/db/periodic-moderation-event-reversal.ts b/packages/bsky/src/db/periodic-moderation-event-reversal.ts deleted file mode 100644 index 9937c113d59..00000000000 --- a/packages/bsky/src/db/periodic-moderation-event-reversal.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { wait } from '@atproto/common' -import { Leader } from './leader' -import { dbLogger } from '../logger' -import AppContext from '../context' -import { AtUri } from '@atproto/api' -import { ModerationSubjectStatusRow } from '../services/moderation/types' -import { CID } from 'multiformats/cid' -import AtpAgent from '@atproto/api' -import { retryHttp } from '../util/retry' - -export const MODERATION_ACTION_REVERSAL_ID = 1011 - -export class PeriodicModerationEventReversal { - leader = new Leader( - MODERATION_ACTION_REVERSAL_ID, - this.appContext.db.getPrimary(), - ) - destroyed = false - pushAgent?: AtpAgent - - constructor(private appContext: AppContext) { - this.pushAgent = appContext.moderationPushAgent - } - - async revertState(eventRow: ModerationSubjectStatusRow) { - await this.appContext.db.getPrimary().transaction(async (dbTxn) => { - const moderationTxn = this.appContext.services.moderation(dbTxn) - const originalEvent = - await moderationTxn.getLastReversibleEventForSubject(eventRow) - if (originalEvent) { - const { restored } = await moderationTxn.revertState({ - action: originalEvent.action, - createdBy: originalEvent.createdBy, - comment: - '[SCHEDULED_REVERSAL] Reverting action as originally scheduled', - subject: - eventRow.recordPath && eventRow.recordCid - ? { - uri: AtUri.make( - eventRow.did, - ...eventRow.recordPath.split('/'), - ), - cid: CID.parse(eventRow.recordCid), - } - : { did: eventRow.did }, - createdAt: new Date(), - }) - - const { pushAgent } = this - if ( - originalEvent.action === 'com.atproto.admin.defs#modEventTakedown' && - restored?.subjects?.length && - pushAgent - ) { - await Promise.allSettled( - restored.subjects.map((subject) => - retryHttp(() => - pushAgent.api.com.atproto.admin.updateSubjectStatus({ - subject, - takedown: { - applied: false, - }, - }), - ), - ), - ) - } - } - }) - } - - async findAndRevertDueActions() { - const moderationService = this.appContext.services.moderation( - this.appContext.db.getPrimary(), - ) - const subjectsDueForReversal = - await moderationService.getSubjectsDueForReversal() - - // We shouldn't have too many actions due for reversal at any given time, so running in parallel is probably fine - // Internally, each reversal runs within its own transaction - await Promise.all(subjectsDueForReversal.map(this.revertState.bind(this))) - } - - async run() { - while (!this.destroyed) { - try { - const { ran } = await this.leader.run(async ({ signal }) => { - while (!signal.aborted) { - // super basic synchronization by agreeing when the intervals land relative to unix timestamp - const now = Date.now() - const intervalMs = 1000 * 60 - const nextIteration = Math.ceil(now / intervalMs) - const nextInMs = nextIteration * intervalMs - now - await wait(nextInMs) - if (signal.aborted) break - await this.findAndRevertDueActions() - } - }) - if (ran && !this.destroyed) { - throw new Error('View maintainer completed, but should be persistent') - } - } catch (err) { - dbLogger.error( - { - err, - lockId: MODERATION_ACTION_REVERSAL_ID, - }, - 'moderation action reversal 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/bsky/src/db/tables/actor.ts b/packages/bsky/src/db/tables/actor.ts index 312c5808cab..3ec3864b806 100644 --- a/packages/bsky/src/db/tables/actor.ts +++ b/packages/bsky/src/db/tables/actor.ts @@ -2,7 +2,7 @@ export interface Actor { did: string handle: string | null indexedAt: string - takedownId: number | null // @TODO(bsky) + takedownRef: string | null } export const tableName = 'actor' diff --git a/packages/bsky/src/db/tables/blob-takedown.ts b/packages/bsky/src/db/tables/blob-takedown.ts new file mode 100644 index 00000000000..08ff80b1e0e --- /dev/null +++ b/packages/bsky/src/db/tables/blob-takedown.ts @@ -0,0 +1,9 @@ +export interface BlobTakedown { + did: string + cid: string + takedownRef: string +} + +export const tableName = 'blob_takedown' + +export type PartialDB = { [tableName]: BlobTakedown } diff --git a/packages/bsky/src/db/tables/moderation.ts b/packages/bsky/src/db/tables/moderation.ts index 99f5e73310d..f1ac3572785 100644 --- a/packages/bsky/src/db/tables/moderation.ts +++ b/packages/bsky/src/db/tables/moderation.ts @@ -20,7 +20,6 @@ export interface ModerationEvent { | 'com.atproto.admin.defs#modEventMute' | 'com.atproto.admin.defs#modEventReverseTakedown' | 'com.atproto.admin.defs#modEventEmail' - | 'com.atproto.admin.defs#modEventResolveAppeal' subjectType: 'com.atproto.admin.defs#repoRef' | 'com.atproto.repo.strongRef' subjectDid: string subjectUri: string | null @@ -48,11 +47,9 @@ export interface ModerationSubjectStatus { lastReviewedBy: string | null lastReviewedAt: string | null lastReportedAt: string | null - lastAppealedAt: string | null muteUntil: string | null suspendUntil: string | null takendown: boolean - appealed: boolean | null comment: string | null } diff --git a/packages/bsky/src/db/tables/record.ts b/packages/bsky/src/db/tables/record.ts index ed35cf559f2..5efe5667efa 100644 --- a/packages/bsky/src/db/tables/record.ts +++ b/packages/bsky/src/db/tables/record.ts @@ -4,7 +4,7 @@ export interface Record { did: string json: string indexedAt: string - takedownId: number | null // @TODO(bsky) + takedownRef: string | null } export const tableName = 'record' diff --git a/packages/bsky/src/db/util.ts b/packages/bsky/src/db/util.ts index dfd93e66a17..b8269ba08ac 100644 --- a/packages/bsky/src/db/util.ts +++ b/packages/bsky/src/db/util.ts @@ -20,11 +20,11 @@ export const actorWhereClause = (actor: string) => { // Applies to actor or record table export const notSoftDeletedClause = (alias: DbRef) => { - return sql`${alias}."takedownId" is null` + return sql`${alias}."takedownRef" is null` } -export const softDeleted = (actorOrRecord: { takedownId: number | null }) => { - return actorOrRecord.takedownId !== null +export const softDeleted = (actorOrRecord: { takedownRef: string | null }) => { + return actorOrRecord.takedownRef !== null } export const countAll = sql`count(*)` diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index 2f83efb3746..7c89a997310 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -33,20 +33,20 @@ import { NotificationServer } from './notifications' import { AtpAgent } from '@atproto/api' import { Keypair } from '@atproto/crypto' import { Redis } from './redis' +import { AuthVerifier } from './auth-verifier' export type { ServerConfigValues } from './config' export type { MountedAlgos } from './feed-gen/types' export { ServerConfig } from './config' export { Database, PrimaryDatabase, DatabaseCoordinator } from './db' -export { PeriodicModerationEventReversal } from './db/periodic-moderation-event-reversal' export { Redis } from './redis' export { ViewMaintainer } from './db/views' export { AppContext } from './context' +export type { ImageInvalidator } from './image/invalidator' export { makeAlgos } from './feed-gen' export * from './daemon' export * from './indexer' export * from './ingester' -export { MigrateModerationData } from './migrate-moderation-data' export class BskyAppView { public ctx: AppContext @@ -127,6 +127,14 @@ export class BskyAppView { }, }) + const authVerifier = new AuthVerifier(idResolver, { + ownDid: config.serverDid, + adminDid: config.modServiceDid, + adminPass: config.adminPassword, + moderatorPass: config.moderatorPassword, + triagePass: config.triagePassword, + }) + const ctx = new AppContext({ db, cfg: config, @@ -140,6 +148,7 @@ export class BskyAppView { searchAgent, algos, notifServer, + authVerifier, }) const xrpcOpts: XrpcServerOptions = { diff --git a/packages/bsky/src/indexer/config.ts b/packages/bsky/src/indexer/config.ts index dd8b9ab89d5..6acf86f9543 100644 --- a/packages/bsky/src/indexer/config.ts +++ b/packages/bsky/src/indexer/config.ts @@ -3,6 +3,7 @@ import { DAY, HOUR, parseIntWithFallback } from '@atproto/common' export interface IndexerConfigValues { version: string + serverDid: string dbPostgresUrl: string dbPostgresSchema?: string redisHost?: string // either set redis host, or both sentinel name and hosts @@ -13,7 +14,6 @@ export interface IndexerConfigValues { didCacheStaleTTL: number didCacheMaxTTL: number handleResolveNameservers?: string[] - labelerDid: string hiveApiKey?: string abyssEndpoint?: string abyssPassword?: string @@ -21,7 +21,7 @@ export interface IndexerConfigValues { fuzzyMatchB64?: string fuzzyFalsePositiveB64?: string labelerKeywords: Record - moderationPushUrl?: string + moderationPushUrl: string indexerConcurrency?: number indexerPartitionIds: number[] indexerPartitionBatchSize?: number @@ -37,6 +37,7 @@ export class IndexerConfig { static readEnv(overrides?: Partial) { const version = process.env.BSKY_VERSION || '0.0.0' + const serverDid = process.env.SERVER_DID || 'did:example:test' const dbPostgresUrl = overrides?.dbPostgresUrl || process.env.DB_PRIMARY_POSTGRES_URL const dbPostgresSchema = @@ -66,11 +67,11 @@ export class IndexerConfig { const handleResolveNameservers = process.env.HANDLE_RESOLVE_NAMESERVERS ? process.env.HANDLE_RESOLVE_NAMESERVERS.split(',') : [] - const labelerDid = process.env.LABELER_DID || 'did:example:labeler' const moderationPushUrl = overrides?.moderationPushUrl || process.env.MODERATION_PUSH_URL || undefined + assert(moderationPushUrl) const hiveApiKey = process.env.HIVE_API_KEY || undefined const abyssEndpoint = process.env.ABYSS_ENDPOINT const abyssPassword = process.env.ABYSS_PASSWORD @@ -101,6 +102,7 @@ export class IndexerConfig { assert(indexerPartitionIds.length > 0) return new IndexerConfig({ version, + serverDid, dbPostgresUrl, dbPostgresSchema, redisHost, @@ -111,7 +113,6 @@ export class IndexerConfig { didCacheStaleTTL, didCacheMaxTTL, handleResolveNameservers, - labelerDid, moderationPushUrl, hiveApiKey, abyssEndpoint, @@ -136,6 +137,10 @@ export class IndexerConfig { return this.cfg.version } + get serverDid() { + return this.cfg.serverDid + } + get dbPostgresUrl() { return this.cfg.dbPostgresUrl } @@ -176,10 +181,6 @@ export class IndexerConfig { return this.cfg.handleResolveNameservers } - get labelerDid() { - return this.cfg.labelerDid - } - get moderationPushUrl() { return this.cfg.moderationPushUrl } diff --git a/packages/bsky/src/indexer/index.ts b/packages/bsky/src/indexer/index.ts index 496cff67c73..fec81faa374 100644 --- a/packages/bsky/src/indexer/index.ts +++ b/packages/bsky/src/indexer/index.ts @@ -13,8 +13,6 @@ import { AutoModerator } from '../auto-moderator' import { Redis } from '../redis' import { NotificationServer } from '../notifications' import { CloseFn, createServer, startServer } from './server' -import { ImageUriBuilder } from '../image/uri' -import { ImageInvalidator } from '../image/invalidator' export { IndexerConfig } from './config' export type { IndexerConfigValues } from './config' @@ -42,7 +40,6 @@ export class BskyIndexer { redis: Redis redisCache: Redis cfg: IndexerConfig - imgInvalidator?: ImageInvalidator }): BskyIndexer { const { db, redis, redisCache, cfg } = opts const didCache = new DidRedisCache(redisCache.withNamespace('did-doc'), { @@ -56,17 +53,11 @@ export class BskyIndexer { }) const backgroundQueue = new BackgroundQueue(db) - const imgUriBuilder = cfg.imgUriEndpoint - ? new ImageUriBuilder(cfg.imgUriEndpoint) - : undefined - const imgInvalidator = opts.imgInvalidator const autoMod = new AutoModerator({ db, idResolver, cfg, backgroundQueue, - imgUriBuilder, - imgInvalidator, }) const notifServer = cfg.pushNotificationEndpoint diff --git a/packages/bsky/src/ingester/config.ts b/packages/bsky/src/ingester/config.ts index 969aeeff7aa..5c157571f2a 100644 --- a/packages/bsky/src/ingester/config.ts +++ b/packages/bsky/src/ingester/config.ts @@ -9,6 +9,7 @@ export interface IngesterConfigValues { redisSentinelHosts?: string[] redisPassword?: string repoProvider: string + labelProvider?: string ingesterPartitionCount: number ingesterNamespace?: string ingesterSubLockId?: number @@ -40,6 +41,7 @@ export class IngesterConfig { const redisPassword = overrides?.redisPassword || process.env.REDIS_PASSWORD || undefined const repoProvider = overrides?.repoProvider || process.env.REPO_PROVIDER // E.g. ws://abc.com:4000 + const labelProvider = overrides?.labelProvider || process.env.LABEL_PROVIDER const ingesterPartitionCount = overrides?.ingesterPartitionCount || maybeParseInt(process.env.INGESTER_PARTITION_COUNT) @@ -69,6 +71,7 @@ export class IngesterConfig { redisSentinelHosts, redisPassword, repoProvider, + labelProvider, ingesterPartitionCount, ingesterSubLockId, ingesterNamespace, @@ -110,6 +113,10 @@ export class IngesterConfig { return this.cfg.repoProvider } + get labelProvider() { + return this.cfg.labelProvider + } + get ingesterPartitionCount() { return this.cfg.ingesterPartitionCount } diff --git a/packages/bsky/src/ingester/context.ts b/packages/bsky/src/ingester/context.ts index 792d3c2015a..797545b9f98 100644 --- a/packages/bsky/src/ingester/context.ts +++ b/packages/bsky/src/ingester/context.ts @@ -1,6 +1,7 @@ import { PrimaryDatabase } from '../db' import { Redis } from '../redis' import { IngesterConfig } from './config' +import { LabelSubscription } from './label-subscription' export class IngesterContext { constructor( @@ -8,6 +9,7 @@ export class IngesterContext { db: PrimaryDatabase redis: Redis cfg: IngesterConfig + labelSubscription?: LabelSubscription }, ) {} @@ -22,6 +24,10 @@ export class IngesterContext { get cfg(): IngesterConfig { return this.opts.cfg } + + get labelSubscription(): LabelSubscription | undefined { + return this.opts.labelSubscription + } } export default IngesterContext diff --git a/packages/bsky/src/ingester/index.ts b/packages/bsky/src/ingester/index.ts index 376da2887da..b923b92c09c 100644 --- a/packages/bsky/src/ingester/index.ts +++ b/packages/bsky/src/ingester/index.ts @@ -5,6 +5,7 @@ import { Redis } from '../redis' import { IngesterConfig } from './config' import { IngesterContext } from './context' import { IngesterSubscription } from './subscription' +import { LabelSubscription } from './label-subscription' export { IngesterConfig } from './config' export type { IngesterConfigValues } from './config' @@ -26,7 +27,15 @@ export class BskyIngester { cfg: IngesterConfig }): BskyIngester { const { db, redis, cfg } = opts - const ctx = new IngesterContext({ db, redis, cfg }) + const labelSubscription = cfg.labelProvider + ? new LabelSubscription(db, cfg.labelProvider) + : undefined + const ctx = new IngesterContext({ + db, + redis, + cfg, + labelSubscription, + }) const sub = new IngesterSubscription(ctx, { service: cfg.repoProvider, subLockId: cfg.ingesterSubLockId, @@ -63,11 +72,13 @@ export class BskyIngester { 'ingester stats', ) }, 500) + await this.ctx.labelSubscription?.start() this.sub.run() return this } async destroy(opts?: { skipDb: boolean }): Promise { + await this.ctx.labelSubscription?.destroy() await this.sub.destroy() clearInterval(this.subStatsInterval) await this.ctx.redis.destroy() diff --git a/packages/bsky/src/ingester/label-subscription.ts b/packages/bsky/src/ingester/label-subscription.ts new file mode 100644 index 00000000000..d486473cf98 --- /dev/null +++ b/packages/bsky/src/ingester/label-subscription.ts @@ -0,0 +1,76 @@ +import AtpAgent from '@atproto/api' +import { PrimaryDatabase } from '../db' +import { sql } from 'kysely' +import { dbLogger } from '../logger' +import { SECOND } from '@atproto/common' + +export class LabelSubscription { + destroyed = false + promise: Promise = Promise.resolve() + timer: NodeJS.Timer | undefined + lastLabel: number | undefined + labelAgent: AtpAgent + + constructor(public db: PrimaryDatabase, public labelProvider: string) { + this.labelAgent = new AtpAgent({ service: labelProvider }) + } + + async start() { + const res = await this.db.db + .selectFrom('label') + .select('cts') + .orderBy('cts', 'desc') + .limit(1) + .executeTakeFirst() + this.lastLabel = res ? new Date(res.cts).getTime() : undefined + this.poll() + } + + poll() { + if (this.destroyed) return + this.promise = this.fetchLabels() + .catch((err) => + dbLogger.error({ err }, 'failed to fetch and store labels'), + ) + .finally(() => { + this.timer = setTimeout(() => this.poll(), SECOND) + }) + } + + async fetchLabels() { + const res = await this.labelAgent.api.com.atproto.temp.fetchLabels({ + since: this.lastLabel, + }) + const last = res.data.labels.at(-1) + if (!last) { + return + } + const dbVals = res.data.labels.map((l) => ({ + ...l, + cid: l.cid ?? '', + neg: l.neg ?? false, + })) + const { ref } = this.db.db.dynamic + const excluded = (col: string) => ref(`excluded.${col}`) + await this.db + .asPrimary() + .db.insertInto('label') + .values(dbVals) + .onConflict((oc) => + oc.columns(['src', 'uri', 'cid', 'val']).doUpdateSet({ + neg: sql`${excluded('neg')}`, + cts: sql`${excluded('cts')}`, + }), + ) + .execute() + this.lastLabel = new Date(last.cts).getTime() + } + + async destroy() { + this.destroyed = true + if (this.timer) { + clearTimeout(this.timer) + } + await this.promise + } +} diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index 40c50cd1687..386f77196e7 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -15,6 +15,7 @@ import * as ComAtprotoAdminDisableInviteCodes from './types/com/atproto/admin/di import * as ComAtprotoAdminEmitModerationEvent from './types/com/atproto/admin/emitModerationEvent' import * as ComAtprotoAdminEnableAccountInvites from './types/com/atproto/admin/enableAccountInvites' import * as ComAtprotoAdminGetAccountInfo from './types/com/atproto/admin/getAccountInfo' +import * as ComAtprotoAdminGetAccountInfos from './types/com/atproto/admin/getAccountInfos' import * as ComAtprotoAdminGetInviteCodes from './types/com/atproto/admin/getInviteCodes' import * as ComAtprotoAdminGetModerationEvent from './types/com/atproto/admin/getModerationEvent' import * as ComAtprotoAdminGetRecord from './types/com/atproto/admin/getRecord' @@ -265,6 +266,17 @@ export class AdminNS { return this._server.xrpc.method(nsid, cfg) } + getAccountInfos( + cfg: ConfigOf< + AV, + ComAtprotoAdminGetAccountInfos.Handler>, + ComAtprotoAdminGetAccountInfos.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.getAccountInfos' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + getInviteCodes( cfg: ConfigOf< AV, diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index c0e7e51fddc..258d297c69e 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -436,6 +436,12 @@ export const schemaDict = { email: { type: 'string', }, + relatedRecords: { + type: 'array', + items: { + type: 'unknown', + }, + }, indexedAt: { type: 'string', format: 'datetime', @@ -1046,6 +1052,45 @@ export const schemaDict = { }, }, }, + ComAtprotoAdminGetAccountInfos: { + lexicon: 1, + id: 'com.atproto.admin.getAccountInfos', + defs: { + main: { + type: 'query', + description: 'Get details about some accounts.', + parameters: { + type: 'params', + required: ['dids'], + properties: { + dids: { + type: 'array', + items: { + type: 'string', + format: 'did', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['infos'], + properties: { + infos: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#accountView', + }, + }, + }, + }, + }, + }, + }, + }, ComAtprotoAdminGetInviteCodes: { lexicon: 1, id: 'com.atproto.admin.getInviteCodes', @@ -7875,6 +7920,7 @@ export const ids = { ComAtprotoAdminEmitModerationEvent: 'com.atproto.admin.emitModerationEvent', ComAtprotoAdminEnableAccountInvites: 'com.atproto.admin.enableAccountInvites', ComAtprotoAdminGetAccountInfo: 'com.atproto.admin.getAccountInfo', + ComAtprotoAdminGetAccountInfos: 'com.atproto.admin.getAccountInfos', ComAtprotoAdminGetInviteCodes: 'com.atproto.admin.getInviteCodes', ComAtprotoAdminGetModerationEvent: 'com.atproto.admin.getModerationEvent', ComAtprotoAdminGetRecord: 'com.atproto.admin.getRecord', diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts index 4be9efb21a9..8236f848fa0 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts @@ -255,6 +255,7 @@ export interface AccountView { did: string handle: string email?: string + relatedRecords?: {}[] indexedAt: string invitedBy?: ComAtprotoServerDefs.InviteCode invites?: ComAtprotoServerDefs.InviteCode[] diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/getAccountInfos.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/getAccountInfos.ts new file mode 100644 index 00000000000..46d917293a8 --- /dev/null +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/getAccountInfos.ts @@ -0,0 +1,46 @@ +/** + * 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 ComAtprotoAdminDefs from './defs' + +export interface QueryParams { + dids: string[] +} + +export type InputSchema = undefined + +export interface OutputSchema { + infos: ComAtprotoAdminDefs.AccountView[] + [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 +} + +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/migrate-moderation-data.ts b/packages/bsky/src/migrate-moderation-data.ts deleted file mode 100644 index 6919358170a..00000000000 --- a/packages/bsky/src/migrate-moderation-data.ts +++ /dev/null @@ -1,414 +0,0 @@ -import { sql } from 'kysely' -import { DatabaseCoordinator, PrimaryDatabase } from './index' -import { adjustModerationSubjectStatus } from './services/moderation/status' -import { ModerationEventRow } from './services/moderation/types' - -type ModerationActionRow = Omit & { - reason: string | null -} - -const getEnv = () => ({ - DB_URL: - process.env.MODERATION_MIGRATION_DB_URL || - 'postgresql://pg:password@127.0.0.1:5433/postgres', - DB_POOL_SIZE: Number(process.env.MODERATION_MIGRATION_DB_POOL_SIZE) || 10, - DB_SCHEMA: process.env.MODERATION_MIGRATION_DB_SCHEMA || 'bsky', -}) - -const countEntries = async (db: PrimaryDatabase) => { - const [allActions, allReports] = await Promise.all([ - db.db - // @ts-ignore - .selectFrom('moderation_action') - // @ts-ignore - .select((eb) => eb.fn.count('id').as('count')) - .executeTakeFirstOrThrow(), - db.db - // @ts-ignore - .selectFrom('moderation_report') - // @ts-ignore - .select((eb) => eb.fn.count('id').as('count')) - .executeTakeFirstOrThrow(), - ]) - - return { reportsCount: allReports.count, actionsCount: allActions.count } -} - -const countEvents = async (db: PrimaryDatabase) => { - const events = await db.db - .selectFrom('moderation_event') - .select((eb) => eb.fn.count('id').as('count')) - .executeTakeFirstOrThrow() - - return events.count -} - -const getLatestReportLegacyRefId = async (db: PrimaryDatabase) => { - const events = await db.db - .selectFrom('moderation_event') - .select((eb) => eb.fn.max('legacyRefId').as('latestLegacyRefId')) - .where('action', '=', 'com.atproto.admin.defs#modEventReport') - .executeTakeFirstOrThrow() - - return events.latestLegacyRefId -} - -const countStatuses = async (db: PrimaryDatabase) => { - const events = await db.db - .selectFrom('moderation_subject_status') - .select((eb) => eb.fn.count('id').as('count')) - .executeTakeFirstOrThrow() - - return events.count -} - -const processLegacyReports = async ( - db: PrimaryDatabase, - legacyIds: number[], -) => { - if (!legacyIds.length) { - console.log('No legacy reports to process') - return - } - const reports = await db.db - .selectFrom('moderation_event') - .where('action', '=', 'com.atproto.admin.defs#modEventReport') - .where('legacyRefId', 'in', legacyIds) - .orderBy('legacyRefId', 'asc') - .selectAll() - .execute() - - console.log(`Processing ${reports.length} reports from ${legacyIds.length}`) - await db.transaction(async (tx) => { - // This will be slow but we need to run this in sequence - for (const report of reports) { - await adjustModerationSubjectStatus(tx, report) - } - }) - console.log(`Completed processing ${reports.length} reports`) -} - -const getReportEventsAboveLegacyId = async ( - db: PrimaryDatabase, - aboveLegacyId: number, -) => { - return await db.db - .selectFrom('moderation_event') - .where('action', '=', 'com.atproto.admin.defs#modEventReport') - .where('legacyRefId', '>', aboveLegacyId) - .select(sql`"legacyRefId"`.as('legacyRefId')) - .execute() -} - -const createEvents = async ( - db: PrimaryDatabase, - opts?: { onlyReportsAboveId: number }, -) => { - const commonColumnsToSelect = [ - 'subjectDid', - 'subjectUri', - 'subjectType', - 'subjectCid', - sql`reason`.as('comment'), - 'createdAt', - ] - const commonColumnsToInsert = [ - 'subjectDid', - 'subjectUri', - 'subjectType', - 'subjectCid', - 'comment', - 'createdAt', - 'action', - 'createdBy', - ] as const - - let totalActions: number - if (!opts?.onlyReportsAboveId) { - await db.db - .insertInto('moderation_event') - .columns([ - 'id', - ...commonColumnsToInsert, - 'createLabelVals', - 'negateLabelVals', - 'durationInHours', - 'expiresAt', - ]) - .expression((eb) => - eb - // @ts-ignore - .selectFrom('moderation_action') - // @ts-ignore - .select([ - 'id', - ...commonColumnsToSelect, - sql`CONCAT('com.atproto.admin.defs#modEvent', UPPER(SUBSTRING(SPLIT_PART(action, '#', 2) FROM 1 FOR 1)), SUBSTRING(SPLIT_PART(action, '#', 2) FROM 2))`.as( - 'action', - ), - 'createdBy', - 'createLabelVals', - 'negateLabelVals', - 'durationInHours', - 'expiresAt', - ]) - .orderBy('id', 'asc'), - ) - .execute() - - totalActions = await countEvents(db) - console.log(`Created ${totalActions} events from actions`) - - await sql`SELECT setval(pg_get_serial_sequence('moderation_event', 'id'), (select max(id) from moderation_event))`.execute( - db.db, - ) - console.log('Reset the id sequence for moderation_event') - } else { - totalActions = await countEvents(db) - } - - await db.db - .insertInto('moderation_event') - .columns([...commonColumnsToInsert, 'meta', 'legacyRefId']) - .expression((eb) => { - const builder = eb - // @ts-ignore - .selectFrom('moderation_report') - // @ts-ignore - .select([ - ...commonColumnsToSelect, - sql`'com.atproto.admin.defs#modEventReport'`.as('action'), - sql`"reportedByDid"`.as('createdBy'), - sql`json_build_object('reportType', "reasonType")`.as('meta'), - sql`id`.as('legacyRefId'), - ]) - - if (opts?.onlyReportsAboveId) { - // @ts-ignore - return builder.where('id', '>', opts.onlyReportsAboveId) - } - - return builder - }) - .execute() - - const totalEvents = await countEvents(db) - console.log(`Created ${totalEvents - totalActions} events from reports`) - - return -} - -const setReportedAtTimestamp = async (db: PrimaryDatabase) => { - console.log('Initiating lastReportedAt timestamp sync') - const didUpdate = await sql` - UPDATE moderation_subject_status - SET "lastReportedAt" = reports."createdAt" - FROM ( - select "subjectDid", "subjectUri", MAX("createdAt") as "createdAt" - from moderation_report - where "subjectUri" is null - group by "subjectDid", "subjectUri" - ) as reports - WHERE reports."subjectDid" = moderation_subject_status."did" - AND "recordPath" = '' - AND ("lastReportedAt" is null OR "lastReportedAt" < reports."createdAt") - `.execute(db.db) - - console.log( - `Updated lastReportedAt for ${didUpdate.numUpdatedOrDeletedRows} did subject`, - ) - - const contentUpdate = await sql` - UPDATE moderation_subject_status - SET "lastReportedAt" = reports."createdAt" - FROM ( - select "subjectDid", "subjectUri", MAX("createdAt") as "createdAt" - from moderation_report - where "subjectUri" is not null - group by "subjectDid", "subjectUri" - ) as reports - WHERE reports."subjectDid" = moderation_subject_status."did" - AND "recordPath" is not null - AND POSITION(moderation_subject_status."recordPath" IN reports."subjectUri") > 0 - AND ("lastReportedAt" is null OR "lastReportedAt" < reports."createdAt") - `.execute(db.db) - - console.log( - `Updated lastReportedAt for ${contentUpdate.numUpdatedOrDeletedRows} subject with uri`, - ) -} - -const createStatusFromActions = async (db: PrimaryDatabase) => { - const allEvents = await db.db - // @ts-ignore - .selectFrom('moderation_action') - // @ts-ignore - .where('reversedAt', 'is', null) - // @ts-ignore - .select((eb) => eb.fn.count('id').as('count')) - .executeTakeFirstOrThrow() - - const chunkSize = 2500 - const totalChunks = Math.ceil(allEvents.count / chunkSize) - - console.log(`Processing ${allEvents.count} actions in ${totalChunks} chunks`) - - await db.transaction(async (tx) => { - // This is not used for pagination but only for logging purposes - let currentChunk = 1 - let lastProcessedId: undefined | number = 0 - do { - const eventsQuery = tx.db - // @ts-ignore - .selectFrom('moderation_action') - // @ts-ignore - .where('reversedAt', 'is', null) - // @ts-ignore - .where('id', '>', lastProcessedId) - .limit(chunkSize) - .selectAll() - const events = (await eventsQuery.execute()) as ModerationActionRow[] - - for (const event of events) { - // Remap action to event data type - const actionParts = event.action.split('#') - await adjustModerationSubjectStatus(tx, { - ...event, - action: `com.atproto.admin.defs#modEvent${actionParts[1] - .charAt(0) - .toUpperCase()}${actionParts[1].slice( - 1, - )}` as ModerationEventRow['action'], - comment: event.reason, - meta: null, - }) - } - - console.log(`Processed events chunk ${currentChunk} of ${totalChunks}`) - lastProcessedId = events.at(-1)?.id - currentChunk++ - } while (lastProcessedId !== undefined) - }) - - console.log(`Events migration complete!`) - - const totalStatuses = await countStatuses(db) - console.log(`Created ${totalStatuses} statuses`) -} - -const remapFlagToAcknlowedge = async (db: PrimaryDatabase) => { - console.log('Initiating flag to ack remap') - const results = await sql` - UPDATE moderation_event - SET "action" = 'com.atproto.admin.defs#modEventAcknowledge' - WHERE action = 'com.atproto.admin.defs#modEventFlag' - `.execute(db.db) - console.log(`Remapped ${results.numUpdatedOrDeletedRows} flag actions to ack`) -} - -const syncBlobCids = async (db: PrimaryDatabase) => { - console.log('Initiating blob cid sync') - const results = await sql` - UPDATE moderation_subject_status - SET "blobCids" = blob_action."cids" - FROM ( - SELECT moderation_action."subjectUri", moderation_action."subjectDid", jsonb_agg(moderation_action_subject_blob."cid") as cids - FROM moderation_action_subject_blob - JOIN moderation_action - ON moderation_action.id = moderation_action_subject_blob."actionId" - WHERE moderation_action."reversedAt" is NULL - GROUP by moderation_action."subjectUri", moderation_action."subjectDid" - ) as blob_action - WHERE did = "subjectDid" AND position("recordPath" IN "subjectUri") > 0 - `.execute(db.db) - console.log(`Updated blob cids on ${results.numUpdatedOrDeletedRows} rows`) -} - -async function updateStatusFromUnresolvedReports(db: PrimaryDatabase) { - const { ref } = db.db.dynamic - const reports = await db.db - // @ts-ignore - .selectFrom('moderation_report') - .whereNotExists((qb) => - qb - .selectFrom('moderation_report_resolution') - .selectAll() - // @ts-ignore - .whereRef('reportId', '=', ref('moderation_report.id')), - ) - .select(sql`moderation_report.id`.as('legacyId')) - .execute() - - console.log('Updating statuses based on unresolved reports') - await processLegacyReports( - db, - reports.map((report) => report.legacyId), - ) - console.log('Completed updating statuses based on unresolved reports') -} - -export async function MigrateModerationData() { - const env = getEnv() - const db = new DatabaseCoordinator({ - schema: env.DB_SCHEMA, - primary: { - url: env.DB_URL, - poolSize: env.DB_POOL_SIZE, - }, - replicas: [], - }) - - const primaryDb = db.getPrimary() - - const [counts, existingEventsCount] = await Promise.all([ - countEntries(primaryDb), - countEvents(primaryDb), - ]) - - // If there are existing events in the moderation_event table, we assume that the migration has already been run - // so we just bring over any new reports since last run - if (existingEventsCount) { - console.log( - `Found ${existingEventsCount} existing events. Migrating ${counts.reportsCount} reports only, ignoring actions`, - ) - const reportMigrationStartedAt = Date.now() - const latestReportLegacyRefId = await getLatestReportLegacyRefId(primaryDb) - - if (latestReportLegacyRefId) { - await createEvents(primaryDb, { - onlyReportsAboveId: latestReportLegacyRefId, - }) - const newReportEvents = await getReportEventsAboveLegacyId( - primaryDb, - latestReportLegacyRefId, - ) - await processLegacyReports( - primaryDb, - newReportEvents.map((evt) => evt.legacyRefId), - ) - await setReportedAtTimestamp(primaryDb) - } else { - console.log('No reports have been migrated into events yet, bailing.') - } - - console.log( - `Time spent: ${(Date.now() - reportMigrationStartedAt) / 1000} seconds`, - ) - console.log('Migration complete!') - return - } - - const totalEntries = counts.actionsCount + counts.reportsCount - console.log(`Migrating ${totalEntries} rows of actions and reports`) - const startedAt = Date.now() - await createEvents(primaryDb) - // Important to run this before creation statuses from actions to ensure that we are not attempting to map flag actions - await remapFlagToAcknlowedge(primaryDb) - await createStatusFromActions(primaryDb) - await updateStatusFromUnresolvedReports(primaryDb) - await setReportedAtTimestamp(primaryDb) - await syncBlobCids(primaryDb) - - console.log(`Time spent: ${(Date.now() - startedAt) / 1000 / 60} minutes`) - console.log('Migration complete!') -} diff --git a/packages/bsky/src/services/actor/index.ts b/packages/bsky/src/services/actor/index.ts index 7ef61529926..b8898570688 100644 --- a/packages/bsky/src/services/actor/index.ts +++ b/packages/bsky/src/services/actor/index.ts @@ -10,6 +10,8 @@ import { SearchKeyset, getUserSearchQuery } from '../util/search' import { FromDb } from '../types' import { GraphService } from '../graph' import { LabelService } from '../label' +import { AtUri } from '@atproto/syntax' +import { ids } from '../../lexicon/lexicons' export * from './types' @@ -96,6 +98,26 @@ export class ActorService { }) } + async getProfileRecords(dids: string[], includeSoftDeleted = false) { + if (dids.length === 0) return new Map() + const profileUris = dids.map((did) => + AtUri.make(did, ids.AppBskyActorProfile, 'self').toString(), + ) + const { ref } = this.db.db.dynamic + const res = await this.db.db + .selectFrom('record') + .innerJoin('actor', 'actor.did', 'record.did') + .if(!includeSoftDeleted, (qb) => + qb.where(notSoftDeletedClause(ref('actor'))), + ) + .where('uri', 'in', profileUris) + .select(['record.did', 'record.json']) + .execute() + return res.reduce((acc, cur) => { + return acc.set(cur.did, JSON.parse(cur.json)) + }, new Map()) + } + async getSearchResults({ cursor, limit = 25, diff --git a/packages/bsky/src/services/moderation/index.ts b/packages/bsky/src/services/moderation/index.ts index 84769100ae9..71380e16884 100644 --- a/packages/bsky/src/services/moderation/index.ts +++ b/packages/bsky/src/services/moderation/index.ts @@ -1,37 +1,9 @@ import { CID } from 'multiformats/cid' import { AtUri } from '@atproto/syntax' -import { InvalidRequestError } from '@atproto/xrpc-server' import { PrimaryDatabase } from '../../db' -import { ModerationViews } from './views' import { ImageUriBuilder } from '../../image/uri' -import { Main as StrongRef } from '../../lexicon/types/com/atproto/repo/strongRef' import { ImageInvalidator } from '../../image/invalidator' -import { - isModEventComment, - isModEventLabel, - isModEventMute, - isModEventReport, - isModEventTakedown, - isModEventEmail, - RepoRef, - RepoBlobRef, -} from '../../lexicon/types/com/atproto/admin/defs' -import { addHoursToDate } from '../../util/date' -import { - adjustModerationSubjectStatus, - getStatusIdentifierFromSubject, -} from './status' -import { - ModEventType, - ModerationEventRow, - ModerationEventRowWithHandle, - ModerationSubjectStatusRow, - ReversibleModerationEvent, - SubjectInfo, -} from './types' -import { ModerationEvent } from '../../db/tables/moderation' -import { paginate } from '../../db/pagination' -import { StatusKeyset, TimeIdKeyset } from './pagination' +import { StatusAttr } from '../../lexicon/types/com/atproto/admin/defs' export class ModerationService { constructor( @@ -48,630 +20,99 @@ export class ModerationService { new ModerationService(db, imgUriBuilder, imgInvalidator) } - views = new ModerationViews(this.db) - - async getEvent(id: number): Promise { - return await this.db.db - .selectFrom('moderation_event') - .selectAll() - .where('id', '=', id) - .executeTakeFirst() - } - - async getEventOrThrow(id: number): Promise { - const event = await this.getEvent(id) - if (!event) throw new InvalidRequestError('Moderation event not found') - return event - } - - async getEvents(opts: { - subject?: string - createdBy?: string - limit: number - cursor?: string - includeAllUserRecords: boolean - types: ModerationEvent['action'][] - sortDirection?: 'asc' | 'desc' - }): Promise<{ cursor?: string; events: ModerationEventRowWithHandle[] }> { - const { - subject, - createdBy, - limit, - cursor, - includeAllUserRecords, - sortDirection = 'desc', - types, - } = opts - let builder = this.db.db - .selectFrom('moderation_event') - .leftJoin( - 'actor as creatorActor', - 'creatorActor.did', - 'moderation_event.createdBy', - ) - .leftJoin( - 'actor as subjectActor', - 'subjectActor.did', - 'moderation_event.subjectDid', - ) - if (subject) { - builder = builder.where((qb) => { - if (includeAllUserRecords) { - // If subject is an at-uri, we need to extract the DID from the at-uri - // otherwise, subject is probably a DID already - if (subject.startsWith('at://')) { - const uri = new AtUri(subject) - return qb.where('subjectDid', '=', uri.hostname) - } - return qb.where('subjectDid', '=', subject) - } - return qb - .where((subQb) => - subQb - .where('subjectDid', '=', subject) - .where('subjectUri', 'is', null), - ) - .orWhere('subjectUri', '=', subject) - }) - } - if (types.length) { - builder = builder.where((qb) => { - if (types.length === 1) { - return qb.where('action', '=', types[0]) - } - - return qb.where('action', 'in', types) - }) - } - if (createdBy) { - builder = builder.where('createdBy', '=', createdBy) - } - - const { ref } = this.db.db.dynamic - const keyset = new TimeIdKeyset( - ref(`moderation_event.createdAt`), - ref('moderation_event.id'), - ) - const paginatedBuilder = paginate(builder, { - limit, - cursor, - keyset, - direction: sortDirection, - tryIndex: true, - }) - - const result = await paginatedBuilder - .selectAll(['moderation_event']) - .select([ - 'subjectActor.handle as subjectHandle', - 'creatorActor.handle as creatorHandle', - ]) - .execute() - - return { cursor: keyset.packFromResult(result), events: result } - } - - async getReport(id: number): Promise { - return await this.db.db - .selectFrom('moderation_event') - .where('action', '=', 'com.atproto.admin.defs#modEventReport') - .selectAll() - .where('id', '=', id) - .executeTakeFirst() - } - - async getCurrentStatus( - subject: { did: string } | { uri: AtUri } | { cids: CID[] }, - ) { - let builder = this.db.db.selectFrom('moderation_subject_status').selectAll() - if ('did' in subject) { - builder = builder.where('did', '=', subject.did) - } else if ('uri' in subject) { - builder = builder.where('recordPath', '=', subject.uri.toString()) - } - // TODO: Handle the cid status - return await builder.execute() - } - - buildSubjectInfo( - subject: { did: string } | { uri: AtUri; cid: CID }, - subjectBlobCids?: CID[], - ): SubjectInfo { - if ('did' in subject) { - if (subjectBlobCids?.length) { - throw new InvalidRequestError('Blobs do not apply to repo subjects') - } - // Allowing dids that may not exist: may have been deleted but needs to remain actionable. - return { - subjectType: 'com.atproto.admin.defs#repoRef', - subjectDid: subject.did, - subjectUri: null, - subjectCid: null, - } - } - - // Allowing records/blobs that may not exist: may have been deleted but needs to remain actionable. - return { - subjectType: 'com.atproto.repo.strongRef', - subjectDid: subject.uri.host, - subjectUri: subject.uri.toString(), - subjectCid: subject.cid.toString(), - } - } - - async logEvent(info: { - event: ModEventType - subject: { did: string } | { uri: AtUri; cid: CID } - subjectBlobCids?: CID[] - createdBy: string - createdAt?: Date - }): Promise { - this.db.assertTransaction() - const { - event, - createdBy, - subject, - subjectBlobCids, - createdAt = new Date(), - } = info - - // Resolve subject info - const subjectInfo = this.buildSubjectInfo(subject, subjectBlobCids) - - const createLabelVals = - isModEventLabel(event) && event.createLabelVals.length > 0 - ? event.createLabelVals.join(' ') - : undefined - const negateLabelVals = - isModEventLabel(event) && event.negateLabelVals.length > 0 - ? event.negateLabelVals.join(' ') - : undefined - - const meta: Record = {} - - if (isModEventReport(event)) { - meta.reportType = event.reportType - } - - if (isModEventComment(event) && event.sticky) { - meta.sticky = event.sticky - } - - if (isModEventEmail(event)) { - meta.subjectLine = event.subjectLine - } - - const modEvent = await this.db.db - .insertInto('moderation_event') - .values({ - comment: event.comment ? `${event.comment}` : null, - action: event.$type as ModerationEvent['action'], - createdAt: createdAt.toISOString(), - createdBy, - createLabelVals, - negateLabelVals, - durationInHours: event.durationInHours - ? Number(event.durationInHours) - : null, - meta, - expiresAt: - (isModEventTakedown(event) || isModEventMute(event)) && - event.durationInHours - ? addHoursToDate(event.durationInHours, createdAt).toISOString() - : undefined, - ...subjectInfo, - }) - .returningAll() - .executeTakeFirstOrThrow() - - await adjustModerationSubjectStatus(this.db, modEvent, subjectBlobCids) - - return modEvent - } - - async getLastReversibleEventForSubject({ - did, - muteUntil, - recordPath, - suspendUntil, - }: ModerationSubjectStatusRow) { - const isSuspended = suspendUntil && new Date(suspendUntil) < new Date() - const isMuted = muteUntil && new Date(muteUntil) < new Date() - - // If the subject is neither suspended nor muted don't bother finding the last reversible event - // Ideally, this should never happen because the caller of this method should only call this - // after ensuring that the suspended or muted subjects are being reversed - if (!isSuspended && !isMuted) { - return null - } - - let builder = this.db.db - .selectFrom('moderation_event') - .where('subjectDid', '=', did) - - if (recordPath) { - builder = builder.where('subjectUri', 'like', `%${recordPath}%`) - } - - // Means the subject was suspended and needs to be unsuspended - if (isSuspended) { - builder = builder - .where('action', '=', 'com.atproto.admin.defs#modEventTakedown') - .where('durationInHours', 'is not', null) - } - if (isMuted) { - builder = builder - .where('action', '=', 'com.atproto.admin.defs#modEventMute') - .where('durationInHours', 'is not', null) - } - - return await builder - .orderBy('id', 'desc') - .selectAll() - .limit(1) - .executeTakeFirst() - } - - async getSubjectsDueForReversal(): Promise { - const subjectsDueForReversal = await this.db.db - .selectFrom('moderation_subject_status') - .where('suspendUntil', '<', new Date().toISOString()) - .orWhere('muteUntil', '<', new Date().toISOString()) - .selectAll() - .execute() - - return subjectsDueForReversal - } - - async isSubjectSuspended(did: string): Promise { - const res = await this.db.db - .selectFrom('moderation_subject_status') - .where('did', '=', did) - .where('recordPath', '=', '') - .where('suspendUntil', '>', new Date().toISOString()) - .select('did') - .limit(1) - .executeTakeFirst() - return !!res - } - - async revertState({ - createdBy, - createdAt, - comment, - action, - subject, - }: ReversibleModerationEvent): Promise<{ - result: ModerationEventRow - restored?: TakedownSubjects - }> { - const isRevertingTakedown = - action === 'com.atproto.admin.defs#modEventTakedown' - this.db.assertTransaction() - const result = await this.logEvent({ - event: { - $type: isRevertingTakedown - ? 'com.atproto.admin.defs#modEventReverseTakedown' - : 'com.atproto.admin.defs#modEventUnmute', - comment: comment ?? undefined, - }, - createdAt, - createdBy, - subject, - }) - - let restored: TakedownSubjects | undefined - - if (!isRevertingTakedown) { - return { result, restored } - } - - if ( - result.subjectType === 'com.atproto.admin.defs#repoRef' && - result.subjectDid - ) { - await this.reverseTakedownRepo({ - did: result.subjectDid, - }) - restored = { - did: result.subjectDid, - subjects: [ - { - $type: 'com.atproto.admin.defs#repoRef', - did: result.subjectDid, - }, - ], - } - } - - if ( - result.subjectType === 'com.atproto.repo.strongRef' && - result.subjectUri - ) { - const uri = new AtUri(result.subjectUri) - await this.reverseTakedownRecord({ - uri, - }) - const did = uri.hostname - // TODO: MOD_EVENT This bit needs testing - const subjectStatus = await this.db.db - .selectFrom('moderation_subject_status') - .where('did', '=', uri.host) - .where('recordPath', '=', `${uri.collection}/${uri.rkey}`) - .select('blobCids') - .executeTakeFirst() - const blobCids = subjectStatus?.blobCids || [] - restored = { - did, - subjects: [ - { - $type: 'com.atproto.repo.strongRef', - uri: result.subjectUri, - cid: result.subjectCid ?? '', - }, - ...blobCids.map((cid) => ({ - $type: 'com.atproto.admin.defs#repoBlobRef', - did, - cid, - recordUri: result.subjectUri, - })), - ], - } - } - - return { result, restored } - } - - async takedownRepo(info: { - takedownId: number - did: string - }): Promise { - const { takedownId, did } = info + async takedownRepo(info: { takedownRef: string; did: string }) { + const { takedownRef, did } = info await this.db.db .updateTable('actor') - .set({ takedownId }) + .set({ takedownRef }) .where('did', '=', did) - .where('takedownId', 'is', null) + .where('takedownRef', 'is', null) .executeTakeFirst() - - return { - did, - subjects: [ - { - $type: 'com.atproto.admin.defs#repoRef', - did, - }, - ], - } } async reverseTakedownRepo(info: { did: string }) { await this.db.db .updateTable('actor') - .set({ takedownId: null }) + .set({ takedownRef: null }) .where('did', '=', info.did) .execute() } - async takedownRecord(info: { - takedownId: number - uri: AtUri - cid: CID - blobCids?: CID[] - }): Promise { - const { takedownId, uri, cid, blobCids } = info - const did = uri.hostname - this.db.assertTransaction() + async takedownRecord(info: { takedownRef: string; uri: AtUri; cid: CID }) { + const { takedownRef, uri } = info await this.db.db .updateTable('record') - .set({ takedownId }) + .set({ takedownRef }) .where('uri', '=', uri.toString()) - .where('takedownId', 'is', null) + .where('takedownRef', 'is', null) .executeTakeFirst() - if (blobCids) { - await Promise.all( - blobCids.map(async (cid) => { - const paths = ImageUriBuilder.presets.map((id) => { - const imgUri = this.imgUriBuilder.getPresetUri(id, uri.host, cid) - return imgUri.replace(this.imgUriBuilder.endpoint, '') - }) - await this.imgInvalidator.invalidate(cid.toString(), paths) - }), - ) - } - return { - did, - subjects: [ - { - $type: 'com.atproto.repo.strongRef', - uri: uri.toString(), - cid: cid.toString(), - }, - ...(blobCids || []).map((cid) => ({ - $type: 'com.atproto.admin.defs#repoBlobRef', - did, - cid: cid.toString(), - recordUri: uri.toString(), - })), - ], - } } async reverseTakedownRecord(info: { uri: AtUri }) { - this.db.assertTransaction() await this.db.db .updateTable('record') - .set({ takedownId: null }) + .set({ takedownRef: null }) .where('uri', '=', info.uri.toString()) .execute() } - async report(info: { - reasonType: NonNullable['reportType'] - reason?: string - subject: { did: string } | { uri: AtUri; cid: CID } - reportedBy: string - createdAt?: Date - }): Promise { - const { - reasonType, - reason, - reportedBy, - createdAt = new Date(), - subject, - } = info - - const event = await this.logEvent({ - event: { - $type: 'com.atproto.admin.defs#modEventReport', - reportType: reasonType, - comment: reason, - }, - createdBy: reportedBy, - subject, - createdAt, + async takedownBlob(info: { takedownRef: string; did: string; cid: string }) { + const { takedownRef, did, cid } = info + await this.db.db + .insertInto('blob_takedown') + .values({ did, cid, takedownRef }) + .onConflict((oc) => oc.doNothing()) + .execute() + const paths = ImageUriBuilder.presets.map((id) => { + const imgUri = this.imgUriBuilder.getPresetUri(id, did, cid) + return imgUri.replace(this.imgUriBuilder.endpoint, '') }) - - return event + await this.imgInvalidator.invalidate(cid.toString(), paths) } - async getSubjectStatuses({ - cursor, - limit = 50, - takendown, - appealed, - reviewState, - reviewedAfter, - reviewedBefore, - reportedAfter, - reportedBefore, - includeMuted, - ignoreSubjects, - sortDirection, - lastReviewedBy, - sortField, - subject, - }: { - cursor?: string - limit?: number - takendown?: boolean - appealed?: boolean | null - reviewedBefore?: string - reviewState?: ModerationSubjectStatusRow['reviewState'] - reviewedAfter?: string - reportedAfter?: string - reportedBefore?: string - includeMuted?: boolean - subject?: string - ignoreSubjects?: string[] - sortDirection: 'asc' | 'desc' - lastReviewedBy?: string - sortField: 'lastReviewedAt' | 'lastReportedAt' - }) { - let builder = this.db.db - .selectFrom('moderation_subject_status') - .leftJoin('actor', 'actor.did', 'moderation_subject_status.did') - - if (subject) { - const subjectInfo = getStatusIdentifierFromSubject(subject) - builder = builder - .where('moderation_subject_status.did', '=', subjectInfo.did) - .where((qb) => - subjectInfo.recordPath - ? qb.where('recordPath', '=', subjectInfo.recordPath) - : qb.where('recordPath', '=', ''), - ) - } - - if (ignoreSubjects?.length) { - builder = builder - .where('moderation_subject_status.did', 'not in', ignoreSubjects) - .where('recordPath', 'not in', ignoreSubjects) - } - - if (reviewState) { - builder = builder.where('reviewState', '=', reviewState) - } - - if (lastReviewedBy) { - builder = builder.where('lastReviewedBy', '=', lastReviewedBy) - } - - if (reviewedAfter) { - builder = builder.where('lastReviewedAt', '>', reviewedAfter) - } - - if (reviewedBefore) { - builder = builder.where('lastReviewedAt', '<', reviewedBefore) - } - - if (reportedAfter) { - builder = builder.where('lastReviewedAt', '>', reportedAfter) - } - - if (reportedBefore) { - builder = builder.where('lastReportedAt', '<', reportedBefore) - } - - if (takendown) { - builder = builder.where('takendown', '=', true) - } - - if (appealed !== undefined) { - builder = - appealed === null - ? builder.where('appealed', 'is', null) - : builder.where('appealed', '=', appealed) - } - - if (!includeMuted) { - builder = builder.where((qb) => - qb - .where('muteUntil', '<', new Date().toISOString()) - .orWhere('muteUntil', 'is', null), - ) - } - - const { ref } = this.db.db.dynamic - const keyset = new StatusKeyset( - ref(`moderation_subject_status.${sortField}`), - ref('moderation_subject_status.id'), - ) - const paginatedBuilder = paginate(builder, { - limit, - cursor, - keyset, - direction: sortDirection, - tryIndex: true, - nullsLast: true, - }) - - const results = await paginatedBuilder - .select('actor.handle as handle') - .selectAll('moderation_subject_status') + async reverseTakedownBlob(info: { did: string; cid: string }) { + const { did, cid } = info + await this.db.db + .deleteFrom('blob_takedown') + .where('did', '=', did) + .where('cid', '=', cid) .execute() - - return { statuses: results, cursor: keyset.packFromResult(results) } } - async isSubjectTakendown( - subject: { did: string } | { uri: AtUri }, - ): Promise { - const { did, recordPath } = getStatusIdentifierFromSubject( - 'did' in subject ? subject.did : subject.uri, - ) - const builder = this.db.db - .selectFrom('moderation_subject_status') + async getRepoTakedownRef(did: string): Promise { + const res = await this.db.db + .selectFrom('actor') .where('did', '=', did) - .where('recordPath', '=', recordPath || '') + .selectAll() + .executeTakeFirst() + return res ? formatStatus(res.takedownRef) : null + } - const result = await builder.select('takendown').executeTakeFirst() + async getRecordTakedownRef(uri: string): Promise { + const res = await this.db.db + .selectFrom('record') + .where('uri', '=', uri) + .selectAll() + .executeTakeFirst() + return res ? formatStatus(res.takedownRef) : null + } - return !!result?.takendown + async getBlobTakedownRef( + did: string, + cid: string, + ): Promise { + const res = await this.db.db + .selectFrom('blob_takedown') + .where('did', '=', did) + .where('cid', '=', cid) + .selectAll() + .executeTakeFirst() + // this table only tracks takedowns not all blobs + // so if no result is returned then the blob is not taken down (rather than not found) + return formatStatus(res?.takedownRef ?? null) } } -export type TakedownSubjects = { - did: string - subjects: (RepoRef | RepoBlobRef | StrongRef)[] +const formatStatus = (ref: string | null): StatusAttr => { + return ref ? { applied: true, ref } : { applied: false } } diff --git a/packages/bsky/src/services/moderation/pagination.ts b/packages/bsky/src/services/moderation/pagination.ts deleted file mode 100644 index c68de0822d4..00000000000 --- a/packages/bsky/src/services/moderation/pagination.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { DynamicModule, sql } from 'kysely' - -import { Cursor, GenericKeyset } from '../../db/pagination' - -type StatusKeysetParam = { - lastReviewedAt: string | null - lastReportedAt: string | null - id: number -} - -export class StatusKeyset extends GenericKeyset { - labelResult(result: StatusKeysetParam): Cursor - labelResult(result: StatusKeysetParam) { - const primaryField = ( - this.primary as ReturnType - ).dynamicReference.includes('lastReviewedAt') - ? 'lastReviewedAt' - : 'lastReportedAt' - - return { - primary: result[primaryField] - ? new Date(`${result[primaryField]}`).getTime().toString() - : '', - secondary: result.id.toString(), - } - } - labeledResultToCursor(labeled: Cursor) { - return { - primary: labeled.primary, - secondary: labeled.secondary, - } - } - cursorToLabeledResult(cursor: Cursor) { - return { - primary: cursor.primary - ? new Date(parseInt(cursor.primary, 10)).toISOString() - : '', - secondary: cursor.secondary, - } - } - unpackCursor(cursorStr?: string): Cursor | undefined { - if (!cursorStr) return - const result = cursorStr.split('::') - const [primary, secondary, ...others] = result - if (!secondary || others.length > 0) { - throw new InvalidRequestError('Malformed cursor') - } - return { - primary, - secondary, - } - } - // This is specifically built to handle nullable columns as primary sorting column - getSql(labeled?: Cursor, direction?: 'asc' | 'desc') { - if (labeled === undefined) return - if (direction === 'asc') { - return !labeled.primary - ? sql`(${this.primary} IS NULL AND ${this.secondary} > ${labeled.secondary})` - : sql`((${this.primary}, ${this.secondary}) > (${labeled.primary}, ${labeled.secondary}) OR (${this.primary} is null))` - } else { - return !labeled.primary - ? sql`(${this.primary} IS NULL AND ${this.secondary} < ${labeled.secondary})` - : sql`((${this.primary}, ${this.secondary}) < (${labeled.primary}, ${labeled.secondary}) OR (${this.primary} is null))` - } - } -} - -type TimeIdKeysetParam = { - id: number - createdAt: string -} -type TimeIdResult = TimeIdKeysetParam - -export class TimeIdKeyset extends GenericKeyset { - labelResult(result: TimeIdResult): Cursor - labelResult(result: TimeIdResult) { - return { primary: result.createdAt, secondary: result.id.toString() } - } - labeledResultToCursor(labeled: Cursor) { - return { - primary: new Date(labeled.primary).getTime().toString(), - secondary: labeled.secondary, - } - } - cursorToLabeledResult(cursor: Cursor) { - const primaryDate = new Date(parseInt(cursor.primary, 10)) - if (isNaN(primaryDate.getTime())) { - throw new InvalidRequestError('Malformed cursor') - } - return { - primary: primaryDate.toISOString(), - secondary: cursor.secondary, - } - } -} diff --git a/packages/bsky/src/services/moderation/views.ts b/packages/bsky/src/services/moderation/views.ts deleted file mode 100644 index 654a6e54291..00000000000 --- a/packages/bsky/src/services/moderation/views.ts +++ /dev/null @@ -1,551 +0,0 @@ -import { sql } from 'kysely' -import { ArrayEl } from '@atproto/common' -import { AtUri } from '@atproto/syntax' -import { INVALID_HANDLE } from '@atproto/syntax' -import { BlobRef, jsonStringToLex } from '@atproto/lexicon' -import { Database } from '../../db' -import { Actor } from '../../db/tables/actor' -import { Record as RecordRow } from '../../db/tables/record' -import { - ModEventView, - RepoView, - RepoViewDetail, - RecordView, - RecordViewDetail, - ReportViewDetail, - BlobView, - SubjectStatusView, - ModEventViewDetail, -} 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 { - ModerationEventRowWithHandle, - ModerationSubjectStatusRowWithHandle, -} from './types' -import { getSelfLabels } from '../label' -import { REASONOTHER } from '../../lexicon/types/com/atproto/moderation/defs' - -export class ModerationViews { - constructor(private db: Database) {} - - repo(result: RepoResult): Promise - repo(result: RepoResult[]): Promise - async repo( - result: RepoResult | RepoResult[], - ): Promise { - const results = Array.isArray(result) ? result : [result] - if (results.length === 0) return [] - - const [info, subjectStatuses] = await Promise.all([ - await this.db.db - .selectFrom('actor') - .leftJoin('profile', 'profile.creator', 'actor.did') - .leftJoin( - 'record as profile_record', - 'profile_record.uri', - 'profile.uri', - ) - .where( - 'actor.did', - 'in', - results.map((r) => r.did), - ) - .select(['actor.did as did', 'profile_record.json as profileJson']) - .execute(), - this.getSubjectStatus(results.map((r) => ({ did: r.did }))), - ]) - - const infoByDid = info.reduce( - (acc, cur) => Object.assign(acc, { [cur.did]: cur }), - {} as Record>, - ) - const subjectStatusByDid = subjectStatuses.reduce( - (acc, cur) => - Object.assign(acc, { [cur.did ?? '']: this.subjectStatus(cur) }), - {}, - ) - - const views = results.map((r) => { - const { profileJson } = infoByDid[r.did] ?? {} - const relatedRecords: object[] = [] - if (profileJson) { - relatedRecords.push( - jsonStringToLex(profileJson) as Record, - ) - } - return { - // No email or invite info on appview - did: r.did, - handle: r.handle ?? INVALID_HANDLE, - relatedRecords, - indexedAt: r.indexedAt, - moderation: { - subjectStatus: subjectStatusByDid[r.did] ?? undefined, - }, - } - }) - - return Array.isArray(result) ? views : views[0] - } - event(result: EventResult): Promise - event(result: EventResult[]): Promise - async event( - result: EventResult | EventResult[], - ): Promise { - const results = Array.isArray(result) ? result : [result] - if (results.length === 0) return [] - - const views = results.map((res) => { - const eventView: ModEventView = { - id: res.id, - event: { - $type: res.action, - comment: res.comment ?? undefined, - }, - subject: - res.subjectType === 'com.atproto.admin.defs#repoRef' - ? { - $type: 'com.atproto.admin.defs#repoRef', - did: res.subjectDid, - } - : { - $type: 'com.atproto.repo.strongRef', - uri: res.subjectUri, - cid: res.subjectCid, - }, - subjectBlobCids: [], - createdBy: res.createdBy, - createdAt: res.createdAt, - subjectHandle: res.subjectHandle ?? undefined, - creatorHandle: res.creatorHandle ?? undefined, - } - - if ( - [ - 'com.atproto.admin.defs#modEventTakedown', - 'com.atproto.admin.defs#modEventMute', - ].includes(res.action) - ) { - eventView.event = { - ...eventView.event, - durationInHours: res.durationInHours ?? undefined, - } - } - - if (res.action === 'com.atproto.admin.defs#modEventLabel') { - eventView.event = { - ...eventView.event, - createLabelVals: res.createLabelVals?.length - ? res.createLabelVals.split(' ') - : [], - negateLabelVals: res.negateLabelVals?.length - ? res.negateLabelVals.split(' ') - : [], - } - } - - // This is for legacy data only, for new events, these types of events won't have labels attached - if ( - [ - 'com.atproto.admin.defs#modEventAcknowledge', - 'com.atproto.admin.defs#modEventTakedown', - 'com.atproto.admin.defs#modEventEscalate', - ].includes(res.action) - ) { - if (res.createLabelVals?.length) { - eventView.event = { - ...eventView.event, - createLabelVals: res.createLabelVals.split(' '), - } - } - - if (res.negateLabelVals?.length) { - eventView.event = { - ...eventView.event, - negateLabelVals: res.negateLabelVals.split(' '), - } - } - } - - if (res.action === 'com.atproto.admin.defs#modEventReport') { - eventView.event = { - ...eventView.event, - reportType: res.meta?.reportType ?? undefined, - } - } - - if (res.action === 'com.atproto.admin.defs#modEventEmail') { - eventView.event = { - ...eventView.event, - subjectLine: res.meta?.subjectLine ?? '', - } - } - - if ( - res.action === 'com.atproto.admin.defs#modEventComment' && - res.meta?.sticky - ) { - eventView.event.sticky = true - } - - return eventView - }) - - return Array.isArray(result) ? views : views[0] - } - - async eventDetail(result: EventResult): Promise { - const [event, subject] = await Promise.all([ - this.event(result), - this.subject(result), - ]) - const allBlobs = findBlobRefs(subject.value) - const subjectBlobs = await this.blob( - allBlobs.filter((blob) => - event.subjectBlobCids.includes(blob.ref.toString()), - ), - ) - return { - ...event, - subject, - subjectBlobs, - } - } - - async repoDetail(result: RepoResult): Promise { - const [repo, labels] = await Promise.all([ - this.repo(result), - this.labels(result.did), - ]) - - return { - ...repo, - moderation: { - ...repo.moderation, - }, - labels, - } - } - - record(result: RecordResult): Promise - record(result: RecordResult[]): Promise - async record( - result: RecordResult | RecordResult[], - ): Promise { - const results = Array.isArray(result) ? result : [result] - if (results.length === 0) return [] - - const [repoResults, subjectStatuses] = await Promise.all([ - this.db.db - .selectFrom('actor') - .where( - 'actor.did', - 'in', - results.map((r) => didFromUri(r.uri)), - ) - .selectAll() - .execute(), - this.getSubjectStatus(results.map((r) => didAndRecordPathFromUri(r.uri))), - ]) - const repos = await this.repo(repoResults) - - const reposByDid = repos.reduce( - (acc, cur) => Object.assign(acc, { [cur.did]: cur }), - {} as Record>, - ) - const subjectStatusByUri = subjectStatuses.reduce( - (acc, cur) => - Object.assign(acc, { - [`${cur.did}/${cur.recordPath}` ?? '']: this.subjectStatus(cur), - }), - {}, - ) - - const views = results.map((res) => { - const repo = reposByDid[didFromUri(res.uri)] - const { did, recordPath } = didAndRecordPathFromUri(res.uri) - const subjectStatus = subjectStatusByUri[`${did}/${recordPath}`] - if (!repo) throw new Error(`Record repo is missing: ${res.uri}`) - const value = jsonStringToLex(res.json) as Record - return { - uri: res.uri, - cid: res.cid, - value, - blobCids: findBlobRefs(value).map((blob) => blob.ref.toString()), - indexedAt: res.indexedAt, - repo, - moderation: { - subjectStatus, - }, - } - }) - - return Array.isArray(result) ? views : views[0] - } - - async recordDetail(result: RecordResult): Promise { - const [record, subjectStatusResult] = await Promise.all([ - this.record(result), - this.getSubjectStatus(didAndRecordPathFromUri(result.uri)), - ]) - - const [blobs, labels, subjectStatus] = await Promise.all([ - this.blob(findBlobRefs(record.value)), - this.labels(record.uri), - subjectStatusResult?.length - ? this.subjectStatus(subjectStatusResult[0]) - : Promise.resolve(undefined), - ]) - const selfLabels = getSelfLabels({ - uri: result.uri, - cid: result.cid, - record: jsonStringToLex(result.json) as Record, - }) - return { - ...record, - blobs, - moderation: { - ...record.moderation, - subjectStatus, - }, - labels: [...labels, ...selfLabels], - } - } - reportPublic(report: ReportResult): ReportOutput { - return { - id: report.id, - createdAt: report.createdAt, - // Ideally, we would never have a report entry that does not have a reasonType but at the schema level - // we are not guarantying that so in whatever case, if we end up with such entries, default to 'other' - reasonType: report.meta?.reportType - ? (report.meta?.reportType as string) - : REASONOTHER, - reason: report.comment ?? undefined, - reportedBy: report.createdBy, - subject: - report.subjectType === 'com.atproto.admin.defs#repoRef' - ? { - $type: 'com.atproto.admin.defs#repoRef', - did: report.subjectDid, - } - : { - $type: 'com.atproto.repo.strongRef', - uri: report.subjectUri, - cid: report.subjectCid, - }, - } - } - // Partial view for subjects - - async subject(result: SubjectResult): Promise { - let subject: SubjectView - if (result.subjectType === 'com.atproto.admin.defs#repoRef') { - const repoResult = await this.db.db - .selectFrom('actor') - .selectAll() - .where('did', '=', result.subjectDid) - .executeTakeFirst() - if (repoResult) { - subject = await this.repo(repoResult) - subject.$type = 'com.atproto.admin.defs#repoView' - } else { - subject = { did: result.subjectDid } - subject.$type = 'com.atproto.admin.defs#repoViewNotFound' - } - } else if ( - result.subjectType === 'com.atproto.repo.strongRef' && - result.subjectUri !== null - ) { - const recordResult = await this.db.db - .selectFrom('record') - .selectAll() - .where('uri', '=', result.subjectUri) - .executeTakeFirst() - if (recordResult) { - subject = await this.record(recordResult) - subject.$type = 'com.atproto.admin.defs#recordView' - } else { - subject = { uri: result.subjectUri } - subject.$type = 'com.atproto.admin.defs#recordViewNotFound' - } - } else { - throw new Error(`Bad subject data: (${result.id}) ${result.subjectType}`) - } - return subject - } - - // Partial view for blobs - - async blob(blobs: BlobRef[]): Promise { - if (!blobs.length) return [] - const { ref } = this.db.db.dynamic - const modStatusResults = await this.db.db - .selectFrom('moderation_subject_status') - .where( - sql`${ref( - 'moderation_subject_status.blobCids', - )} @> ${JSON.stringify(blobs.map((blob) => blob.ref.toString()))}`, - ) - .selectAll() - .executeTakeFirst() - const statusByCid = (modStatusResults?.blobCids || [])?.reduce( - (acc, cur) => Object.assign(acc, { [cur]: modStatusResults }), - {}, - ) - // Intentionally missing details field, since we don't have any on appview. - // We also don't know when the blob was created, so we use a canned creation time. - const unknownTime = new Date(0).toISOString() - return blobs.map((blob) => { - const cid = blob.ref.toString() - const subjectStatus = statusByCid[cid] - ? this.subjectStatus(statusByCid[cid]) - : undefined - return { - cid, - mimeType: blob.mimeType, - size: blob.size, - createdAt: unknownTime, - moderation: { - subjectStatus, - }, - } - }) - } - - 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', '=', false)) - .selectAll() - .execute() - return res.map((l) => ({ - ...l, - cid: l.cid === '' ? undefined : l.cid, - neg: l.neg, - })) - } - - async getSubjectStatus( - subject: - | { did: string; recordPath?: string } - | { did: string; recordPath?: string }[], - ): Promise { - const subjectFilters = Array.isArray(subject) ? subject : [subject] - const filterForSubject = - ({ did, recordPath }: { did: string; recordPath?: string }) => - // TODO: Fix the typing here? - (clause: any) => { - clause = clause - .where('moderation_subject_status.did', '=', did) - .where('moderation_subject_status.recordPath', '=', recordPath || '') - return clause - } - - const builder = this.db.db - .selectFrom('moderation_subject_status') - .leftJoin('actor', 'actor.did', 'moderation_subject_status.did') - .where((clause) => { - subjectFilters.forEach(({ did, recordPath }, i) => { - const applySubjectFilter = filterForSubject({ did, recordPath }) - if (i === 0) { - clause = clause.where(applySubjectFilter) - } else { - clause = clause.orWhere(applySubjectFilter) - } - }) - - return clause - }) - .selectAll('moderation_subject_status') - .select('actor.handle as handle') - - return builder.execute() - } - - subjectStatus(result: ModerationSubjectStatusRowWithHandle): SubjectStatusView - subjectStatus( - result: ModerationSubjectStatusRowWithHandle[], - ): SubjectStatusView[] - subjectStatus( - result: - | ModerationSubjectStatusRowWithHandle - | ModerationSubjectStatusRowWithHandle[], - ): SubjectStatusView | SubjectStatusView[] { - const results = Array.isArray(result) ? result : [result] - if (results.length === 0) return [] - - const decoratedSubjectStatuses = results.map((subjectStatus) => ({ - id: subjectStatus.id, - reviewState: subjectStatus.reviewState, - createdAt: subjectStatus.createdAt, - updatedAt: subjectStatus.updatedAt, - comment: subjectStatus.comment ?? undefined, - lastReviewedBy: subjectStatus.lastReviewedBy ?? undefined, - lastReviewedAt: subjectStatus.lastReviewedAt ?? undefined, - lastReportedAt: subjectStatus.lastReportedAt ?? undefined, - lastAppealedAt: subjectStatus.lastAppealedAt ?? undefined, - muteUntil: subjectStatus.muteUntil ?? undefined, - suspendUntil: subjectStatus.suspendUntil ?? undefined, - takendown: subjectStatus.takendown ?? undefined, - appealed: subjectStatus.appealed ?? undefined, - subjectRepoHandle: subjectStatus.handle ?? undefined, - subjectBlobCids: subjectStatus.blobCids || [], - subject: !subjectStatus.recordPath - ? { - $type: 'com.atproto.admin.defs#repoRef', - did: subjectStatus.did, - } - : { - $type: 'com.atproto.repo.strongRef', - uri: AtUri.make( - subjectStatus.did, - // Not too intuitive but the recordpath is basically / - // which is what the last 2 params of .make() arguments are - ...subjectStatus.recordPath.split('/'), - ).toString(), - cid: subjectStatus.recordCid, - }, - })) - - return Array.isArray(result) - ? decoratedSubjectStatuses - : decoratedSubjectStatuses[0] - } -} - -type RepoResult = Actor - -type EventResult = ModerationEventRowWithHandle - -type ReportResult = ModerationEventRowWithHandle - -type RecordResult = RecordRow - -type SubjectResult = Pick< - EventResult & ReportResult, - 'id' | 'subjectType' | 'subjectDid' | 'subjectUri' | 'subjectCid' -> - -type SubjectView = ModEventViewDetail['subject'] & ReportViewDetail['subject'] - -function didFromUri(uri: string) { - return new AtUri(uri).host -} - -function didAndRecordPathFromUri(uri: string) { - const atUri = new AtUri(uri) - return { did: atUri.host, recordPath: `${atUri.collection}/${atUri.rkey}` } -} - -function findBlobRefs(value: unknown, refs: BlobRef[] = []) { - if (value instanceof BlobRef) { - refs.push(value) - } else if (Array.isArray(value)) { - value.forEach((val) => findBlobRefs(val, refs)) - } else if (value && typeof value === 'object') { - Object.values(value).forEach((val) => findBlobRefs(val, refs)) - } - return refs -} diff --git a/packages/bsky/src/util/date.ts b/packages/bsky/src/util/date.ts deleted file mode 100644 index af9767a0f7f..00000000000 --- a/packages/bsky/src/util/date.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * This function takes a number as input and returns a Date object, - * which is the current date and time plus the input number of hours. - * - * @param {number} hours - The number of hours to add to the current date and time. - * @param {Date} startingDate - If provided, the function will add `hours` to the provided date instead of the current date. - * @returns {Date} - The new Date object, which is the current date and time plus the input number of hours. - */ -export function addHoursToDate(hours: number, startingDate?: Date): Date { - // When date is passe, let's clone before calling `setHours()` so that we are not mutating the original date - const currentDate = startingDate ? new Date(startingDate) : new Date() - currentDate.setHours(currentDate.getHours() + hours) - return currentDate -} diff --git a/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap b/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap index ac9e0eee7a0..4ac043c9b88 100644 --- a/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap +++ b/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap @@ -461,12 +461,12 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -517,7 +517,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -528,7 +528,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -721,12 +721,12 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -777,7 +777,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -788,7 +788,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -937,12 +937,12 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -987,7 +987,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -998,7 +998,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1222,12 +1222,12 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -1278,7 +1278,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1289,7 +1289,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/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 88c02c6e3e0..142866aeebd 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/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -113,7 +113,7 @@ Array [ "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(3)", "val": "test-label", }, @@ -121,7 +121,7 @@ Array [ "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(3)", "val": "test-label-2", }, @@ -134,7 +134,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -207,7 +207,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(3)", + "did": "user(4)", "handle": "dan.test", "labels": Array [], "viewer": Object { @@ -223,7 +223,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(4)", + "did": "user(5)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -245,7 +245,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -256,7 +256,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -317,7 +317,7 @@ Array [ "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(6)", "val": "test-label", }, @@ -416,7 +416,7 @@ Array [ "cursor": "0000000000000::bafycid", "follows": Array [ Object { - "did": "user(3)", + "did": "user(4)", "handle": "dan.test", "labels": Array [], "viewer": Object { diff --git a/packages/bsky/tests/admin/admin-auth.test.ts b/packages/bsky/tests/admin/admin-auth.test.ts new file mode 100644 index 00000000000..ff00d0906b0 --- /dev/null +++ b/packages/bsky/tests/admin/admin-auth.test.ts @@ -0,0 +1,164 @@ +import { SeedClient, usersSeed, TestNetwork } from '@atproto/dev-env' +import AtpAgent from '@atproto/api' +import { Secp256k1Keypair } from '@atproto/crypto' +import { createServiceAuthHeaders } from '@atproto/xrpc-server' +import { RepoRef } from '../../src/lexicon/types/com/atproto/admin/defs' + +describe('admin auth', () => { + let network: TestNetwork + let agent: AtpAgent + let sc: SeedClient + + let repoSubject: RepoRef + + const modServiceDid = 'did:example:mod' + const altModDid = 'did:example:alt' + let modServiceKey: Secp256k1Keypair + let bskyDid: string + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'bsky_admin_auth', + bsky: { + modServiceDid, + }, + }) + + bskyDid = network.bsky.ctx.cfg.serverDid + + modServiceKey = await Secp256k1Keypair.create() + const origResolve = network.bsky.ctx.idResolver.did.resolveAtprotoKey + network.bsky.ctx.idResolver.did.resolveAtprotoKey = async ( + did: string, + forceRefresh?: boolean, + ) => { + if (did === modServiceDid || did === altModDid) { + return modServiceKey.did() + } + return origResolve(did, forceRefresh) + } + + agent = network.bsky.getClient() + sc = network.getSeedClient() + await usersSeed(sc) + await network.processAll() + repoSubject = { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.bob, + } + }) + + afterAll(async () => { + await network.close() + }) + + it('allows service auth requests from the configured appview did', async () => { + const headers = await createServiceAuthHeaders({ + iss: modServiceDid, + aud: bskyDid, + keypair: modServiceKey, + }) + await agent.api.com.atproto.admin.updateSubjectStatus( + { + subject: repoSubject, + takedown: { applied: true, ref: 'test-repo' }, + }, + { + ...headers, + encoding: 'application/json', + }, + ) + + const res = await agent.api.com.atproto.admin.getSubjectStatus( + { + did: repoSubject.did, + }, + headers, + ) + expect(res.data.subject.did).toBe(repoSubject.did) + expect(res.data.takedown?.applied).toBe(true) + }) + + it('does not allow requests from another did', async () => { + const headers = await createServiceAuthHeaders({ + iss: altModDid, + aud: bskyDid, + keypair: modServiceKey, + }) + const attempt = agent.api.com.atproto.admin.updateSubjectStatus( + { + subject: repoSubject, + takedown: { applied: true, ref: 'test-repo' }, + }, + { + ...headers, + encoding: 'application/json', + }, + ) + await expect(attempt).rejects.toThrow('Untrusted issuer') + }) + + it('does not allow requests from an authenticated user', async () => { + const aliceKey = await network.pds.ctx.actorStore.keypair(sc.dids.alice) + const headers = await createServiceAuthHeaders({ + iss: sc.dids.alice, + aud: bskyDid, + keypair: aliceKey, + }) + const attempt = agent.api.com.atproto.admin.updateSubjectStatus( + { + subject: repoSubject, + takedown: { applied: true, ref: 'test-repo' }, + }, + { + ...headers, + encoding: 'application/json', + }, + ) + await expect(attempt).rejects.toThrow('Untrusted issuer') + }) + + it('does not allow requests with a bad signature', async () => { + const badKey = await Secp256k1Keypair.create() + const headers = await createServiceAuthHeaders({ + iss: modServiceDid, + aud: bskyDid, + keypair: badKey, + }) + const attempt = agent.api.com.atproto.admin.updateSubjectStatus( + { + subject: repoSubject, + takedown: { applied: true, ref: 'test-repo' }, + }, + { + ...headers, + encoding: 'application/json', + }, + ) + await expect(attempt).rejects.toThrow( + 'jwt signature does not match jwt issuer', + ) + }) + + it('does not allow requests with a bad aud', async () => { + // repo subject is bob, so we set alice as the audience + const headers = await createServiceAuthHeaders({ + iss: modServiceDid, + aud: sc.dids.alice, + keypair: modServiceKey, + }) + const attempt = agent.api.com.atproto.admin.updateSubjectStatus( + { + subject: repoSubject, + takedown: { applied: true, ref: 'test-repo' }, + }, + { + ...headers, + encoding: 'application/json', + }, + ) + await expect(attempt).rejects.toThrow( + 'jwt audience does not match service did', + ) + }) +}) diff --git a/packages/bsky/tests/admin/moderation.test.ts b/packages/bsky/tests/admin/moderation.test.ts index 93e0998f2d3..6b01bfbbcb6 100644 --- a/packages/bsky/tests/admin/moderation.test.ts +++ b/packages/bsky/tests/admin/moderation.test.ts @@ -1,885 +1,209 @@ -import { TestNetwork, ImageRef, RecordRef, SeedClient } from '@atproto/dev-env' -import AtpAgent, { - ComAtprotoAdminEmitModerationEvent, - ComAtprotoAdminQueryModerationStatuses, - ComAtprotoModerationCreateReport, -} from '@atproto/api' -import { AtUri } from '@atproto/syntax' -import { forSnapshot } from '../_util' -import basicSeed from '../seeds/basic' +import { ImageRef, SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env' +import AtpAgent from '@atproto/api' import { - REASONMISLEADING, - REASONOTHER, - REASONSPAM, -} from '../../src/lexicon/types/com/atproto/moderation/defs' -import { - ModEventLabel, - ModEventTakedown, - REVIEWCLOSED, - REVIEWESCALATED, + RepoBlobRef, + RepoRef, } from '../../src/lexicon/types/com/atproto/admin/defs' -import { PeriodicModerationEventReversal } from '../../src' - -type BaseCreateReportParams = - | { account: string } - | { content: { uri: string; cid: string } } -type CreateReportParams = BaseCreateReportParams & { - author: string -} & Omit - -type TakedownParams = BaseCreateReportParams & - Omit +import { Main as StrongRef } from '../../src/lexicon/types/com/atproto/repo/strongRef' describe('moderation', () => { let network: TestNetwork let agent: AtpAgent - let pdsAgent: AtpAgent let sc: SeedClient - const createReport = async (params: CreateReportParams) => { - const { author, ...rest } = params - return agent.api.com.atproto.moderation.createReport( + let repoSubject: RepoRef + let recordSubject: StrongRef + let blobSubject: RepoBlobRef + let blobRef: ImageRef + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'bsky_moderation', + }) + + agent = network.bsky.getClient() + sc = network.getSeedClient() + await basicSeed(sc) + await network.processAll() + repoSubject = { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.bob, + } + const post = sc.posts[sc.dids.carol][0] + recordSubject = { + $type: 'com.atproto.repo.strongRef', + uri: post.ref.uriStr, + cid: post.ref.cidStr, + } + blobRef = post.images[1] + blobSubject = { + $type: 'com.atproto.admin.defs#repoBlobRef', + did: sc.dids.carol, + cid: blobRef.image.ref.toString(), + } + }) + + afterAll(async () => { + await network.close() + }) + + it('takes down accounts', async () => { + await agent.api.com.atproto.admin.updateSubjectStatus( { - // Set default type to spam - reasonType: REASONSPAM, - ...rest, - subject: - 'account' in params - ? { - $type: 'com.atproto.admin.defs#repoRef', - did: params.account, - } - : { - $type: 'com.atproto.repo.strongRef', - uri: params.content.uri, - cid: params.content.cid, - }, + subject: repoSubject, + takedown: { applied: true, ref: 'test-repo' }, }, { - headers: await network.serviceHeaders(author), encoding: 'application/json', + headers: network.bsky.adminAuthHeaders('moderator'), }, ) - } + const res = await agent.api.com.atproto.admin.getSubjectStatus( + { + did: repoSubject.did, + }, + { headers: network.bsky.adminAuthHeaders('moderator') }, + ) + expect(res.data.subject.did).toEqual(sc.dids.bob) + expect(res.data.takedown?.applied).toBe(true) + expect(res.data.takedown?.ref).toBe('test-repo') + }) - const performTakedown = async ({ - durationInHours, - ...rest - }: TakedownParams & Pick) => - agent.api.com.atproto.admin.emitModerationEvent( + it('restores takendown accounts', async () => { + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { - $type: 'com.atproto.admin.defs#modEventTakedown', - durationInHours, - }, - subject: - 'account' in rest - ? { - $type: 'com.atproto.admin.defs#repoRef', - did: rest.account, - } - : { - $type: 'com.atproto.repo.strongRef', - uri: rest.content.uri, - cid: rest.content.cid, - }, - createdBy: 'did:example:admin', - ...rest, + subject: repoSubject, + takedown: { applied: false }, }, { encoding: 'application/json', - headers: network.bsky.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders('moderator'), }, ) + const res = await agent.api.com.atproto.admin.getSubjectStatus( + { + did: repoSubject.did, + }, + { headers: network.bsky.adminAuthHeaders('moderator') }, + ) + expect(res.data.subject.did).toEqual(sc.dids.bob) + expect(res.data.takedown?.applied).toBe(false) + expect(res.data.takedown?.ref).toBeUndefined() + }) - const performReverseTakedown = async (params: TakedownParams) => - agent.api.com.atproto.admin.emitModerationEvent( + it('takes down records', async () => { + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { - $type: 'com.atproto.admin.defs#modEventReverseTakedown', - }, - subject: - 'account' in params - ? { - $type: 'com.atproto.admin.defs#repoRef', - did: params.account, - } - : { - $type: 'com.atproto.repo.strongRef', - uri: params.content.uri, - cid: params.content.cid, - }, - createdBy: 'did:example:admin', - ...params, + subject: recordSubject, + takedown: { applied: true, ref: 'test-record' }, }, { encoding: 'application/json', - headers: network.bsky.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders('moderator'), }, ) - - const getStatuses = async ( - params: ComAtprotoAdminQueryModerationStatuses.QueryParams, - ) => { - const { data } = await agent.api.com.atproto.admin.queryModerationStatuses( - params, - { headers: network.bsky.adminAuthHeaders() }, + const res = await agent.api.com.atproto.admin.getSubjectStatus( + { + uri: recordSubject.uri, + }, + { headers: network.bsky.adminAuthHeaders('moderator') }, ) - - return data - } - - beforeAll(async () => { - network = await TestNetwork.create({ - dbPostgresSchema: 'bsky_moderation', - }) - agent = network.bsky.getClient() - pdsAgent = network.pds.getClient() - sc = network.getSeedClient() - await basicSeed(sc) - await network.processAll() - }) - - afterAll(async () => { - await network.close() + expect(res.data.subject.uri).toEqual(recordSubject.uri) + expect(res.data.takedown?.applied).toBe(true) + expect(res.data.takedown?.ref).toBe('test-record') }) - describe('reporting', () => { - it('creates reports of a repo.', async () => { - const { data: reportA } = await createReport({ - reasonType: REASONSPAM, - account: sc.dids.bob, - author: sc.dids.alice, - }) - const { data: reportB } = await createReport({ - reasonType: REASONOTHER, - reason: 'impersonation', - account: sc.dids.bob, - author: sc.dids.carol, - }) - expect(forSnapshot([reportA, reportB])).toMatchSnapshot() - }) - - it("allows reporting a repo that doesn't exist.", async () => { - const promise = createReport({ - reasonType: REASONSPAM, - account: 'did:plc:unknown', - author: sc.dids.alice, - }) - await expect(promise).resolves.toBeDefined() - }) - - it('creates reports of a record.', async () => { - const postA = sc.posts[sc.dids.bob][0].ref - const postB = sc.posts[sc.dids.bob][1].ref - const { data: reportA } = await createReport({ - author: sc.dids.alice, - reasonType: REASONSPAM, - content: { - $type: 'com.atproto.repo.strongRef', - uri: postA.uriStr, - cid: postA.cidStr, - }, - }) - const { data: reportB } = await createReport({ - reasonType: REASONOTHER, - reason: 'defamation', - content: { - $type: 'com.atproto.repo.strongRef', - uri: postB.uriStr, - cid: postB.cidStr, - }, - author: sc.dids.carol, - }) - expect(forSnapshot([reportA, reportB])).toMatchSnapshot() - }) - - it("allows reporting a record that doesn't exist.", async () => { - const postA = sc.posts[sc.dids.bob][0].ref - const postB = sc.posts[sc.dids.bob][1].ref - const postUriBad = new AtUri(postA.uriStr) - postUriBad.rkey = 'badrkey' - - const promiseA = createReport({ - reasonType: REASONSPAM, - content: { - $type: 'com.atproto.repo.strongRef', - uri: postUriBad.toString(), - cid: postA.cidStr, - }, - author: sc.dids.alice, - }) - await expect(promiseA).resolves.toBeDefined() - - const promiseB = createReport({ - reasonType: REASONOTHER, - reason: 'defamation', - content: { - $type: 'com.atproto.repo.strongRef', - uri: postB.uri.toString(), - cid: postA.cidStr, // bad cid - }, - author: sc.dids.carol, - }) - await expect(promiseB).resolves.toBeDefined() - }) + it('restores takendown records', async () => { + await agent.api.com.atproto.admin.updateSubjectStatus( + { + subject: recordSubject, + takedown: { applied: false }, + }, + { + encoding: 'application/json', + headers: network.bsky.adminAuthHeaders('moderator'), + }, + ) + const res = await agent.api.com.atproto.admin.getSubjectStatus( + { + uri: recordSubject.uri, + }, + { headers: network.bsky.adminAuthHeaders('moderator') }, + ) + expect(res.data.subject.uri).toEqual(recordSubject.uri) + expect(res.data.takedown?.applied).toBe(false) + expect(res.data.takedown?.ref).toBeUndefined() }) - describe('actioning', () => { - it('resolves reports on repos and records.', async () => { - const post = sc.posts[sc.dids.bob][1].ref - - await Promise.all([ - createReport({ - reasonType: REASONSPAM, - account: sc.dids.bob, - author: sc.dids.alice, - }), - createReport({ - reasonType: REASONOTHER, - reason: 'defamation', - content: { - uri: post.uri.toString(), - cid: post.cid.toString(), - }, - author: sc.dids.carol, - }), - ]) - - await performTakedown({ - account: sc.dids.bob, - }) - - const moderationStatusOnBobsAccount = await getStatuses({ - subject: sc.dids.bob, - }) - - // Validate that subject status is set to review closed and takendown flag is on - expect(moderationStatusOnBobsAccount.subjectStatuses[0]).toMatchObject({ - reviewState: REVIEWCLOSED, - takendown: true, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.bob, - }, - }) - - // Cleanup - await performReverseTakedown({ - account: sc.dids.bob, - }) - }) - - it('supports escalating a subject', async () => { - const alicesPostRef = sc.posts[sc.dids.alice][0].ref - const alicesPostSubject = { - $type: 'com.atproto.repo.strongRef', - uri: alicesPostRef.uri.toString(), - cid: alicesPostRef.cid.toString(), - } - await agent.api.com.atproto.admin.emitModerationEvent( - { - event: { - $type: 'com.atproto.admin.defs#modEventEscalate', - comment: 'Y', - }, - subject: alicesPostSubject, - createdBy: 'did:example:admin', - }, - { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('triage'), - }, - ) - - const alicesPostStatus = await getStatuses({ - subject: alicesPostRef.uri.toString(), - }) - - expect(alicesPostStatus.subjectStatuses[0]).toMatchObject({ - reviewState: REVIEWESCALATED, - takendown: false, - subject: alicesPostSubject, - }) - }) - - it('adds persistent comment on subject through comment event', async () => { - const alicesPostRef = sc.posts[sc.dids.alice][0].ref - const alicesPostSubject = { - $type: 'com.atproto.repo.strongRef', - uri: alicesPostRef.uri.toString(), - cid: alicesPostRef.cid.toString(), - } - await agent.api.com.atproto.admin.emitModerationEvent( - { - event: { - $type: 'com.atproto.admin.defs#modEventComment', - sticky: true, - comment: 'This is a persistent note', - }, - subject: alicesPostSubject, - createdBy: 'did:example:admin', - }, - { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('triage'), - }, - ) - - const alicesPostStatus = await getStatuses({ - subject: alicesPostRef.uri.toString(), - }) - - expect(alicesPostStatus.subjectStatuses[0].comment).toEqual( - 'This is a persistent note', - ) - }) - - it('reverses status when revert event is triggered.', async () => { - const alicesPostRef = sc.posts[sc.dids.alice][0].ref - const emitModEvent = async ( - event: ComAtprotoAdminEmitModerationEvent.InputSchema['event'], - overwrites: Partial = {}, - ) => { - const baseAction = { - subject: { - $type: 'com.atproto.repo.strongRef', - uri: alicesPostRef.uriStr, - cid: alicesPostRef.cidStr, - }, - createdBy: 'did:example:admin', - } - return agent.api.com.atproto.admin.emitModerationEvent( - { - event, - ...baseAction, - ...overwrites, - }, - { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders(), - }, - ) - } - // Validate that subject status is marked as escalated - await emitModEvent({ - $type: 'com.atproto.admin.defs#modEventReport', - reportType: REASONSPAM, - }) - await emitModEvent({ - $type: 'com.atproto.admin.defs#modEventReport', - reportType: REASONMISLEADING, - }) - await emitModEvent({ - $type: 'com.atproto.admin.defs#modEventEscalate', - }) - const alicesPostStatusAfterEscalation = await getStatuses({ - subject: alicesPostRef.uriStr, - }) - expect( - alicesPostStatusAfterEscalation.subjectStatuses[0].reviewState, - ).toEqual(REVIEWESCALATED) - - // Validate that subject status is marked as takendown - - await emitModEvent({ - $type: 'com.atproto.admin.defs#modEventLabel', - createLabelVals: ['nsfw'], - negateLabelVals: [], - }) - await emitModEvent({ - $type: 'com.atproto.admin.defs#modEventTakedown', - }) - - const alicesPostStatusAfterTakedown = await getStatuses({ - subject: alicesPostRef.uriStr, - }) - expect(alicesPostStatusAfterTakedown.subjectStatuses[0]).toMatchObject({ - reviewState: REVIEWCLOSED, - takendown: true, - }) - - await emitModEvent({ - $type: 'com.atproto.admin.defs#modEventReverseTakedown', - }) - const alicesPostStatusAfterRevert = await getStatuses({ - subject: alicesPostRef.uriStr, - }) - // Validate that after reverting, the status of the subject is reverted to the last status changing event - expect(alicesPostStatusAfterRevert.subjectStatuses[0]).toMatchObject({ - reviewState: REVIEWCLOSED, - takendown: false, - }) - // Validate that after reverting, the last review date of the subject - // DOES NOT update to the the last status changing event - expect( - new Date( - alicesPostStatusAfterEscalation.subjectStatuses[0] - .lastReviewedAt as string, - ) < - new Date( - alicesPostStatusAfterRevert.subjectStatuses[0] - .lastReviewedAt as string, - ), - ).toBeTruthy() - }) - - it('negates an existing label.', async () => { - const { ctx } = network.bsky - const post = sc.posts[sc.dids.bob][0].ref - const bobsPostSubject = { - $type: 'com.atproto.repo.strongRef', - uri: post.uriStr, - cid: post.cidStr, - } - const labelingService = ctx.services.label(ctx.db.getPrimary()) - await labelingService.formatAndCreate( - ctx.cfg.labelerDid, - post.uriStr, - post.cidStr, - { create: ['kittens'] }, - ) - await emitLabelEvent({ - negateLabelVals: ['kittens'], - createLabelVals: [], - subject: bobsPostSubject, - }) - await expect(getRecordLabels(post.uriStr)).resolves.toEqual([]) - - await emitLabelEvent({ - createLabelVals: ['kittens'], - negateLabelVals: [], - subject: bobsPostSubject, - }) - 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 } = network.bsky - const post = sc.posts[sc.dids.bob][0].ref - const labelingService = ctx.services.label(ctx.db.getPrimary()) - await emitLabelEvent({ - negateLabelVals: ['bears'], - createLabelVals: [], - subject: { - $type: 'com.atproto.repo.strongRef', - uri: post.uriStr, - cid: post.cidStr, - }, - }) - await expect(getRecordLabels(post.uriStr)).resolves.toEqual([]) - await emitLabelEvent({ - createLabelVals: ['bears'], - negateLabelVals: [], - subject: { - $type: 'com.atproto.repo.strongRef', - uri: post.uriStr, - cid: post.cidStr, - }, - }) - 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 - await emitLabelEvent({ - 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 emitLabelEvent({ - negateLabelVals: ['puppies', 'doggies'], - createLabelVals: [], - subject: { - $type: 'com.atproto.repo.strongRef', - uri: post.uriStr, - cid: post.cidStr, - }, - }) - await expect(getRecordLabels(post.uriStr)).resolves.toEqual([]) - }) - - it('creates labels on a repo and reverses.', async () => { - await emitLabelEvent({ - 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 emitLabelEvent({ - negateLabelVals: ['puppies', 'doggies'], - createLabelVals: [], - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.bob, - }, - }) - await expect(getRepoLabels(sc.dids.bob)).resolves.toEqual([]) - }) - - it('creates and negates labels on a repo and reverses.', async () => { - const { ctx } = network.bsky - const labelingService = ctx.services.label(ctx.db.getPrimary()) - await labelingService.formatAndCreate( - ctx.cfg.labelerDid, - sc.dids.bob, - null, - { create: ['kittens'] }, - ) - await emitLabelEvent({ - 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 emitLabelEvent({ - negateLabelVals: ['puppies'], - createLabelVals: ['kittens'], - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.bob, - }, - }) - await expect(getRepoLabels(sc.dids.bob)).resolves.toEqual(['kittens']) - }) - - it('does not allow triage moderators to label.', async () => { - const attemptLabel = agent.api.com.atproto.admin.emitModerationEvent( + it('does not allow non-full moderators to update subject state', async () => { + const subject = { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.bob, + } + const attemptTakedownTriage = + agent.api.com.atproto.admin.updateSubjectStatus( { - event: { - $type: 'com.atproto.admin.defs#modEventLabel', - negateLabelVals: ['a'], - createLabelVals: ['b', 'c'], - }, - createdBy: 'did:example:moderator', - reason: 'Y', - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.bob, - }, + subject, + takedown: { applied: true }, }, { encoding: 'application/json', headers: network.bsky.adminAuthHeaders('triage'), }, ) - await expect(attemptLabel).rejects.toThrow( - 'Must be a full moderator to label content', - ) - }) - - it('does not allow take down event on takendown post or reverse takedown on available post.', async () => { - await performTakedown({ - account: sc.dids.bob, - }) - await expect( - performTakedown({ - account: sc.dids.bob, - }), - ).rejects.toThrow('Subject is already taken down') - - // Cleanup - await performReverseTakedown({ - account: sc.dids.bob, - }) - await expect( - performReverseTakedown({ - account: sc.dids.bob, - }), - ).rejects.toThrow('Subject is not taken down') - }) - it('fans out repo takedowns to pds', async () => { - await performTakedown({ - account: sc.dids.bob, - }) - - const res1 = await pdsAgent.api.com.atproto.admin.getSubjectStatus( - { - did: sc.dids.bob, - }, - { headers: network.pds.adminAuthHeaders() }, - ) - expect(res1.data.takedown?.applied).toBe(true) - - // cleanup - await performReverseTakedown({ account: sc.dids.bob }) - - const res2 = await pdsAgent.api.com.atproto.admin.getSubjectStatus( - { - did: sc.dids.bob, - }, - { headers: network.pds.adminAuthHeaders() }, - ) - expect(res2.data.takedown?.applied).toBe(false) - }) - - it('fans out record takedowns to pds', async () => { - const post = sc.posts[sc.dids.bob][0] - const uri = post.ref.uriStr - const cid = post.ref.cidStr - await performTakedown({ - content: { uri, cid }, - }) - const res1 = await pdsAgent.api.com.atproto.admin.getSubjectStatus( - { uri }, - { headers: network.pds.adminAuthHeaders() }, - ) - expect(res1.data.takedown?.applied).toBe(true) - - // cleanup - await performReverseTakedown({ content: { uri, cid } }) - - const res2 = await pdsAgent.api.com.atproto.admin.getSubjectStatus( - { uri }, - { headers: network.pds.adminAuthHeaders() }, - ) - expect(res2.data.takedown?.applied).toBe(false) - }) - - it('allows full moderators to takedown.', async () => { - await agent.api.com.atproto.admin.emitModerationEvent( - { - event: { - $type: 'com.atproto.admin.defs#modEventTakedown', - }, - createdBy: 'did:example:moderator', - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.bob, - }, - }, - { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('moderator'), - }, - ) - // cleanup - await reverse({ - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.bob, - }, - }) - }) - - it('does not allow non-full moderators to takedown.', async () => { - const attemptTakedownTriage = - agent.api.com.atproto.admin.emitModerationEvent( - { - event: { - $type: 'com.atproto.admin.defs#modEventTakedown', - }, - createdBy: 'did:example:moderator', - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.bob, - }, - }, - { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('triage'), - }, - ) - await expect(attemptTakedownTriage).rejects.toThrow( - 'Must be a full moderator to perform an account takedown', - ) - }) - it('automatically reverses actions marked with duration', async () => { - await createReport({ - reasonType: REASONSPAM, - account: sc.dids.bob, - author: sc.dids.alice, - }) - const { data: action } = await performTakedown({ - account: sc.dids.bob, - // Use negative value to set the expiry time in the past so that the action is automatically reversed - // right away without having to wait n number of hours for a successful assertion - durationInHours: -1, - }) - - const { data: statusesAfterTakedown } = - await agent.api.com.atproto.admin.queryModerationStatuses( - { subject: sc.dids.bob }, - { headers: network.bsky.adminAuthHeaders('moderator') }, - ) - - expect(statusesAfterTakedown.subjectStatuses[0]).toMatchObject({ - takendown: true, - }) - - // In the actual app, this will be instantiated and run on server startup - const periodicReversal = new PeriodicModerationEventReversal( - network.bsky.ctx, - ) - await periodicReversal.findAndRevertDueActions() + await expect(attemptTakedownTriage).rejects.toThrow( + 'Must be a full moderator to update subject state', + ) + const res = await agent.api.com.atproto.admin.getSubjectStatus( + { + did: subject.did, + }, + { headers: network.bsky.adminAuthHeaders('moderator') }, + ) + expect(res.data.takedown?.applied).toBe(false) + }) - const [{ data: eventList }, { data: statuses }] = await Promise.all([ - agent.api.com.atproto.admin.queryModerationEvents( - { subject: sc.dids.bob }, - { headers: network.bsky.adminAuthHeaders('moderator') }, - ), - agent.api.com.atproto.admin.queryModerationStatuses( - { subject: sc.dids.bob }, - { headers: network.bsky.adminAuthHeaders('moderator') }, - ), - ]) + describe('blob takedown', () => { + let blobUri: string + let imageUri: string - expect(statuses.subjectStatuses[0]).toMatchObject({ - takendown: false, - reviewState: REVIEWCLOSED, - }) - // Verify that the automatic reversal is attributed to the original moderator of the temporary action - // and that the reason is set to indicate that the action was automatically reversed. - expect(eventList.events[0]).toMatchObject({ - createdBy: action.createdBy, - event: { - $type: 'com.atproto.admin.defs#modEventReverseTakedown', - comment: - '[SCHEDULED_REVERSAL] Reverting action as originally scheduled', - }, - }) + beforeAll(async () => { + blobUri = `${network.bsky.url}/blob/${blobSubject.did}/${blobSubject.cid}` + imageUri = network.bsky.ctx.imgUriBuilder + .getPresetUri('feed_thumbnail', blobSubject.did, blobSubject.cid) + .replace(network.bsky.ctx.cfg.publicUrl || '', network.bsky.url) + // Warm image server cache + await fetch(imageUri) + const cached = await fetch(imageUri) + expect(cached.headers.get('x-cache')).toEqual('hit') }) - async function emitLabelEvent( - opts: Partial & { - subject: ComAtprotoAdminEmitModerationEvent.InputSchema['subject'] - createLabelVals: ModEventLabel['createLabelVals'] - negateLabelVals: ModEventLabel['negateLabelVals'] - }, - ) { - const { createLabelVals, negateLabelVals } = opts - const result = await agent.api.com.atproto.admin.emitModerationEvent( + it('takes down blobs', async () => { + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { - $type: 'com.atproto.admin.defs#modEventLabel', - createLabelVals, - negateLabelVals, - }, - createdBy: 'did:example:admin', - reason: 'Y', - ...opts, + subject: blobSubject, + takedown: { applied: true, ref: 'test-blob' }, }, { encoding: 'application/json', headers: network.bsky.adminAuthHeaders(), }, ) - return result.data - } - - async function reverse( - opts: Partial & { - subject: ComAtprotoAdminEmitModerationEvent.InputSchema['subject'] - }, - ) { - await agent.api.com.atproto.admin.emitModerationEvent( - { - event: { - $type: 'com.atproto.admin.defs#modEventReverseTakedown', - }, - createdBy: 'did:example:admin', - reason: 'Y', - ...opts, - }, + const res = await agent.api.com.atproto.admin.getSubjectStatus( { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders(), + did: blobSubject.did, + blob: blobSubject.cid, }, + { headers: network.bsky.adminAuthHeaders('moderator') }, ) - } - - async function getRecordLabels(uri: string) { - const result = await agent.api.com.atproto.admin.getRecord( - { uri }, - { headers: network.bsky.adminAuthHeaders() }, - ) - 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: network.bsky.adminAuthHeaders() }, - ) - 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 - beforeAll(async () => { - const { ctx } = network.bsky - post = sc.posts[sc.dids.carol][0] - blob = post.images[1] - imageUri = ctx.imgUriBuilder - .getPresetUri( - 'feed_thumbnail', - sc.dids.carol, - blob.image.ref.toString(), - ) - .replace(ctx.cfg.publicUrl || '', network.bsky.url) - // Warm image server cache - await fetch(imageUri) - const cached = await fetch(imageUri) - expect(cached.headers.get('x-cache')).toEqual('hit') - await performTakedown({ - content: { - uri: post.ref.uriStr, - cid: post.ref.cidStr, - }, - subjectBlobCids: [blob.image.ref.toString()], - }) - }) - - it('sets blobCids in moderation status', async () => { - const { subjectStatuses } = await getStatuses({ - subject: post.ref.uriStr, - }) - - expect(subjectStatuses[0].subjectBlobCids).toEqual([ - blob.image.ref.toString(), - ]) + expect(res.data.subject.did).toEqual(blobSubject.did) + expect(res.data.subject.cid).toEqual(blobSubject.cid) + expect(res.data.takedown?.applied).toBe(true) + expect(res.data.takedown?.ref).toBe('test-blob') }) it('prevents resolution of blob', async () => { - const blobPath = `/blob/${sc.dids.carol}/${blob.image.ref.toString()}` - const resolveBlob = await fetch(`${network.bsky.url}${blobPath}`) + const resolveBlob = await fetch(blobUri) expect(resolveBlob.status).toEqual(404) expect(await resolveBlob.json()).toEqual({ error: 'NotFoundError', @@ -893,29 +217,20 @@ describe('moderation', () => { expect(await fetchImage.json()).toEqual({ message: 'Image not found' }) }) - it('fans takedown out to pds', async () => { - const res = await pdsAgent.api.com.atproto.admin.getSubjectStatus( + it('restores blob when takedown is removed', async () => { + await agent.api.com.atproto.admin.updateSubjectStatus( { - did: sc.dids.carol, - blob: blob.image.ref.toString(), + subject: blobSubject, + takedown: { applied: false }, }, - { headers: network.pds.adminAuthHeaders() }, - ) - expect(res.data.takedown?.applied).toBe(true) - }) - - it('restores blob when action is reversed.', async () => { - await performReverseTakedown({ - content: { - uri: post.ref.uriStr, - cid: post.ref.cidStr, + { + encoding: 'application/json', + headers: network.bsky.adminAuthHeaders(), }, - subjectBlobCids: [blob.image.ref.toString()], - }) + ) // Can resolve blob - const blobPath = `/blob/${sc.dids.carol}/${blob.image.ref.toString()}` - const resolveBlob = await fetch(`${network.bsky.url}${blobPath}`) + const resolveBlob = await fetch(blobUri) expect(resolveBlob.status).toEqual(200) // Can fetch through image server @@ -924,16 +239,5 @@ describe('moderation', () => { const size = Number(fetchImage.headers.get('content-length')) expect(size).toBeGreaterThan(9000) }) - - it('fans reversal out to pds', async () => { - const res = await pdsAgent.api.com.atproto.admin.getSubjectStatus( - { - did: sc.dids.carol, - blob: blob.image.ref.toString(), - }, - { headers: network.pds.adminAuthHeaders() }, - ) - expect(res.data.takedown?.applied).toBe(false) - }) }) }) diff --git a/packages/bsky/tests/algos/hot-classic.test.ts b/packages/bsky/tests/algos/hot-classic.test.ts index bb44ca5c0e8..185cc962c21 100644 --- a/packages/bsky/tests/algos/hot-classic.test.ts +++ b/packages/bsky/tests/algos/hot-classic.test.ts @@ -1,6 +1,5 @@ import AtpAgent, { AtUri } from '@atproto/api' -import { TestNetwork, SeedClient } from '@atproto/dev-env' -import basicSeed from '../seeds/basic' +import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' import { makeAlgos } from '../../src' describe('algo hot-classic', () => { @@ -40,7 +39,7 @@ describe('algo hot-classic', () => { it('returns well liked posts', async () => { const img = await sc.uploadFile( alice, - 'tests/sample-img/key-landscape-small.jpg', + '../dev-env/src/seed/img/key-landscape-small.jpg', 'image/jpeg', ) const one = await sc.post(alice, 'first post', undefined, [img]) diff --git a/packages/bsky/tests/auth.test.ts b/packages/bsky/tests/auth.test.ts index 6b3fbd6b73d..e08049fa84c 100644 --- a/packages/bsky/tests/auth.test.ts +++ b/packages/bsky/tests/auth.test.ts @@ -1,6 +1,5 @@ import AtpAgent from '@atproto/api' -import { SeedClient, TestNetwork } from '@atproto/dev-env' -import usersSeed from './seeds/users' +import { SeedClient, TestNetwork, usersSeed } from '@atproto/dev-env' import { createServiceJwt } from '@atproto/xrpc-server' import { Keypair, Secp256k1Keypair } from '@atproto/crypto' diff --git a/packages/bsky/tests/auto-moderator/fuzzy-matcher.test.ts b/packages/bsky/tests/auto-moderator/fuzzy-matcher.test.ts index 60fe50d582d..f7d539e63b7 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 { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' import { FuzzyMatcher, encode } from '../../src/auto-moderator/fuzzy-matcher' -import basicSeed from '../seeds/basic' import { AtpAgent } from '@atproto/api' import { ImageInvalidator } from '../../src/image/invalidator' @@ -35,9 +34,8 @@ describe('fuzzy matcher', () => { }) const getAllReports = () => { - return network.bsky.ctx.db - .getPrimary() - .db.selectFrom('moderation_event') + return network.ozone.ctx.db.db + .selectFrom('moderation_event') .where('action', '=', 'com.atproto.admin.defs#modEventReport') .selectAll() .orderBy('id', 'asc') diff --git a/packages/bsky/tests/auto-moderator/labeler.test.ts b/packages/bsky/tests/auto-moderator/labeler.test.ts index b7a2fc76e70..b735ebb28b2 100644 --- a/packages/bsky/tests/auto-moderator/labeler.test.ts +++ b/packages/bsky/tests/auto-moderator/labeler.test.ts @@ -1,16 +1,13 @@ -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, usersSeed } 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 { TID } from '@atproto/common' -import { LabelService } from '../../src/services/label' -import usersSeed from '../seeds/users' import { CID } from 'multiformats/cid' import { ImgLabeler } from '../../src/auto-moderator/hive' -import { ModerationService } from '../../src/services/moderation' -import { ImageInvalidator } from '../../src/image/invalidator' +import { TestOzone } from '@atproto/dev-env/src/ozone' // outside of test suite so that TestLabeler can access them let badCid1: CID | undefined = undefined @@ -18,10 +15,9 @@ let badCid2: CID | undefined = undefined describe('labeler', () => { let network: TestNetwork + let ozone: TestOzone let autoMod: AutoModerator - let labelSrvc: LabelService let ctx: IndexerContext - let labelerDid: string let badBlob1: BlobRef let badBlob2: BlobRef let goodBlob: BlobRef @@ -32,12 +28,11 @@ describe('labeler', () => { network = await TestNetwork.create({ dbPostgresSchema: 'bsky_labeler', }) + ozone = network.ozone ctx = network.bsky.indexer.ctx const pdsCtx = network.pds.ctx - labelerDid = ctx.cfg.labelerDid autoMod = ctx.autoMod autoMod.imgLabeler = new TestImgLabeler() - labelSrvc = ctx.services.label(ctx.db) const sc = network.getSeedClient() await usersSeed(sc) await network.processAll() @@ -54,11 +49,7 @@ describe('labeler', () => { constraints: {}, } await store.repo.blob.verifyBlobAndMakePermanent(preparedBlobRef) - await store.repo.blob.associateBlob( - preparedBlobRef, - postUri(), - TID.nextStr(), - ) + await store.repo.blob.associateBlob(preparedBlobRef, postUri()) return blobRef }) } @@ -76,11 +67,15 @@ describe('labeler', () => { await network.close() }) + const getLabels = async (subject: string) => { + return ozone.ctx.db.db + .selectFrom('label') + .selectAll() + .where('uri', '=', subject) + .execute() + } + it('labels text in posts', async () => { - autoMod.services.moderation = ModerationService.creator( - new NoopImageUriBuilder(''), - new NoopInvalidator(), - ) const post = { $type: 'app.bsky.feed.post', text: 'blah blah label_me', @@ -89,11 +84,11 @@ describe('labeler', () => { const cid = await cidForRecord(post) const uri = postUri() autoMod.processRecord(uri, cid, post) - await autoMod.processAll() - const labels = await labelSrvc.getLabels(uri.toString()) + await network.processAll() + const labels = await getLabels(uri.toString()) expect(labels.length).toBe(1) expect(labels[0]).toMatchObject({ - src: labelerDid, + src: ozone.ctx.cfg.service.did, uri: uri.toString(), cid: cid.toString(), val: 'test-label', @@ -102,7 +97,7 @@ describe('labeler', () => { // Verify that along with applying the labels, we are also leaving trace of the label as moderation event // Temporarily assign an instance of moderation service to the autoMod so that we can validate label event - const modSrvc = autoMod.services.moderation(ctx.db) + const modSrvc = ozone.ctx.modService(ozone.ctx.db) const { events } = await modSrvc.getEvents({ includeAllUserRecords: false, subject: uri.toString(), @@ -116,11 +111,8 @@ describe('labeler', () => { createLabelVals: 'test-label', negateLabelVals: null, comment: `[AutoModerator]: Applying labels`, - createdBy: labelerDid, + createdBy: network.bsky.indexer.ctx.cfg.serverDid, }) - - // Cleanup the temporary assignment, knowing that by default, moderation service is not available - autoMod.services.moderation = undefined }) it('labels embeds in posts', async () => { @@ -150,36 +142,12 @@ describe('labeler', () => { const cid = await cidForRecord(post) autoMod.processRecord(uri, cid, post) await autoMod.processAll() - const dbLabels = await labelSrvc.getLabels(uri.toString()) + const dbLabels = await getLabels(uri.toString()) const labels = dbLabels.map((row) => row.val).sort() expect(labels).toEqual( ['test-label', 'test-label-2', 'img-label', 'other-img-label'].sort(), ) }) - - it('retrieves repo labels on profile views', async () => { - await ctx.db.db - .insertInto('label') - .values({ - src: labelerDid, - uri: alice, - cid: '', - val: 'repo-label', - neg: false, - cts: new Date().toISOString(), - }) - .execute() - - const labels = await labelSrvc.getLabelsForProfile(alice) - - expect(labels.length).toBe(1) - expect(labels[0]).toMatchObject({ - src: labelerDid, - uri: alice, - val: 'repo-label', - neg: false, - }) - }) }) class TestImgLabeler implements ImgLabeler { @@ -193,14 +161,3 @@ class TestImgLabeler implements ImgLabeler { return [] } } - -class NoopInvalidator implements ImageInvalidator { - async invalidate() {} -} -class NoopImageUriBuilder { - constructor(public endpoint: string) {} - - getPresetUri() { - return '' - } -} diff --git a/packages/bsky/tests/auto-moderator/takedowns.test.ts b/packages/bsky/tests/auto-moderator/takedowns.test.ts index 8c1f1a21cdd..1b8b4fe53a4 100644 --- a/packages/bsky/tests/auto-moderator/takedowns.test.ts +++ b/packages/bsky/tests/auto-moderator/takedowns.test.ts @@ -1,16 +1,16 @@ import fs from 'fs/promises' -import { TestNetwork, SeedClient, ImageRef } from '@atproto/dev-env' +import { TestNetwork, SeedClient, ImageRef, usersSeed } 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 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' import { ids } from '../../src/lexicon/lexicons' +import { TestOzone } from '@atproto/dev-env/src/ozone' +import { PrimaryDatabase } from '../../src' // outside of test suite so that TestLabeler can access them let badCid1: CID | undefined = undefined @@ -18,9 +18,10 @@ let badCid2: CID | undefined = undefined describe('takedowner', () => { let network: TestNetwork + let ozone: TestOzone + let bskyDb: PrimaryDatabase let autoMod: AutoModerator let testInvalidator: TestInvalidator - let ctx: IndexerContext let pdsAgent: AtpAgent let sc: SeedClient let alice: string @@ -36,8 +37,9 @@ describe('takedowner', () => { imgInvalidator: testInvalidator, }, }) - ctx = network.bsky.indexer.ctx - autoMod = ctx.autoMod + ozone = network.ozone + bskyDb = network.bsky.ctx.db.getPrimary() + autoMod = network.bsky.indexer.ctx.autoMod autoMod.imageFlagger = new TestFlagger() pdsAgent = new AtpAgent({ service: network.pds.url }) sc = network.getSeedClient() @@ -45,26 +47,26 @@ describe('takedowner', () => { await network.processAll() alice = sc.dids.alice const fileBytes1 = await fs.readFile( - 'tests/sample-img/key-portrait-small.jpg', + '../dev-env/src/seed/img/key-portrait-small.jpg', ) const fileBytes2 = await fs.readFile( - 'tests/sample-img/key-portrait-large.jpg', + '../dev-env/src/seed/img/key-portrait-large.jpg', ) badCid1 = sha256RawToCid(await sha256(fileBytes1)) badCid2 = sha256RawToCid(await sha256(fileBytes2)) goodBlob = await sc.uploadFile( alice, - 'tests/sample-img/key-landscape-small.jpg', + '../dev-env/src/seed/img/key-landscape-small.jpg', 'image/jpeg', ) badBlob1 = await sc.uploadFile( alice, - 'tests/sample-img/key-portrait-small.jpg', + '../dev-env/src/seed/img/key-portrait-small.jpg', 'image/jpeg', ) badBlob2 = await sc.uploadFile( alice, - 'tests/sample-img/key-portrait-large.jpg', + '../dev-env/src/seed/img/key-portrait-large.jpg', 'image/jpeg', ) }) @@ -76,9 +78,8 @@ describe('takedowner', () => { it('takes down flagged content in posts', async () => { const post = await sc.post(alice, 'blah', undefined, [goodBlob, badBlob1]) await network.processAll() - await autoMod.processAll() const [modStatus, takedownEvent] = await Promise.all([ - ctx.db.db + ozone.ctx.db.db .selectFrom('moderation_subject_status') .where('did', '=', alice) .where( @@ -88,7 +89,7 @@ describe('takedowner', () => { ) .select(['takendown', 'id']) .executeTakeFirst(), - ctx.db.db + ozone.ctx.db.db .selectFrom('moderation_event') .where('subjectDid', '=', alice) .where('action', '=', 'com.atproto.admin.defs#modEventTakedown') @@ -99,12 +100,12 @@ describe('takedowner', () => { throw new Error('expected mod action') } expect(modStatus.takendown).toEqual(true) - const record = await ctx.db.db + const record = await bskyDb.db .selectFrom('record') .where('uri', '=', post.ref.uriStr) - .select('takedownId') + .select('takedownRef') .executeTakeFirst() - expect(record?.takedownId).toBeGreaterThan(0) + expect(record?.takedownRef).toEqual(`BSKY-TAKEDOWN-${takedownEvent.id}`) const recordPds = await network.pds.ctx.actorStore.read( post.ref.uri.hostname, @@ -115,7 +116,7 @@ describe('takedowner', () => { .select('takedownRef') .executeTakeFirst(), ) - expect(recordPds?.takedownRef).toEqual(takedownEvent.id.toString()) + expect(recordPds?.takedownRef).toEqual(`BSKY-TAKEDOWN-${takedownEvent.id}`) expect(testInvalidator.invalidated.length).toBe(1) expect(testInvalidator.invalidated[0].subject).toBe( @@ -137,13 +138,13 @@ describe('takedowner', () => { ) await network.processAll() const [modStatus, takedownEvent] = await Promise.all([ - ctx.db.db + ozone.ctx.db.db .selectFrom('moderation_subject_status') .where('did', '=', alice) .where('recordPath', '=', `${ids.AppBskyActorProfile}/self`) .select(['takendown', 'id']) .executeTakeFirst(), - ctx.db.db + ozone.ctx.db.db .selectFrom('moderation_event') .where('subjectDid', '=', alice) .where( @@ -159,12 +160,12 @@ describe('takedowner', () => { throw new Error('expected mod action') } expect(modStatus.takendown).toEqual(true) - const record = await ctx.db.db + const recordBsky = await bskyDb.db .selectFrom('record') .where('uri', '=', res.data.uri) - .select('takedownId') + .select('takedownRef') .executeTakeFirst() - expect(record?.takedownId).toBeGreaterThan(0) + expect(recordBsky?.takedownRef).toEqual(`BSKY-TAKEDOWN-${takedownEvent.id}`) const recordPds = await network.pds.ctx.actorStore.read(alice, (store) => store.db.db @@ -173,7 +174,7 @@ describe('takedowner', () => { .select('takedownRef') .executeTakeFirst(), ) - expect(recordPds?.takedownRef).toEqual(takedownEvent.id.toString()) + expect(recordPds?.takedownRef).toEqual(`BSKY-TAKEDOWN-${takedownEvent.id}`) expect(testInvalidator.invalidated.length).toBe(2) expect(testInvalidator.invalidated[1].subject).toBe( diff --git a/packages/bsky/tests/blob-resolver.test.ts b/packages/bsky/tests/blob-resolver.test.ts index 79491c5601e..e428c70ca08 100644 --- a/packages/bsky/tests/blob-resolver.test.ts +++ b/packages/bsky/tests/blob-resolver.test.ts @@ -1,8 +1,7 @@ import axios, { AxiosInstance } from 'axios' import { CID } from 'multiformats/cid' import { verifyCidForBytes } from '@atproto/common' -import { TestNetwork } from '@atproto/dev-env' -import basicSeed from './seeds/basic' +import { TestNetwork, basicSeed } from '@atproto/dev-env' import { randomBytes } from '@atproto/crypto' describe('blob resolver', () => { diff --git a/packages/bsky/tests/daemon.test.ts b/packages/bsky/tests/daemon.test.ts index 32f0d6617ab..cb3c7058cff 100644 --- a/packages/bsky/tests/daemon.test.ts +++ b/packages/bsky/tests/daemon.test.ts @@ -1,8 +1,7 @@ import assert from 'assert' import { AtUri } from '@atproto/api' -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, usersSeed } from '@atproto/dev-env' import { BskyDaemon, DaemonConfig, PrimaryDatabase } from '../src' -import usersSeed from './seeds/users' import { countAll, excluded } from '../src/db/util' import { NotificationsDaemon } from '../src/daemon/notifications' import { diff --git a/packages/bsky/tests/db.test.ts b/packages/bsky/tests/db.test.ts index bb7562e9a92..28008f9897e 100644 --- a/packages/bsky/tests/db.test.ts +++ b/packages/bsky/tests/db.test.ts @@ -75,7 +75,7 @@ describe('db', () => { did: 'x', handle: 'x', indexedAt: 'bad-date', - takedownId: null, + takedownRef: null, }) }) diff --git a/packages/bsky/tests/did-cache.test.ts b/packages/bsky/tests/did-cache.test.ts index 8314981102e..20114779fff 100644 --- a/packages/bsky/tests/did-cache.test.ts +++ b/packages/bsky/tests/did-cache.test.ts @@ -1,5 +1,4 @@ -import { TestNetwork, SeedClient } from '@atproto/dev-env' -import userSeed from './seeds/users' +import { TestNetwork, SeedClient, usersSeed } from '@atproto/dev-env' import { IdResolver } from '@atproto/identity' import DidRedisCache from '../src/did-cache' import { wait } from '@atproto/common' @@ -25,7 +24,7 @@ describe('did cache', () => { redis = network.bsky.indexer.ctx.redis didCache = network.bsky.indexer.ctx.didCache sc = network.getSeedClient() - await userSeed(sc) + await usersSeed(sc) await network.processAll() alice = sc.dids.alice bob = sc.dids.bob diff --git a/packages/bsky/tests/feed-generation.test.ts b/packages/bsky/tests/feed-generation.test.ts index aceecec3204..0cf29ec95c0 100644 --- a/packages/bsky/tests/feed-generation.test.ts +++ b/packages/bsky/tests/feed-generation.test.ts @@ -5,6 +5,7 @@ import { TestFeedGen, SeedClient, RecordRef, + basicSeed, } 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' @@ -14,7 +15,6 @@ import { FeedViewPost, SkeletonFeedPost, } from '../src/lexicon/types/app/bsky/feed/defs' -import basicSeed from './seeds/basic' import { forSnapshot, paginateAll } from './_util' import { AuthRequiredError } from '@atproto/xrpc-server' import assert from 'assert' @@ -157,16 +157,17 @@ describe('feed generation', () => { sc.getHeaders(alice), ) await network.processAll() - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: prime.uri, cid: prime.cid, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', diff --git a/packages/bsky/tests/handle-invalidation.test.ts b/packages/bsky/tests/handle-invalidation.test.ts index cee9cfb61df..70ac7c29a09 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, SeedClient } from '@atproto/dev-env' +import { TestNetwork, SeedClient, usersSeed } from '@atproto/dev-env' import { AtpAgent } from '@atproto/api' -import userSeed from './seeds/users' describe('handle invalidation', () => { let network: TestNetwork @@ -20,7 +19,7 @@ describe('handle invalidation', () => { agent = network.bsky.getClient() pdsAgent = network.pds.getClient() sc = network.getSeedClient() - await userSeed(sc) + await usersSeed(sc) await network.processAll() alice = sc.dids.alice diff --git a/packages/bsky/tests/image/server.test.ts b/packages/bsky/tests/image/server.test.ts index 3bce638ce45..ee4d668945d 100644 --- a/packages/bsky/tests/image/server.test.ts +++ b/packages/bsky/tests/image/server.test.ts @@ -1,9 +1,8 @@ import axios, { AxiosInstance } from 'axios' import { CID } from 'multiformats/cid' import { cidForCbor } from '@atproto/common' -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, basicSeed } from '@atproto/dev-env' import { getInfo } from '../../src/image/sharp' -import basicSeed from '../seeds/basic' import { ImageUriBuilder } from '../../src/image/uri' describe('image processing server', () => { diff --git a/packages/bsky/tests/image/sharp.test.ts b/packages/bsky/tests/image/sharp.test.ts index 17b3b7f3964..2296b0fdbeb 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(`tests/sample-img/${fixture}`) + const image = createReadStream(`../dev-env/src/seed/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 f874a084567..3a5a12b7ac6 100644 --- a/packages/bsky/tests/indexing.test.ts +++ b/packages/bsky/tests/indexing.test.ts @@ -11,10 +11,8 @@ import AtpAgent, { AppBskyFeedRepost, AppBskyGraphFollow, } from '@atproto/api' -import { TestNetwork, SeedClient } from '@atproto/dev-env' +import { TestNetwork, SeedClient, usersSeed, basicSeed } from '@atproto/dev-env' import { forSnapshot } from './_util' -import usersSeed from './seeds/users' -import basicSeed from './seeds/basic' import { ids } from '../src/lexicon/lexicons' import { Database } from '../src/db' diff --git a/packages/bsky/tests/notification-server.test.ts b/packages/bsky/tests/notification-server.test.ts index 6f9c8b00224..11b9f2395e8 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, SeedClient } from '@atproto/dev-env' -import basicSeed from './seeds/basic' +import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' import { NotificationServer } from '../src/notifications' import { Database } from '../src' diff --git a/packages/bsky/tests/pipeline/backpressure.test.ts b/packages/bsky/tests/pipeline/backpressure.test.ts index 583d749100e..87e01b8cc89 100644 --- a/packages/bsky/tests/pipeline/backpressure.test.ts +++ b/packages/bsky/tests/pipeline/backpressure.test.ts @@ -6,8 +6,8 @@ import { getIngester, processAll, SeedClient, + basicSeed, } from '@atproto/dev-env' -import basicSeed from '../seeds/basic' import { BskyIngester } from '../../src' const TEST_NAME = 'pipeline_backpressure' diff --git a/packages/bsky/tests/pipeline/reingest.test.ts b/packages/bsky/tests/pipeline/reingest.test.ts index 3c860bcf680..8d90f9fea8f 100644 --- a/packages/bsky/tests/pipeline/reingest.test.ts +++ b/packages/bsky/tests/pipeline/reingest.test.ts @@ -3,8 +3,8 @@ import { SeedClient, getIngester, ingestAll, + basicSeed, } from '@atproto/dev-env' -import basicSeed from '../seeds/basic' import { BskyIngester } from '../../src' const TEST_NAME = 'pipeline_reingest' diff --git a/packages/bsky/tests/pipeline/repartition.test.ts b/packages/bsky/tests/pipeline/repartition.test.ts index f228b954fb6..2c7470fc06d 100644 --- a/packages/bsky/tests/pipeline/repartition.test.ts +++ b/packages/bsky/tests/pipeline/repartition.test.ts @@ -6,8 +6,8 @@ import { getIngester, ingestAll, processAll, + usersSeed, } from '@atproto/dev-env' -import usersSeed from '../seeds/users' import { BskyIngester } from '../../src' import { countAll } from '../../src/db/util' diff --git a/packages/bsky/tests/reprocessing.test.ts b/packages/bsky/tests/reprocessing.test.ts index 046bc58076b..fd9199379c7 100644 --- a/packages/bsky/tests/reprocessing.test.ts +++ b/packages/bsky/tests/reprocessing.test.ts @@ -1,7 +1,6 @@ import axios from 'axios' import { AtUri } from '@atproto/syntax' -import { TestNetwork, SeedClient } from '@atproto/dev-env' -import basicSeed from './seeds/basic' +import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' import { Database } from '../src/db' describe('reprocessing', () => { diff --git a/packages/bsky/tests/server.test.ts b/packages/bsky/tests/server.test.ts index 3f54b2a37bd..157b352136f 100644 --- a/packages/bsky/tests/server.test.ts +++ b/packages/bsky/tests/server.test.ts @@ -1,10 +1,9 @@ import { AddressInfo } from 'net' import express from 'express' import axios, { AxiosError } from 'axios' -import { TestNetwork } from '@atproto/dev-env' +import { TestNetwork, basicSeed } from '@atproto/dev-env' import { handler as errorHandler } from '../src/error' import { Database } from '../src' -import basicSeed from './seeds/basic' describe('server', () => { let network: TestNetwork diff --git a/packages/bsky/tests/subscription/repo.test.ts b/packages/bsky/tests/subscription/repo.test.ts index 1c83e4c0cca..fe910c85603 100644 --- a/packages/bsky/tests/subscription/repo.test.ts +++ b/packages/bsky/tests/subscription/repo.test.ts @@ -1,5 +1,5 @@ import AtpAgent from '@atproto/api' -import { TestNetwork, SeedClient } from '@atproto/dev-env' +import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' import { CommitData } from '@atproto/repo' import { PreparedWrite } from '@atproto/pds/src/repo' import * as sequencer from '@atproto/pds/src/sequencer' @@ -8,7 +8,6 @@ import { DatabaseSchemaType } from '../../src/db/database-schema' import { ids } from '../../src/lexicon/lexicons' import { forSnapshot } from '../_util' import { AppContext, Database } from '../../src' -import basicSeed from '../seeds/basic' describe('sync', () => { let network: TestNetwork 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 9d1d41bd3db..37478713bd9 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/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -89,7 +89,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(3)", "val": "test-label", }, @@ -97,7 +97,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(3)", "val": "test-label-2", }, @@ -110,7 +110,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -221,7 +221,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(4)", + "did": "user(5)", "handle": "dan.test", "labels": Array [], "viewer": Object { @@ -237,7 +237,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(5)", + "did": "user(6)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -260,7 +260,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -271,7 +271,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -332,7 +332,7 @@ Array [ "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(6)", "val": "test-label", }, @@ -486,7 +486,7 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -498,7 +498,7 @@ Array [ "cid": "cids(0)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(2)", "uri": "record(0)", "val": "test-label", }, @@ -506,7 +506,7 @@ Array [ "cid": "cids(0)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(2)", "uri": "record(0)", "val": "test-label-2", }, @@ -519,7 +519,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -552,8 +552,8 @@ Array [ "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)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", + "did": "user(3)", "displayName": "ali", "handle": "alice.test", "labels": Array [ @@ -561,7 +561,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(2)", + "src": "user(3)", "uri": "record(4)", "val": "self-label-a", }, @@ -569,7 +569,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(2)", + "src": "user(3)", "uri": "record(4)", "val": "self-label-b", }, @@ -600,8 +600,8 @@ Array [ "root": 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)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", + "did": "user(3)", "displayName": "ali", "handle": "alice.test", "labels": Array [ @@ -609,7 +609,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(2)", + "src": "user(3)", "uri": "record(4)", "val": "self-label-a", }, @@ -617,7 +617,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(2)", + "src": "user(3)", "uri": "record(4)", "val": "self-label-b", }, @@ -745,12 +745,12 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/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/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -811,7 +811,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/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/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/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/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1102,7 +1102,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/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/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -1236,7 +1236,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(4)", "val": "test-label", }, @@ -1244,7 +1244,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(4)", "val": "test-label-2", }, @@ -1257,7 +1257,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1415,7 +1415,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(5)", + "did": "user(6)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -1431,14 +1431,14 @@ Array [ "$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(6)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(5)@jpeg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(5)@jpeg", }, Object { - "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", + "alt": "../dev-env/src/seed/img/key-alt.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(8)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(8)@jpeg", }, ], }, @@ -1486,7 +1486,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1497,7 +1497,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/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/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -1674,7 +1674,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(5)", "val": "test-label", }, @@ -1682,7 +1682,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(5)", "val": "test-label-2", }, @@ -1695,7 +1695,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1812,7 +1812,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(4)", + "did": "user(5)", "handle": "dan.test", "labels": Array [], "viewer": Object { @@ -1827,7 +1827,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(5)", + "did": "user(6)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -1848,7 +1848,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1859,7 +1859,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1920,7 +1920,7 @@ Array [ "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(8)", "val": "test-label", }, 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 7f0989a5975..a3cfb905dc9 100644 --- a/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap @@ -103,7 +103,7 @@ Object { "cid": "cids(0)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(0)", "val": "test-label", }, diff --git a/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap b/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap index 2a27fcf4955..d5ecf9b2c7e 100644 --- a/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap @@ -103,7 +103,7 @@ Object { "cid": "cids(0)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(0)", "val": "test-label", }, @@ -289,7 +289,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -301,7 +301,7 @@ Object { "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(7)", "val": "test-label", }, @@ -309,7 +309,7 @@ Object { "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(7)", "val": "test-label-2", }, @@ -322,7 +322,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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 d6712c89c56..790cc5db4e6 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/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -90,7 +90,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(5)", "val": "test-label", }, @@ -98,7 +98,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(5)", "val": "test-label-2", }, @@ -111,7 +111,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -221,7 +221,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(5)", "val": "test-label", }, @@ -229,7 +229,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(5)", "val": "test-label-2", }, @@ -242,7 +242,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -408,7 +408,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(4)", + "did": "user(5)", "handle": "dan.test", "labels": Array [], "viewer": Object { @@ -423,7 +423,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(5)", + "did": "user(6)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -444,7 +444,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -455,7 +455,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -516,7 +516,7 @@ Array [ "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(8)", "val": "test-label", }, 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 d4b11f0d235..8b46475eafe 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/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -240,7 +240,7 @@ Object { "cid": "cids(5)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(9)", "val": "test-label", }, @@ -248,7 +248,7 @@ Object { "cid": "cids(5)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(9)", "val": "test-label-2", }, @@ -261,7 +261,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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 0e1c14c2696..655d7b62cb6 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/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -217,7 +217,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(5)", "val": "test-label", }, @@ -225,7 +225,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(5)", "val": "test-label-2", }, @@ -238,7 +238,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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 bce3d4e5139..e2ac2d587c0 100644 --- a/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap @@ -250,7 +250,7 @@ Array [ "cid": "cids(12)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(14)", "val": "test-label", }, @@ -258,7 +258,7 @@ Array [ "cid": "cids(12)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(14)", "val": "test-label-2", }, @@ -272,7 +272,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -694,7 +694,7 @@ Array [ "cid": "cids(15)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(17)", "val": "test-label", }, @@ -702,7 +702,7 @@ Array [ "cid": "cids(15)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(17)", "val": "test-label-2", }, @@ -716,7 +716,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -921,7 +921,7 @@ Array [ "cid": "cids(5)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(4)", "val": "test-label", }, diff --git a/packages/bsky/tests/views/__snapshots__/posts.test.ts.snap b/packages/bsky/tests/views/__snapshots__/posts.test.ts.snap index 3b14a184dc1..b1abb293f8b 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/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/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/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -223,7 +223,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/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/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/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/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -353,7 +353,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/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 fb0fd6a3224..dd71dc9010d 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/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -85,7 +85,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(5)", "val": "test-label", }, @@ -93,7 +93,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(5)", "val": "test-label-2", }, @@ -106,7 +106,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -307,7 +307,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -319,7 +319,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(7)", "val": "test-label", }, @@ -327,7 +327,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(7)", "val": "test-label-2", }, @@ -340,7 +340,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -545,7 +545,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -557,7 +557,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(7)", "val": "test-label", }, @@ -565,7 +565,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(7)", "val": "test-label-2", }, @@ -578,7 +578,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1092,7 +1092,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -1104,7 +1104,7 @@ Object { "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(5)", "val": "test-label", }, @@ -1112,7 +1112,7 @@ Object { "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(5)", "val": "test-label-2", }, @@ -1125,7 +1125,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1292,7 +1292,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -1304,7 +1304,7 @@ Object { "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(5)", "val": "test-label", }, @@ -1312,7 +1312,7 @@ Object { "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(5)", "val": "test-label-2", }, @@ -1325,7 +1325,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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 b5863382fef..0817313a331 100644 --- a/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap @@ -278,7 +278,7 @@ Array [ "cid": "cids(5)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(5)", "val": "test-label", }, @@ -853,7 +853,7 @@ Array [ "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(8)", "val": "test-label", }, @@ -880,8 +880,8 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", - "did": "user(4)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(1)@jpeg", + "did": "user(5)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -999,14 +999,14 @@ Array [ "$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(6)/cids(11)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(11)@jpeg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(11)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(11)@jpeg", }, Object { - "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", + "alt": "../dev-env/src/seed/img/key-alt.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(12)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(12)@jpeg", }, ], }, @@ -1014,8 +1014,8 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", - "did": "user(4)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(1)@jpeg", + "did": "user(5)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -1034,7 +1034,7 @@ Array [ "cid": "cids(13)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(15)", "val": "kind", }, @@ -1058,7 +1058,7 @@ Array [ "cid": "cids(10)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(14)", "val": "kind", }, @@ -1073,7 +1073,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1084,7 +1084,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1116,8 +1116,8 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", - "did": "user(4)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(1)@jpeg", + "did": "user(5)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -1135,7 +1135,7 @@ Array [ "cid": "cids(13)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(15)", "val": "kind", }, @@ -1312,7 +1312,7 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -1324,7 +1324,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(3)", "val": "test-label", }, @@ -1332,7 +1332,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(3)", "val": "test-label-2", }, @@ -1345,7 +1345,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1497,7 +1497,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(5)", + "did": "user(6)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -1515,14 +1515,14 @@ Array [ "$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(6)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(5)@jpeg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(5)@jpeg", }, Object { - "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", + "alt": "../dev-env/src/seed/img/key-alt.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(8)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(8)@jpeg", }, ], }, @@ -1549,7 +1549,7 @@ Array [ "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(11)", "val": "kind", }, @@ -1574,7 +1574,7 @@ Array [ "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(8)", "val": "kind", }, @@ -1589,7 +1589,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1600,7 +1600,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1660,7 +1660,7 @@ Array [ "reason": Object { "$type": "app.bsky.feed.defs#reasonRepost", "by": Object { - "did": "user(5)", + "did": "user(6)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -1748,7 +1748,7 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -1760,7 +1760,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(3)", "val": "test-label", }, @@ -1768,7 +1768,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(3)", "val": "test-label-2", }, @@ -1781,7 +1781,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1859,7 +1859,7 @@ Array [ Object { "post": Object { "author": Object { - "did": "user(5)", + "did": "user(6)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -2004,7 +2004,7 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -2016,7 +2016,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(3)", "val": "test-label", }, @@ -2024,7 +2024,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(3)", "val": "test-label-2", }, @@ -2037,7 +2037,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2209,7 +2209,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(5)", + "did": "user(6)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -2226,7 +2226,7 @@ Array [ "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(8)", "val": "kind", }, @@ -2241,7 +2241,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2252,7 +2252,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2313,7 +2313,7 @@ Array [ "cid": "cids(11)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(13)", "val": "test-label", }, @@ -2430,7 +2430,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(5)", + "did": "user(6)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -2448,14 +2448,14 @@ Array [ "$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(6)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(5)@jpeg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(5)@jpeg", }, Object { - "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", + "alt": "../dev-env/src/seed/img/key-alt.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(8)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(8)@jpeg", }, ], }, @@ -2482,7 +2482,7 @@ Array [ "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(11)", "val": "kind", }, @@ -2507,7 +2507,7 @@ Array [ "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(8)", "val": "kind", }, @@ -2522,7 +2522,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2533,7 +2533,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2621,7 +2621,7 @@ Array [ Object { "post": Object { "author": Object { - "did": "user(5)", + "did": "user(6)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -2638,14 +2638,14 @@ Array [ "$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(6)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(5)@jpeg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(5)@jpeg", }, Object { - "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", + "alt": "../dev-env/src/seed/img/key-alt.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(8)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(8)@jpeg", }, ], }, @@ -2673,7 +2673,7 @@ Array [ "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(11)", "val": "kind", }, @@ -2697,7 +2697,7 @@ Array [ "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(8)", "val": "kind", }, @@ -2712,7 +2712,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2723,7 +2723,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2774,7 +2774,7 @@ Array [ "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(11)", "val": "kind", }, @@ -2897,14 +2897,14 @@ Array [ "$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(5)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(2)@jpeg", }, Object { - "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", + "alt": "../dev-env/src/seed/img/key-alt.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(3)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(3)@jpeg", }, ], }, @@ -2912,8 +2912,8 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(5)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -2929,7 +2929,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(4)", "val": "kind", }, @@ -2954,7 +2954,7 @@ Array [ "cid": "cids(1)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(2)", "val": "kind", }, @@ -2969,7 +2969,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2980,7 +2980,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -3055,7 +3055,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3112,8 +3112,8 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(5)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -3127,9 +3127,9 @@ Array [ "$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(4)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(2)@jpeg", + "alt": "../dev-env/src/seed/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", }, ], }, @@ -3139,7 +3139,7 @@ Array [ "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(10)", "val": "test-label", }, @@ -3147,7 +3147,7 @@ Array [ "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(10)", "val": "test-label-2", }, @@ -3160,7 +3160,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -3192,7 +3192,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3279,7 +3279,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3327,7 +3327,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3377,8 +3377,8 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(5)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -3392,9 +3392,9 @@ Array [ "$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(4)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(2)@jpeg", + "alt": "../dev-env/src/seed/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", }, ], }, @@ -3404,7 +3404,7 @@ Array [ "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(10)", "val": "test-label", }, @@ -3412,7 +3412,7 @@ Array [ "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(10)", "val": "test-label-2", }, @@ -3425,7 +3425,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -3458,7 +3458,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3506,7 +3506,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3556,7 +3556,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3623,7 +3623,7 @@ Array [ "cid": "cids(1)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(2)", "val": "kind", }, @@ -3638,7 +3638,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -3649,7 +3649,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -3710,7 +3710,7 @@ Array [ "cid": "cids(11)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(13)", "val": "test-label", }, @@ -3739,8 +3739,8 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(5)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -3767,7 +3767,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3832,14 +3832,14 @@ Array [ "$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(5)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(2)@jpeg", }, Object { - "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", + "alt": "../dev-env/src/seed/img/key-alt.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(3)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(3)@jpeg", }, ], }, @@ -3847,8 +3847,8 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(5)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -3865,7 +3865,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(4)", "val": "kind", }, @@ -3889,7 +3889,7 @@ Array [ "cid": "cids(1)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(2)", "val": "kind", }, @@ -3904,7 +3904,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -3915,7 +3915,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -3947,8 +3947,8 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(5)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -3964,7 +3964,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(4)", "val": "kind", }, @@ -3988,7 +3988,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -4087,14 +4087,14 @@ Array [ "$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(5)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(2)@jpeg", }, Object { - "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", + "alt": "../dev-env/src/seed/img/key-alt.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(3)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(3)@jpeg", }, ], }, @@ -4102,8 +4102,8 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(5)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -4120,7 +4120,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(2)", "val": "kind", }, @@ -4145,7 +4145,7 @@ Array [ "cid": "cids(1)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(1)", "val": "kind", }, @@ -4160,7 +4160,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -4171,7 +4171,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -4247,7 +4247,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -4304,8 +4304,8 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(5)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -4320,9 +4320,9 @@ Array [ "$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(4)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(2)@jpeg", + "alt": "../dev-env/src/seed/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", }, ], }, @@ -4332,7 +4332,7 @@ Array [ "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(10)", "val": "test-label", }, @@ -4340,7 +4340,7 @@ Array [ "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(10)", "val": "test-label-2", }, @@ -4353,7 +4353,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -4385,7 +4385,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -4471,7 +4471,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -4519,7 +4519,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -4569,7 +4569,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -4634,7 +4634,7 @@ Array [ "cid": "cids(1)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(1)", "val": "kind", }, @@ -4649,7 +4649,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -4660,7 +4660,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -4721,7 +4721,7 @@ Array [ "cid": "cids(11)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(13)", "val": "test-label", }, @@ -4750,7 +4750,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -4814,14 +4814,14 @@ Array [ "$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(5)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(2)@jpeg", }, Object { - "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", + "alt": "../dev-env/src/seed/img/key-alt.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(3)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(3)@jpeg", }, ], }, @@ -4829,8 +4829,8 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(5)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -4848,7 +4848,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(2)", "val": "kind", }, @@ -4872,7 +4872,7 @@ Array [ "cid": "cids(1)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(1)", "val": "kind", }, @@ -4887,7 +4887,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -4898,7 +4898,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -4928,7 +4928,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -5084,7 +5084,7 @@ Array [ "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -5096,7 +5096,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(4)", "val": "test-label", }, @@ -5104,7 +5104,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(4)", "val": "test-label-2", }, @@ -5117,7 +5117,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -5289,7 +5289,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(4)", "val": "test-label", }, @@ -5297,7 +5297,7 @@ Array [ "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(4)", "val": "test-label-2", }, @@ -5310,7 +5310,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -5484,7 +5484,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(5)", + "did": "user(6)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -5500,14 +5500,14 @@ Array [ "$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(6)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(5)@jpeg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(5)@jpeg", }, Object { - "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", + "alt": "../dev-env/src/seed/img/key-alt.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(9)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(9)@jpeg", }, ], }, @@ -5533,7 +5533,7 @@ Array [ "cid": "cids(10)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(12)", "val": "kind", }, @@ -5558,7 +5558,7 @@ Array [ "cid": "cids(8)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(11)", "val": "kind", }, @@ -5573,7 +5573,7 @@ Array [ "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -5584,7 +5584,7 @@ Array [ }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -5689,7 +5689,7 @@ Array [ "cid": "cids(10)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(5)", "uri": "record(12)", "val": "kind", }, diff --git a/packages/bsky/tests/views/actor-likes.test.ts b/packages/bsky/tests/views/actor-likes.test.ts index 642b37e6446..9d8bf1b87b5 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, SeedClient } from '@atproto/dev-env' -import basicSeed from '../seeds/basic' +import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' describe('bsky actor likes feed views', () => { let network: TestNetwork diff --git a/packages/bsky/tests/views/actor-search.test.ts b/packages/bsky/tests/views/actor-search.test.ts index 70f8862f7d7..c0e862de249 100644 --- a/packages/bsky/tests/views/actor-search.test.ts +++ b/packages/bsky/tests/views/actor-search.test.ts @@ -1,8 +1,7 @@ import AtpAgent from '@atproto/api' import { wait } from '@atproto/common' -import { TestNetwork, SeedClient } from '@atproto/dev-env' +import { TestNetwork, SeedClient, usersBulkSeed } from '@atproto/dev-env' import { forSnapshot, paginateAll, stripViewer } from '../_util' -import usersBulkSeed from '../seeds/users-bulk' // @NOTE skipped to help with CI failures // The search code is not used in production & we should switch it out for tests on the search proxy interface @@ -239,15 +238,16 @@ describe.skip('pds actor search views', () => { }) it('search blocks by actor takedown', async () => { - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: sc.dids['cara-wiegand69.test'], }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', diff --git a/packages/bsky/tests/views/admin/repo-search.test.ts b/packages/bsky/tests/views/admin/repo-search.test.ts deleted file mode 100644 index 6d9e8468dc1..00000000000 --- a/packages/bsky/tests/views/admin/repo-search.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -import AtpAgent, { ComAtprotoAdminSearchRepos } from '@atproto/api' -import { wait } from '@atproto/common' -import { TestNetwork, SeedClient } from '@atproto/dev-env' -import usersBulkSeed from '../../seeds/users-bulk' - -describe('pds admin repo search views', () => { - let network: TestNetwork - let agent: AtpAgent - let sc: SeedClient - let headers: { [s: string]: string } - // In results that don't have a related profile record, we will only have handle but not a name - // And names are usually capitalized on each word so the comparison is done on lowercase version - const handleOrNameStartsWith = - (term: string) => (handleOrName: (string | undefined)[]) => - !!handleOrName.find((str) => - str?.toLowerCase().includes(term.toLowerCase()), - ) - const resultToHandlesAndNames = ( - result: ComAtprotoAdminSearchRepos.Response, - ) => - result.data.repos.map((u: any) => [ - u.handle, - (u.relatedRecords[0] as Record)?.displayName, - ]) - - beforeAll(async () => { - network = await TestNetwork.create({ - dbPostgresSchema: 'bsky_views_repo_search', - }) - agent = network.bsky.getClient() - sc = network.getSeedClient() - - await wait(100) // allow pending sub to be established - await network.bsky.ingester.sub.destroy() - await usersBulkSeed(sc) - - // Skip did/handle resolution for expediency - const db = network.bsky.ctx.db.getPrimary() - const now = new Date().toISOString() - await db.db - .insertInto('actor') - .values( - Object.entries(sc.dids).map(([handle, did]) => ({ - did, - handle, - indexedAt: now, - })), - ) - .onConflict((oc) => oc.doNothing()) - .execute() - - // Process remaining profiles - network.bsky.ingester.sub.resume() - await network.processAll(50000) - headers = await network.adminHeaders({}) - }) - - afterAll(async () => { - await network.close() - }) - - it('gives relevant results when searched by handle', async () => { - const term = 'car' - const result = await agent.api.com.atproto.admin.searchRepos( - { term }, - { headers }, - ) - - const shouldContain = [ - // Present despite repo takedown - // First item in the array because of direct handle match - 'cara-wiegand69.test', - 'carlos6.test', - 'aliya-hodkiewicz.test', // Carlton Abernathy IV - 'eudora-dietrich4.test', // Carol Littel - 'carolina-mcdermott77.test', - 'shane-torphy52.test', // Sadie Carter - // Last item in the array because handle and display name none match very close to the the search term - 'cayla-marquardt39.test', - ] - - const handlesAndNames = resultToHandlesAndNames(result) - const handles = handlesAndNames.map(([handle]) => handle) - // Assert that all matches are found - shouldContain.forEach((handle) => expect(handles).toContain(handle)) - // Assert that the order is correct, showing the closest match by handle first - const containsTerm = handleOrNameStartsWith(term) - expect(containsTerm(handlesAndNames[0])).toBeTruthy() - expect( - containsTerm(handlesAndNames[handlesAndNames.length - 1]), - ).toBeFalsy() - }) - - it('pagination respects matching order when searched by handle', async () => { - const term = 'car' - const resultPageOne = await agent.api.com.atproto.admin.searchRepos( - { term, limit: 4 }, - { headers }, - ) - const resultPageTwo = await agent.api.com.atproto.admin.searchRepos( - { term, limit: 4, cursor: resultPageOne.data.cursor }, - { headers }, - ) - - const handlesAndNamesPageOne = resultToHandlesAndNames(resultPageOne) - const handlesAndNamesPageTwo = resultToHandlesAndNames(resultPageTwo) - const containsTerm = handleOrNameStartsWith(term) - - // First result of first page always has matches either handle or did - expect(containsTerm(handlesAndNamesPageOne[0])).toBeTruthy() - // Since we only get 4 items per page max and know that among the test dataset - // at least 4 users have the term in handle or profile, last item in first page - // should contain the term - expect( - containsTerm(handlesAndNamesPageOne[handlesAndNamesPageOne.length - 1]), - ).toBeTruthy() - // However, the last item of second page, should not contain the term - expect( - containsTerm(handlesAndNamesPageTwo[handlesAndNamesPageTwo.length - 1]), - ).toBeFalsy() - }) - - it('gives relevant results when searched by did', async () => { - const term = sc.dids['cara-wiegand69.test'] - const res = await agent.api.com.atproto.admin.searchRepos( - { term }, - { headers }, - ) - - expect(res.data.repos.length).toEqual(1) - expect(res.data.repos[0].did).toEqual(term) - }) -}) diff --git a/packages/bsky/tests/views/author-feed.test.ts b/packages/bsky/tests/views/author-feed.test.ts index c5d863bfb92..4db9ee49028 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, SeedClient } from '@atproto/dev-env' +import { TestNetwork, SeedClient, authorFeedSeed } from '@atproto/dev-env' import { forSnapshot, paginateAll, stripViewerFromPost } from '../_util' -import authorFeedSeed from '../seeds/author-feed' import { isRecord } from '../../src/lexicon/types/app/bsky/feed/post' import { isView as isEmbedRecordWithMedia } from '../../src/lexicon/types/app/bsky/embed/recordWithMedia' import { isView as isImageEmbed } from '../../src/lexicon/types/app/bsky/embed/images' @@ -147,15 +146,16 @@ describe('pds author feed views', () => { expect(preBlock.feed.length).toBeGreaterThan(0) - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: alice, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', @@ -170,15 +170,15 @@ describe('pds author feed views', () => { await expect(attempt).rejects.toThrow('Profile not found') // Cleanup - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: alice, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', @@ -197,16 +197,17 @@ describe('pds author feed views', () => { const post = preBlock.feed[0].post - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: post.uri, cid: post.cid, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', @@ -223,16 +224,16 @@ describe('pds author feed views', () => { expect(postBlock.feed.map((item) => item.post.uri)).not.toContain(post.uri) // Cleanup - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: post.uri, cid: post.cid, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', diff --git a/packages/bsky/tests/views/block-lists.test.ts b/packages/bsky/tests/views/block-lists.test.ts index 6672d690ce1..d64927c2378 100644 --- a/packages/bsky/tests/views/block-lists.test.ts +++ b/packages/bsky/tests/views/block-lists.test.ts @@ -1,7 +1,6 @@ import AtpAgent from '@atproto/api' -import { TestNetwork, SeedClient, RecordRef } from '@atproto/dev-env' +import { TestNetwork, SeedClient, RecordRef, basicSeed } from '@atproto/dev-env' import { forSnapshot } from '../_util' -import basicSeed from '../seeds/basic' import { BlockedActorError } from '@atproto/api/src/client/types/app/bsky/feed/getAuthorFeed' import { BlockedByActorError } from '@atproto/api/src/client/types/app/bsky/feed/getAuthorFeed' @@ -50,7 +49,7 @@ describe('pds views with blocking from block lists', () => { it('creates a list with some items', async () => { const avatar = await sc.uploadFile( alice, - 'tests/sample-img/key-portrait-small.jpg', + '../dev-env/src/seed/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 74079c7f7c6..2f45477c664 100644 --- a/packages/bsky/tests/views/blocks.test.ts +++ b/packages/bsky/tests/views/blocks.test.ts @@ -1,5 +1,5 @@ import assert from 'assert' -import { TestNetwork, RecordRef, SeedClient } from '@atproto/dev-env' +import { TestNetwork, RecordRef, SeedClient, basicSeed } 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' @@ -9,7 +9,6 @@ import { isViewBlocked as isEmbedViewBlocked, } from '@atproto/api/src/client/types/app/bsky/embed/record' import { forSnapshot } from '../_util' -import basicSeed from '../seeds/basic' describe('pds views with blocking', () => { let network: TestNetwork diff --git a/packages/bsky/tests/views/follows.test.ts b/packages/bsky/tests/views/follows.test.ts index f290ec622d5..8367f2d1f61 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, SeedClient } from '@atproto/dev-env' +import { TestNetwork, SeedClient, followsSeed } from '@atproto/dev-env' import { forSnapshot, paginateAll, stripViewer } from '../_util' -import followsSeed from '../seeds/follows' describe('pds follow views', () => { let agent: AtpAgent @@ -120,15 +119,16 @@ describe('pds follow views', () => { }) it('blocks followers by actor takedown', async () => { - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: sc.dids.dan, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', @@ -145,15 +145,15 @@ describe('pds follow views', () => { sc.dids.dan, ) - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: sc.dids.dan, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', @@ -252,15 +252,16 @@ describe('pds follow views', () => { }) it('blocks follows by actor takedown', async () => { - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: sc.dids.dan, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', @@ -277,15 +278,15 @@ describe('pds follow views', () => { sc.dids.dan, ) - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: sc.dids.dan, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', diff --git a/packages/bsky/tests/views/likes.test.ts b/packages/bsky/tests/views/likes.test.ts index f8f9c9a7fef..dd5135357bb 100644 --- a/packages/bsky/tests/views/likes.test.ts +++ b/packages/bsky/tests/views/likes.test.ts @@ -1,6 +1,5 @@ import AtpAgent from '@atproto/api' -import { TestNetwork, SeedClient } from '@atproto/dev-env' -import likesSeed from '../seeds/likes' +import { TestNetwork, SeedClient, likesSeed } from '@atproto/dev-env' import { constantDate, forSnapshot, paginateAll, stripViewer } from '../_util' describe('pds like views', () => { diff --git a/packages/bsky/tests/views/list-feed.test.ts b/packages/bsky/tests/views/list-feed.test.ts index b8cd977922b..4951a6d6a23 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, SeedClient, RecordRef } from '@atproto/dev-env' +import { TestNetwork, SeedClient, RecordRef, basicSeed } from '@atproto/dev-env' import { forSnapshot, paginateAll, stripViewerFromPost } from '../_util' -import basicSeed from '../seeds/basic' describe('list feed views', () => { let network: TestNetwork @@ -112,15 +111,16 @@ describe('list feed views', () => { }) it('blocks posts by actor takedown', async () => { - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: bob, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', @@ -135,15 +135,15 @@ describe('list feed views', () => { expect(hasBob).toBe(false) // Cleanup - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: bob, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', @@ -154,16 +154,17 @@ describe('list feed views', () => { it('blocks posts by record takedown.', async () => { const postRef = sc.replies[bob][0].ref // Post and reply parent - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: postRef.uriStr, cid: postRef.cidStr, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', @@ -180,16 +181,16 @@ describe('list feed views', () => { expect(hasPost).toBe(false) // Cleanup - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: postRef.uriStr, cid: postRef.cidStr, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', diff --git a/packages/bsky/tests/views/mute-lists.test.ts b/packages/bsky/tests/views/mute-lists.test.ts index 07a6690f910..bd8242d3b48 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, SeedClient, RecordRef } from '@atproto/dev-env' +import { TestNetwork, SeedClient, RecordRef, basicSeed } from '@atproto/dev-env' import { forSnapshot } from '../_util' -import basicSeed from '../seeds/basic' describe('bsky views with mutes from mute lists', () => { let network: TestNetwork @@ -42,7 +41,7 @@ describe('bsky views with mutes from mute lists', () => { it('creates a list with some items', async () => { const avatar = await sc.uploadFile( alice, - 'tests/sample-img/key-portrait-small.jpg', + '../dev-env/src/seed/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 6a00c427124..8e26770ef23 100644 --- a/packages/bsky/tests/views/mutes.test.ts +++ b/packages/bsky/tests/views/mutes.test.ts @@ -1,8 +1,11 @@ import AtpAgent from '@atproto/api' -import { TestNetwork, SeedClient } from '@atproto/dev-env' +import { + TestNetwork, + SeedClient, + basicSeed, + usersBulkSeed, +} from '@atproto/dev-env' import { forSnapshot, paginateAll } from '../_util' -import basicSeed from '../seeds/basic' -import usersBulkSeed from '../seeds/users-bulk' describe('mute views', () => { let network: TestNetwork diff --git a/packages/bsky/tests/views/notifications.test.ts b/packages/bsky/tests/views/notifications.test.ts index c75ee7b699e..fad620288af 100644 --- a/packages/bsky/tests/views/notifications.test.ts +++ b/packages/bsky/tests/views/notifications.test.ts @@ -1,7 +1,6 @@ import AtpAgent from '@atproto/api' -import { TestNetwork, SeedClient } from '@atproto/dev-env' +import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' import { forSnapshot, paginateAll } from '../_util' -import basicSeed from '../seeds/basic' import { Notification } from '../../src/lexicon/types/app/bsky/notification/listNotifications' describe('notification views', () => { @@ -241,16 +240,17 @@ describe('notification views', () => { const postRef2 = sc.posts[sc.dids.dan][1].ref // Mention await Promise.all( [postRef1, postRef2].map((postRef) => - agent.api.com.atproto.admin.emitModerationEvent( + agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: postRef.uriStr, cid: postRef.cidStr, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', @@ -277,16 +277,16 @@ describe('notification views', () => { // Cleanup await Promise.all( [postRef1, postRef2].map((postRef) => - agent.api.com.atproto.admin.emitModerationEvent( + agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: postRef.uriStr, cid: postRef.cidStr, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', diff --git a/packages/bsky/tests/views/posts.test.ts b/packages/bsky/tests/views/posts.test.ts index 69bade5b91a..24b84469864 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, SeedClient } from '@atproto/dev-env' +import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' import { forSnapshot, stripViewerFromPost } from '../_util' -import basicSeed from '../seeds/basic' describe('pds posts views', () => { let network: TestNetwork diff --git a/packages/bsky/tests/views/profile.test.ts b/packages/bsky/tests/views/profile.test.ts index fe3f689894b..ddd484b3b31 100644 --- a/packages/bsky/tests/views/profile.test.ts +++ b/packages/bsky/tests/views/profile.test.ts @@ -1,9 +1,8 @@ import fs from 'fs/promises' import AtpAgent from '@atproto/api' -import { TestNetwork, SeedClient } from '@atproto/dev-env' +import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' import { forSnapshot, stripViewer } from '../_util' import { ids } from '../../src/lexicon/lexicons' -import basicSeed from '../seeds/basic' describe('pds profile views', () => { let network: TestNetwork @@ -106,10 +105,10 @@ describe('pds profile views', () => { it('presents avatars & banners', async () => { const avatarImg = await fs.readFile( - 'tests/sample-img/key-portrait-small.jpg', + '../dev-env/src/seed/img/key-portrait-small.jpg', ) const bannerImg = await fs.readFile( - 'tests/sample-img/key-landscape-small.jpg', + '../dev-env/src/seed/img/key-landscape-small.jpg', ) const avatarRes = await pdsAgent.api.com.atproto.repo.uploadBlob( avatarImg, @@ -184,15 +183,16 @@ describe('pds profile views', () => { }) it('blocked by actor takedown', async () => { - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: alice, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', @@ -207,15 +207,15 @@ describe('pds profile views', () => { await expect(promise).rejects.toThrow('Account has been taken down') // Cleanup - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: alice, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', @@ -225,7 +225,7 @@ describe('pds profile views', () => { }) it('blocked by actor suspension', async () => { - await agent.api.com.atproto.admin.emitModerationEvent( + await pdsAgent.api.com.atproto.admin.emitModerationEvent( { event: { $type: 'com.atproto.admin.defs#modEventTakedown', @@ -243,6 +243,7 @@ describe('pds profile views', () => { headers: network.pds.adminAuthHeaders(), }, ) + await network.processAll() const promise = agent.api.app.bsky.actor.getProfile( { actor: alice }, { headers: await network.serviceHeaders(bob) }, @@ -253,7 +254,7 @@ describe('pds profile views', () => { ) // Cleanup - await agent.api.com.atproto.admin.emitModerationEvent( + await pdsAgent.api.com.atproto.admin.emitModerationEvent( { event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { @@ -268,6 +269,7 @@ describe('pds profile views', () => { headers: network.pds.adminAuthHeaders(), }, ) + await network.processAll() }) async function updateProfile(did: string, record: Record) { diff --git a/packages/bsky/tests/views/reposts.test.ts b/packages/bsky/tests/views/reposts.test.ts index 4d386121137..e00650fc6c4 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, SeedClient } from '@atproto/dev-env' +import { TestNetwork, SeedClient, repostsSeed } from '@atproto/dev-env' import { forSnapshot, paginateAll, stripViewer } from '../_util' -import repostsSeed from '../seeds/reposts' describe('pds repost views', () => { let network: TestNetwork diff --git a/packages/bsky/tests/views/suggested-follows.test.ts b/packages/bsky/tests/views/suggested-follows.test.ts index e9aa3248df7..ff9bec4539c 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, SeedClient } from '@atproto/dev-env' -import likesSeed from '../seeds/likes' +import { TestNetwork, SeedClient, likesSeed } from '@atproto/dev-env' describe('suggested follows', () => { let network: TestNetwork diff --git a/packages/bsky/tests/views/suggestions.test.ts b/packages/bsky/tests/views/suggestions.test.ts index 4253f528b13..f79575c529e 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, SeedClient } from '@atproto/dev-env' +import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' import { stripViewer } from '../_util' -import basicSeed from '../seeds/basic' describe('pds user search views', () => { let network: TestNetwork diff --git a/packages/bsky/tests/views/thread.test.ts b/packages/bsky/tests/views/thread.test.ts index d42378aec6e..88f7db4c573 100644 --- a/packages/bsky/tests/views/thread.test.ts +++ b/packages/bsky/tests/views/thread.test.ts @@ -1,7 +1,6 @@ import AtpAgent, { AppBskyFeedGetPostThread } from '@atproto/api' -import { TestNetwork, SeedClient } from '@atproto/dev-env' +import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' import { forSnapshot, stripViewerFromThread } from '../_util' -import basicSeed from '../seeds/basic' import assert from 'assert' import { isThreadViewPost } from '@atproto/api/src/client/types/app/bsky/feed/defs' @@ -31,7 +30,6 @@ describe('pds thread views', () => { // Add a repost of a reply so that we can confirm myState in the thread await sc.repost(bob, sc.replies[alice][0].ref) await network.processAll() - await network.bsky.processAll() }) afterAll(async () => { @@ -165,19 +163,20 @@ describe('pds thread views', () => { describe('takedown', () => { it('blocks post by actor', async () => { - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: alice, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) @@ -192,37 +191,38 @@ describe('pds thread views', () => { ) // Cleanup - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: alice, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) }) it('blocks replies by actor', async () => { - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: carol, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) @@ -235,37 +235,38 @@ describe('pds thread views', () => { expect(forSnapshot(thread.data.thread)).toMatchSnapshot() // Cleanup - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: carol, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) }) it('blocks ancestors by actor', async () => { - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: bob, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) @@ -278,39 +279,40 @@ describe('pds thread views', () => { expect(forSnapshot(thread.data.thread)).toMatchSnapshot() // Cleanup - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did: bob, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) }) it('blocks post by record', async () => { const postRef = sc.posts[alice][1].ref - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: postRef.uriStr, cid: postRef.cidStr, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) @@ -324,20 +326,20 @@ describe('pds thread views', () => { ) // Cleanup - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: postRef.uriStr, cid: postRef.cidStr, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) }) @@ -350,20 +352,21 @@ describe('pds thread views', () => { const parent = threadPreTakedown.data.thread.parent?.['post'] - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: parent.uri, cid: parent.cid, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) @@ -376,20 +379,20 @@ describe('pds thread views', () => { expect(forSnapshot(thread.data.thread)).toMatchSnapshot() // Cleanup - await agent.api.com.atproto.admin.emitModerationEvent( + await agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: parent.uri, cid: parent.cid, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) }) @@ -404,20 +407,21 @@ describe('pds thread views', () => { await Promise.all( [post1, post2].map((post) => - agent.api.com.atproto.admin.emitModerationEvent( + agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: post.uri, cid: post.cid, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ), ), @@ -434,7 +438,7 @@ describe('pds thread views', () => { // Cleanup await Promise.all( [post1, post2].map((post) => - agent.api.com.atproto.admin.emitModerationEvent( + agent.api.com.atproto.admin.updateSubjectStatus( { event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown', @@ -444,12 +448,13 @@ describe('pds thread views', () => { uri: post.uri, cid: post.cid, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ), ), diff --git a/packages/bsky/tests/views/threadgating.test.ts b/packages/bsky/tests/views/threadgating.test.ts index c0667bcf874..feb10dfbadd 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, SeedClient } from '@atproto/dev-env' +import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' import { isNotFoundPost, isThreadViewPost, } from '../../src/lexicon/types/app/bsky/feed/defs' -import basicSeed from '../seeds/basic' import { forSnapshot } from '../_util' describe('views with thread gating', () => { diff --git a/packages/bsky/tests/views/timeline.test.ts b/packages/bsky/tests/views/timeline.test.ts index f9fd5bb4a6e..014bb5339ce 100644 --- a/packages/bsky/tests/views/timeline.test.ts +++ b/packages/bsky/tests/views/timeline.test.ts @@ -1,8 +1,7 @@ import assert from 'assert' import AtpAgent from '@atproto/api' -import { TestNetwork, SeedClient } from '@atproto/dev-env' +import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' import { forSnapshot, getOriginator, paginateAll } from '../_util' -import basicSeed from '../seeds/basic' import { FeedAlgorithm } from '../../src/api/app/bsky/util/feed' import { FeedViewPost } from '../../src/lexicon/types/app/bsky/feed/defs' @@ -35,7 +34,7 @@ describe('timeline views', () => { await network.bsky.ctx.services .label(network.bsky.ctx.db.getPrimary()) .formatAndCreate( - network.bsky.ctx.cfg.labelerDid, + network.ozone.ctx.cfg.service.did, labelPostA.uriStr, labelPostA.cidStr, { create: ['kind'] }, @@ -43,7 +42,7 @@ describe('timeline views', () => { await network.bsky.ctx.services .label(network.bsky.ctx.db.getPrimary()) .formatAndCreate( - network.bsky.ctx.cfg.labelerDid, + network.ozone.ctx.cfg.service.did, labelPostB.uriStr, labelPostB.cidStr, { create: ['kind'] }, @@ -197,15 +196,16 @@ describe('timeline views', () => { it('blocks posts, reposts, replies by actor takedown', async () => { await Promise.all( [bob, carol].map((did) => - agent.api.com.atproto.admin.emitModerationEvent( + agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', @@ -225,15 +225,15 @@ describe('timeline views', () => { // Cleanup await Promise.all( [bob, carol].map((did) => - agent.api.com.atproto.admin.emitModerationEvent( + agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.admin.defs#repoRef', did, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', @@ -249,16 +249,17 @@ describe('timeline views', () => { const postRef2 = sc.replies[bob][0].ref // Post and reply parent await Promise.all( [postRef1, postRef2].map((postRef) => - agent.api.com.atproto.admin.emitModerationEvent( + agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: postRef.uriStr, cid: postRef.cidStr, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: true, + ref: 'test', + }, }, { encoding: 'application/json', @@ -278,16 +279,16 @@ describe('timeline views', () => { // Cleanup await Promise.all( [postRef1, postRef2].map((postRef) => - agent.api.com.atproto.admin.emitModerationEvent( + agent.api.com.atproto.admin.updateSubjectStatus( { - event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, subject: { $type: 'com.atproto.repo.strongRef', uri: postRef.uriStr, cid: postRef.cidStr, }, - createdBy: 'did:example:admin', - reason: 'Y', + takedown: { + applied: false, + }, }, { encoding: 'application/json', diff --git a/packages/common-web/src/times.ts b/packages/common-web/src/times.ts index 90366277fdf..7231904d624 100644 --- a/packages/common-web/src/times.ts +++ b/packages/common-web/src/times.ts @@ -6,3 +6,10 @@ export const DAY = HOUR * 24 export const lessThanAgoMs = (time: Date, range: number) => { return Date.now() < time.getTime() + range } + +export const addHoursToDate = (hours: number, startingDate?: Date): Date => { + // When date is passed, clone before calling `setHours()` so that we are not mutating the original date + const currentDate = startingDate ? new Date(startingDate) : new Date() + currentDate.setHours(currentDate.getHours() + hours) + return currentDate +} diff --git a/packages/pds/src/config/util.ts b/packages/common/src/env.ts similarity index 92% rename from packages/pds/src/config/util.ts rename to packages/common/src/env.ts index 2bf858621bf..a4fcc2e036f 100644 --- a/packages/pds/src/config/util.ts +++ b/packages/common/src/env.ts @@ -1,4 +1,4 @@ -import { parseIntWithFallback } from '@atproto/common' +import { parseIntWithFallback } from '@atproto/common-web' export const envInt = (name: string): number | undefined => { const str = process.env[name] diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 524a090c5ab..fc981b11c02 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1,5 +1,6 @@ export * from '@atproto/common-web' export * from './dates' +export * from './env' export * from './fs' export * from './ipld' export * from './ipld-multi' diff --git a/packages/dev-env/package.json b/packages/dev-env/package.json index 3f9a5032e12..a71b9ce3169 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -31,6 +31,7 @@ "@atproto/crypto": "workspace:^", "@atproto/identity": "workspace:^", "@atproto/lexicon": "workspace:^", + "@atproto/ozone": "workspace:^", "@atproto/pds": "workspace:^", "@atproto/syntax": "workspace:^", "@atproto/xrpc-server": "workspace:^", diff --git a/packages/dev-env/src/bsky.ts b/packages/dev-env/src/bsky.ts index 529b212126a..8eb40ed0b36 100644 --- a/packages/dev-env/src/bsky.ts +++ b/packages/dev-env/src/bsky.ts @@ -44,12 +44,12 @@ export class TestBsky { didCacheMaxTTL: DAY, labelCacheStaleTTL: 30 * SECOND, labelCacheMaxTTL: MINUTE, + modServiceDid: cfg.modServiceDid ?? 'did:example:invalidMod', ...cfg, // Each test suite gets its own lock id for the repo subscription adminPassword: ADMIN_PASSWORD, moderatorPassword: MOD_PASSWORD, triagePassword: TRIAGE_PASSWORD, - labelerDid: 'did:example:labeler', feedGenDid: 'did:example:feedGen', rateLimitsEnabled: false, }) @@ -98,9 +98,9 @@ export class TestBsky { // indexer const indexerCfg = new bsky.IndexerConfig({ version: '0.0.0', + serverDid, didCacheStaleTTL: HOUR, didCacheMaxTTL: DAY, - labelerDid: 'did:example:labeler', redisHost: cfg.redisHost, dbPostgresUrl: cfg.dbPrimaryPostgresUrl, dbPostgresSchema: cfg.dbPostgresSchema, @@ -109,7 +109,8 @@ export class TestBsky { abyssEndpoint: '', abyssPassword: '', imgUriEndpoint: 'img.example.com', - moderationPushUrl: `http://admin:${config.adminPassword}@localhost:${cfg.pdsPort}`, + moderationPushUrl: + cfg.indexer?.moderationPushUrl ?? 'https://modservice.invalid', indexerPartitionIds: [0], indexerNamespace: `ns${ns}`, indexerSubLockId: uniqueLockId(), @@ -129,7 +130,6 @@ export class TestBsky { db: db.getPrimary(), redis: indexerRedis, redisCache, - imgInvalidator: cfg.imgInvalidator, }) // ingester const ingesterCfg = new bsky.IngesterConfig({ @@ -138,6 +138,7 @@ export class TestBsky { dbPostgresUrl: cfg.dbPrimaryPostgresUrl, dbPostgresSchema: cfg.dbPostgresSchema, repoProvider: cfg.repoProvider, + labelProvider: cfg.labelProvider, ingesterNamespace: `ns${ns}`, ingesterSubLockId: uniqueLockId(), ingesterPartitionCount: 1, @@ -157,6 +158,9 @@ export class TestBsky { await indexer.start() await server.start() + // manually process labels in dev-env (in network.processAll) + ingester.ctx.labelSubscription?.destroy() + return new TestBsky(url, port, server, indexer, ingester) } @@ -219,6 +223,7 @@ export async function getIngester( dbPostgresUrl: process.env.DB_POSTGRES_URL || '', dbPostgresSchema: `appview_${name}`, repoProvider: network.pds.url.replace('http://', 'ws://'), + labelProvider: 'http://labeler.invalid', ingesterSubLockId: uniqueLockId(), ingesterPartitionCount: config.ingesterPartitionCount ?? 1, ingesterNamespace: `ns${ns}`, @@ -234,7 +239,9 @@ export async function getIngester( namespace: cfg.ingesterNamespace, }) await db.migrateToLatestOrThrow() - return bsky.BskyIngester.create({ cfg, db, redis }) + const ingester = await bsky.BskyIngester.create({ cfg, db, redis }) + await ingester.ctx.labelSubscription?.destroy() + return ingester } // get multiple indexers for separate partitions, sharing db and redis instance. @@ -249,9 +256,9 @@ export async function getIndexers( const ns = name ? await randomIntFromSeed(name, 1000000) : undefined const baseCfg: bsky.IndexerConfigValues = { version: '0.0.0', + serverDid: 'did:example:bsky', didCacheStaleTTL: HOUR, didCacheMaxTTL: DAY, - labelerDid: 'did:example:labeler', labelerKeywords: { label_me: 'test-label', label_me_2: 'test-label-2' }, redisHost: process.env.REDIS_HOST || '', dbPostgresUrl: process.env.DB_POSTGRES_URL || '', @@ -263,6 +270,7 @@ export async function getIndexers( indexerPartitionIds: [0], indexerNamespace: `ns${ns}`, ingesterPartitionCount: config.ingesterPartitionCount ?? 1, + moderationPushUrl: config.moderationPushUrl ?? 'https://modservice.invalid', ...config, } const db = new bsky.PrimaryDatabase({ diff --git a/packages/dev-env/src/index.ts b/packages/dev-env/src/index.ts index 160247f9fbb..5e8524edf68 100644 --- a/packages/dev-env/src/index.ts +++ b/packages/dev-env/src/index.ts @@ -4,6 +4,6 @@ export * from './network-no-appview' export * from './pds' export * from './plc' export * from './feed-gen' -export * from './seed-client' +export * from './seed' export * from './types' export * from './util' diff --git a/packages/dev-env/src/mock/index.ts b/packages/dev-env/src/mock/index.ts index 10f76b1c259..ab818dfc24c 100644 --- a/packages/dev-env/src/mock/index.ts +++ b/packages/dev-env/src/mock/index.ts @@ -191,7 +191,7 @@ export async function generateMockSetup(env: TestNetwork) { const labelSrvc = ctx.services.label(ctx.db.getPrimary()) await labelSrvc.createLabels([ { - src: ctx.cfg.labelerDid, + src: env.ozone.ctx.cfg.service.did, uri: labeledPost.uri, cid: labeledPost.cid, val: 'nudity', @@ -199,7 +199,7 @@ export async function generateMockSetup(env: TestNetwork) { cts: new Date().toISOString(), }, { - src: ctx.cfg.labelerDid, + src: env.ozone.ctx.cfg.service.did, uri: filteredPost.uri, cid: filteredPost.cid, val: 'dmca-violation', diff --git a/packages/dev-env/src/network-no-appview.ts b/packages/dev-env/src/network-no-appview.ts index 30b978b5b79..44701ece35e 100644 --- a/packages/dev-env/src/network-no-appview.ts +++ b/packages/dev-env/src/network-no-appview.ts @@ -4,7 +4,7 @@ import { TestPlc } from './plc' import { TestPds } from './pds' import { mockNetworkUtilities } from './util' import { TestFeedGen } from './feed-gen' -import { SeedClient } from './seed-client' +import { SeedClient } from './seed/client' export class TestNetworkNoAppView { feedGens: TestFeedGen[] = [] diff --git a/packages/dev-env/src/network.ts b/packages/dev-env/src/network.ts index 6079ec4c968..4e29c3c9384 100644 --- a/packages/dev-env/src/network.ts +++ b/packages/dev-env/src/network.ts @@ -3,18 +3,26 @@ import * as uint8arrays from 'uint8arrays' import getPort from 'get-port' import { wait } from '@atproto/common-web' import { createServiceJwt } from '@atproto/xrpc-server' +import { Client as PlcClient } from '@did-plc/lib' import { TestServerParams } from './types' import { TestPlc } from './plc' import { TestPds } from './pds' import { TestBsky } from './bsky' +import { TestOzone } from './ozone' import { mockNetworkUtilities } from './util' import { TestNetworkNoAppView } from './network-no-appview' +import { Secp256k1Keypair } from '@atproto/crypto' const ADMIN_USERNAME = 'admin' const ADMIN_PASSWORD = 'admin-pass' export class TestNetwork extends TestNetworkNoAppView { - constructor(public plc: TestPlc, public pds: TestPds, public bsky: TestBsky) { + constructor( + public plc: TestPlc, + public pds: TestPds, + public bsky: TestBsky, + public ozone: TestOzone, + ) { super(plc, pds) } @@ -32,29 +40,61 @@ export class TestNetwork extends TestNetworkNoAppView { const bskyPort = params.bsky?.port ?? (await getPort()) const pdsPort = params.pds?.port ?? (await getPort()) + const ozonePort = params.ozone?.port ?? (await getPort()) + + const ozoneKey = await Secp256k1Keypair.create({ exportable: true }) + const ozoneDid = await new PlcClient(plc.url).createDid({ + signingKey: ozoneKey.did(), + rotationKeys: [ozoneKey.did()], + handle: 'ozone.test', + pds: `http://pds.invalid`, + signer: ozoneKey, + }) + const bsky = await TestBsky.create({ port: bskyPort, plcUrl: plc.url, pdsPort, repoProvider: `ws://localhost:${pdsPort}`, + labelProvider: `http://localhost:${ozonePort}`, dbPostgresSchema: `appview_${dbPostgresSchema}`, dbPrimaryPostgresUrl: dbPostgresUrl, redisHost, - moderationPushUrl: `http://admin:${ADMIN_PASSWORD}@localhost:${pdsPort}`, + modServiceDid: ozoneDid, ...params.bsky, + indexer: { + ...params.bsky?.indexer, + moderationPushUrl: `http://admin:${ADMIN_PASSWORD}@localhost:${ozonePort}`, + }, }) + const pds = await TestPds.create({ port: pdsPort, didPlcUrl: plc.url, bskyAppViewUrl: bsky.url, bskyAppViewDid: bsky.ctx.cfg.serverDid, - bskyAppViewModeration: true, + modServiceUrl: `http://localhost:${ozonePort}`, + modServiceDid: ozoneDid, ...params.pds, }) + const ozone = await TestOzone.create({ + port: ozonePort, + plcUrl: plc.url, + signingKey: ozoneKey, + serverDid: ozoneDid, + dbPostgresSchema: `ozone_${dbPostgresSchema}`, + dbPostgresUrl, + appviewUrl: bsky.url, + appviewDid: bsky.ctx.cfg.serverDid, + pdsUrl: pds.url, + pdsDid: pds.ctx.cfg.service.did, + ...params.ozone, + }) + mockNetworkUtilities(pds, bsky) - return new TestNetwork(plc, pds, bsky) + return new TestNetwork(plc, pds, bsky, ozone) } async processFullSubscription(timeout = 5000) { @@ -78,6 +118,8 @@ export class TestNetwork extends TestNetworkNoAppView { await this.pds.processAll() await this.processFullSubscription(timeout) await this.bsky.processAll() + await this.ozone.processAll() + await this.bsky.ingester.ctx.labelSubscription?.fetchLabels() } async serviceHeaders(did: string, aud?: string) { @@ -109,6 +151,7 @@ export class TestNetwork extends TestNetworkNoAppView { async close() { await Promise.all(this.feedGens.map((fg) => fg.close())) + await this.ozone.close() await this.bsky.close() await this.pds.close() await this.plc.close() diff --git a/packages/dev-env/src/ozone.ts b/packages/dev-env/src/ozone.ts new file mode 100644 index 00000000000..4988a888f61 --- /dev/null +++ b/packages/dev-env/src/ozone.ts @@ -0,0 +1,112 @@ +import getPort from 'get-port' +import * as ui8 from 'uint8arrays' +import * as ozone from '@atproto/ozone' +import { AtpAgent } from '@atproto/api' +import { Secp256k1Keypair } from '@atproto/crypto' +import { Client as PlcClient } from '@did-plc/lib' +import { OzoneConfig } from './types' +import { ADMIN_PASSWORD, MOD_PASSWORD, TRIAGE_PASSWORD } from './const' + +export class TestOzone { + constructor( + public url: string, + public port: number, + public server: ozone.OzoneService, + public daemon: ozone.OzoneDaemon, + ) {} + + static async create(config: OzoneConfig): Promise { + const serviceKeypair = + config.signingKey ?? (await Secp256k1Keypair.create({ exportable: true })) + const signingKeyHex = ui8.toString(await serviceKeypair.export(), 'hex') + let serverDid = config.serverDid + if (!serverDid) { + const plcClient = new PlcClient(config.plcUrl) + serverDid = await plcClient.createDid({ + signingKey: serviceKeypair.did(), + rotationKeys: [serviceKeypair.did()], + handle: 'ozone.test', + pds: `https://pds.invalid`, + signer: serviceKeypair, + }) + } + + const port = config.port || (await getPort()) + const url = `http://localhost:${port}` + const env: ozone.OzoneEnvironment = { + version: '0.0.0', + port, + didPlcUrl: config.plcUrl, + publicUrl: 'https://ozone.public.url', + serverDid, + signingKeyHex, + ...config, + adminPassword: ADMIN_PASSWORD, + moderatorPassword: MOD_PASSWORD, + triagePassword: TRIAGE_PASSWORD, + } + + // Separate migration db in case migration changes some connection state that we need in the tests, e.g. "alter database ... set ..." + const migrationDb = new ozone.Database({ + schema: config.dbPostgresSchema, + url: config.dbPostgresUrl, + }) + if (config.migration) { + await migrationDb.migrateToOrThrow(config.migration) + } else { + await migrationDb.migrateToLatestOrThrow() + } + await migrationDb.close() + + const cfg = ozone.envToCfg(env) + const secrets = ozone.envToSecrets(env) + + // api server + const server = await ozone.OzoneService.create(cfg, secrets) + await server.start() + + const daemon = await ozone.OzoneDaemon.create(cfg, secrets) + await daemon.start() + // don't do event reversal in dev-env + await daemon.ctx.eventReverser.destroy() + + return new TestOzone(url, port, server, daemon) + } + + get ctx(): ozone.AppContext { + return this.server.ctx + } + + getClient() { + return new AtpAgent({ service: this.url }) + } + + adminAuth(role: 'admin' | 'moderator' | 'triage' = 'admin'): string { + const password = + role === 'triage' + ? TRIAGE_PASSWORD + : role === 'moderator' + ? MOD_PASSWORD + : ADMIN_PASSWORD + return ( + 'Basic ' + + ui8.toString(ui8.fromString(`admin:${password}`, 'utf8'), 'base64pad') + ) + } + + adminAuthHeaders(role?: 'admin' | 'moderator' | 'triage') { + return { + authorization: this.adminAuth(role), + } + } + + async processAll() { + await this.ctx.backgroundQueue.processAll() + await this.daemon.processAll() + } + + async close() { + await this.daemon.destroy() + await this.server.destroy() + } +} diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index c52ada4a9e9..44b8a063fce 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -47,6 +47,8 @@ export class TestPds { bskyAppViewUrl: 'https://appview.invalid', bskyAppViewDid: 'did:example:invalid', bskyAppViewCdnUrlPattern: 'http://cdn.appview.com/%s/%s/%s', + modServiceUrl: 'https://moderator.invalid', + modServiceDid: 'did:example:invalid', plcRotationKeyK256PrivateKeyHex: plcRotationPriv, inviteRequired: false, ...config, diff --git a/packages/bsky/tests/seeds/author-feed.ts b/packages/dev-env/src/seed/author-feed.ts similarity index 97% rename from packages/bsky/tests/seeds/author-feed.ts rename to packages/dev-env/src/seed/author-feed.ts index 164564f766b..841c58b406c 100644 --- a/packages/bsky/tests/seeds/author-feed.ts +++ b/packages/dev-env/src/seed/author-feed.ts @@ -1,4 +1,4 @@ -import { SeedClient } from '@atproto/dev-env' +import { SeedClient } from './client' import basicSeed from './basic' export default async (sc: SeedClient) => { diff --git a/packages/bsky/tests/seeds/basic.ts b/packages/dev-env/src/seed/basic.ts similarity index 92% rename from packages/bsky/tests/seeds/basic.ts rename to packages/dev-env/src/seed/basic.ts index b935afd3d6f..47c299dce45 100644 --- a/packages/bsky/tests/seeds/basic.ts +++ b/packages/dev-env/src/seed/basic.ts @@ -1,5 +1,4 @@ -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 +33,12 @@ export default async (sc: SeedClient, users = true) => { }) const img1 = await sc.uploadFile( carol, - 'tests/sample-img/key-landscape-small.jpg', + '../dev-env/src/seed/img/key-landscape-small.jpg', 'image/jpeg', ) const img2 = await sc.uploadFile( carol, - 'tests/sample-img/key-alt.jpg', + '../dev-env/src/seed/img/key-alt.jpg', 'image/jpeg', ) await sc.post( @@ -58,7 +57,7 @@ export default async (sc: SeedClient, users = true) => { index: { byteStart: 0, byteEnd: 18 }, features: [ { - $type: `${ids.AppBskyRichtextFacet}#mention`, + $type: `app.bsky.richtext.facet#mention`, did: alice, }, ], @@ -100,7 +99,7 @@ export default async (sc: SeedClient, users = true) => { const replyImg = await sc.uploadFile( bob, - 'tests/sample-img/key-landscape-small.jpg', + '../dev-env/src/seed/img/key-landscape-small.jpg', 'image/jpeg', ) // must ensure ordering of replies in indexing diff --git a/packages/dev-env/src/seed-client.ts b/packages/dev-env/src/seed/client.ts similarity index 98% rename from packages/dev-env/src/seed-client.ts rename to packages/dev-env/src/seed/client.ts index 7fc57d52081..5b7a614228f 100644 --- a/packages/dev-env/src/seed-client.ts +++ b/packages/dev-env/src/seed/client.ts @@ -9,7 +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 { TestNetworkNoAppView } from './network-no-appview' +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. @@ -128,7 +128,9 @@ export class SeedClient { description: string, selfLabels?: string[], ) { - AVATAR_IMG ??= await fs.readFile('tests/sample-img/key-portrait-small.jpg') + AVATAR_IMG ??= await fs.readFile( + '../dev-env/src/seed/img/key-portrait-small.jpg', + ) let avatarBlob { diff --git a/packages/bsky/tests/seeds/follows.ts b/packages/dev-env/src/seed/follows.ts similarity index 96% rename from packages/bsky/tests/seeds/follows.ts rename to packages/dev-env/src/seed/follows.ts index 1abe555ff00..f15156dbff5 100644 --- a/packages/bsky/tests/seeds/follows.ts +++ b/packages/dev-env/src/seed/follows.ts @@ -1,4 +1,4 @@ -import { SeedClient } from '@atproto/dev-env' +import { SeedClient } from './client' export default async (sc: SeedClient) => { await sc.createAccount('alice', users.alice) diff --git a/packages/bsky/tests/sample-img/at.png b/packages/dev-env/src/seed/img/at.png similarity index 100% rename from packages/bsky/tests/sample-img/at.png rename to packages/dev-env/src/seed/img/at.png diff --git a/packages/bsky/tests/sample-img/hd-key.jpg b/packages/dev-env/src/seed/img/hd-key.jpg similarity index 100% rename from packages/bsky/tests/sample-img/hd-key.jpg rename to packages/dev-env/src/seed/img/hd-key.jpg diff --git a/packages/bsky/tests/sample-img/key-alt.jpg b/packages/dev-env/src/seed/img/key-alt.jpg similarity index 100% rename from packages/bsky/tests/sample-img/key-alt.jpg rename to packages/dev-env/src/seed/img/key-alt.jpg diff --git a/packages/bsky/tests/sample-img/key-landscape-large.jpg b/packages/dev-env/src/seed/img/key-landscape-large.jpg similarity index 100% rename from packages/bsky/tests/sample-img/key-landscape-large.jpg rename to packages/dev-env/src/seed/img/key-landscape-large.jpg diff --git a/packages/bsky/tests/sample-img/key-landscape-small.jpg b/packages/dev-env/src/seed/img/key-landscape-small.jpg similarity index 100% rename from packages/bsky/tests/sample-img/key-landscape-small.jpg rename to packages/dev-env/src/seed/img/key-landscape-small.jpg diff --git a/packages/bsky/tests/sample-img/key-portrait-large.jpg b/packages/dev-env/src/seed/img/key-portrait-large.jpg similarity index 100% rename from packages/bsky/tests/sample-img/key-portrait-large.jpg rename to packages/dev-env/src/seed/img/key-portrait-large.jpg diff --git a/packages/bsky/tests/sample-img/key-portrait-small.jpg b/packages/dev-env/src/seed/img/key-portrait-small.jpg similarity index 100% rename from packages/bsky/tests/sample-img/key-portrait-small.jpg rename to packages/dev-env/src/seed/img/key-portrait-small.jpg diff --git a/packages/dev-env/src/seed/index.ts b/packages/dev-env/src/seed/index.ts new file mode 100644 index 00000000000..dcf8996c5fe --- /dev/null +++ b/packages/dev-env/src/seed/index.ts @@ -0,0 +1,9 @@ +export * from './client' + +export { default as authorFeedSeed } from './author-feed' +export { default as basicSeed } from './basic' +export { default as followsSeed } from './follows' +export { default as likesSeed } from './likes' +export { default as repostsSeed } from './reposts' +export { default as usersBulkSeed } from './users-bulk' +export { default as usersSeed } from './users' diff --git a/packages/bsky/tests/seeds/likes.ts b/packages/dev-env/src/seed/likes.ts similarity index 96% rename from packages/bsky/tests/seeds/likes.ts rename to packages/dev-env/src/seed/likes.ts index 9c68375c52f..195da59e886 100644 --- a/packages/bsky/tests/seeds/likes.ts +++ b/packages/dev-env/src/seed/likes.ts @@ -1,4 +1,4 @@ -import { SeedClient } from '@atproto/dev-env' +import { SeedClient } from './client' import basicSeed from './basic' export default async (sc: SeedClient) => { diff --git a/packages/bsky/tests/seeds/reposts.ts b/packages/dev-env/src/seed/reposts.ts similarity index 92% rename from packages/bsky/tests/seeds/reposts.ts rename to packages/dev-env/src/seed/reposts.ts index 9bb444ec8f2..a7c724d1adf 100644 --- a/packages/bsky/tests/seeds/reposts.ts +++ b/packages/dev-env/src/seed/reposts.ts @@ -1,4 +1,4 @@ -import { SeedClient } from '@atproto/dev-env' +import { SeedClient } from './client' import basicSeed from './basic' export default async (sc: SeedClient) => { diff --git a/packages/bsky/tests/seeds/users-bulk.ts b/packages/dev-env/src/seed/users-bulk.ts similarity index 99% rename from packages/bsky/tests/seeds/users-bulk.ts rename to packages/dev-env/src/seed/users-bulk.ts index c20ce85de51..5a6cc42981b 100644 --- a/packages/bsky/tests/seeds/users-bulk.ts +++ b/packages/dev-env/src/seed/users-bulk.ts @@ -1,5 +1,5 @@ -import { SeedClient } from '@atproto/dev-env' -import { chunkArray } from '@atproto/common' +import { chunkArray } from '@atproto/common-web' +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/dev-env/src/seed/users.ts similarity index 96% rename from packages/bsky/tests/seeds/users.ts rename to packages/dev-env/src/seed/users.ts index 2ed5762065a..8c14b894db4 100644 --- a/packages/bsky/tests/seeds/users.ts +++ b/packages/dev-env/src/seed/users.ts @@ -1,4 +1,4 @@ -import { SeedClient } from '@atproto/dev-env' +import { SeedClient } from './client' export default async (sc: SeedClient) => { await sc.createAccount('alice', users.alice) diff --git a/packages/dev-env/src/types.ts b/packages/dev-env/src/types.ts index 3bbcaf15257..51d22976da2 100644 --- a/packages/dev-env/src/types.ts +++ b/packages/dev-env/src/types.ts @@ -1,6 +1,8 @@ import * as pds from '@atproto/pds' import * as bsky from '@atproto/bsky' -import { ImageInvalidator } from '@atproto/bsky/src/image/invalidator' +import * as ozone from '@atproto/ozone' +import { ImageInvalidator } from '@atproto/bsky' +import { ExportableKeypair } from '@atproto/crypto' export type PlcConfig = { port?: number @@ -15,6 +17,7 @@ export type PdsConfig = Partial & { export type BskyConfig = Partial & { plcUrl: string repoProvider: string + labelProvider: string dbPrimaryPostgresUrl: string redisHost: string pdsPort: number @@ -25,10 +28,19 @@ export type BskyConfig = Partial & { ingester?: Partial } +export type OzoneConfig = Partial & { + plcUrl: string + appviewUrl: string + dbPostgresUrl: string + migration?: string + signingKey?: ExportableKeypair +} + export type TestServerParams = { dbPostgresUrl: string dbPostgresSchema: string pds: Partial plc: Partial bsky: Partial + ozone: Partial } diff --git a/packages/ozone/README.md b/packages/ozone/README.md new file mode 100644 index 00000000000..09cb1c6044f --- /dev/null +++ b/packages/ozone/README.md @@ -0,0 +1,15 @@ +# @atproto/ozone: Bluesky Moderation Service + +Backend service for moderating the Bluesky network. + +[![NPM](https://img.shields.io/npm/v/@atproto/ozone)](https://www.npmjs.com/package/@atproto/ozone) +[![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 + +This project is dual-licensed under MIT and Apache 2.0 terms: + +- MIT license ([LICENSE-MIT.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-MIT.txt) or http://opensource.org/licenses/MIT) +- Apache License, Version 2.0, ([LICENSE-APACHE.txt](https://github.com/bluesky-social/atproto/blob/main/LICENSE-APACHE.txt) or http://www.apache.org/licenses/LICENSE-2.0) + +Downstream projects and end users may chose either license individually, or both together, at their discretion. The motivation for this dual-licensing is the additional software patent assurance provided by Apache 2.0. diff --git a/packages/ozone/babel.config.js b/packages/ozone/babel.config.js new file mode 100644 index 00000000000..ee58f35df11 --- /dev/null +++ b/packages/ozone/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: [['@babel/preset-env']], +} diff --git a/packages/ozone/bin/migration-create.ts b/packages/ozone/bin/migration-create.ts new file mode 100644 index 00000000000..b51c536c4f2 --- /dev/null +++ b/packages/ozone/bin/migration-create.ts @@ -0,0 +1,38 @@ +#!/usr/bin/env ts-node + +import * as fs from 'fs/promises' +import * as path from 'path' + +export async function main() { + const now = new Date() + const prefix = now.toISOString().replace(/[^a-z0-9]/gi, '') // Order of migrations matches alphabetical order of their names + const name = process.argv[2] + if (!name || !name.match(/^[a-z0-9-]+$/)) { + process.exitCode = 1 + return console.error( + 'Must pass a migration name consisting of lowercase digits, numbers, and dashes.', + ) + } + const filename = `${prefix}-${name}` + const dir = path.join(__dirname, '..', 'src', 'db', 'migrations') + + await fs.writeFile(path.join(dir, `${filename}.ts`), template, { flag: 'wx' }) + await fs.writeFile( + path.join(dir, 'index.ts'), + `export * as _${prefix} from './${filename}'\n`, + { flag: 'a' }, + ) +} + +const template = `import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + // Migration code +} + +export async function down(db: Kysely): Promise { + // Migration code +} +` + +main() diff --git a/packages/ozone/build.js b/packages/ozone/build.js new file mode 100644 index 00000000000..45d59b50f8e --- /dev/null +++ b/packages/ozone/build.js @@ -0,0 +1,18 @@ +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', 'src/db/index.ts'], + bundle: true, + sourcemap: true, + outdir: 'dist', + platform: 'node', + external: [ + // Referenced in pg driver, but optional and we don't use it + 'pg-native', + ], + plugins: buildShallow ? [nodeExternalsPlugin()] : [], +}) diff --git a/packages/ozone/jest.config.js b/packages/ozone/jest.config.js new file mode 100644 index 00000000000..14720ce82eb --- /dev/null +++ b/packages/ozone/jest.config.js @@ -0,0 +1,6 @@ +const base = require('../../jest.config.base.js') + +module.exports = { + ...base, + displayName: 'Bsky App View', +} diff --git a/packages/ozone/package.json b/packages/ozone/package.json new file mode 100644 index 00000000000..3d837db9913 --- /dev/null +++ b/packages/ozone/package.json @@ -0,0 +1,70 @@ +{ + "name": "@atproto/ozone", + "version": "0.0.1", + "license": "MIT", + "description": "Backend service for moderating the Bluesky network.", + "keywords": [ + "atproto", + "bluesky" + ], + "homepage": "https://atproto.com", + "repository": { + "type": "git", + "url": "https://github.com/bluesky-social/atproto", + "directory": "packages/ozone" + }, + "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/index.d.ts" + }, + "bin": "dist/bin.js", + "scripts": { + "codegen": "lex gen-server ./src/lexicon ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", + "build": "node ./build.js", + "postbuild": "tsc --build tsconfig.build.json", + "update-main-to-dist": "node ../../update-main-to-dist.js packages/ozone", + "start": "node --enable-source-maps dist/bin.js", + "test": "../dev-infra/with-test-redis-and-db.sh jest", + "test:log": "tail -50 test.log | pino-pretty", + "test:updateSnapshot": "jest --updateSnapshot", + "migration:create": "ts-node ./bin/migration-create.ts" + }, + "dependencies": { + "@atproto/api": "workspace:^", + "@atproto/common": "workspace:^", + "@atproto/crypto": "workspace:^", + "@atproto/syntax": "workspace:^", + "@atproto/identity": "workspace:^", + "@atproto/lexicon": "workspace:^", + "@atproto/xrpc-server": "workspace:^", + "@did-plc/lib": "^0.0.1", + "compression": "^1.7.4", + "cors": "^2.8.5", + "express": "^4.17.2", + "express-async-errors": "^3.1.1", + "http-terminator": "^3.2.0", + "kysely": "^0.22.0", + "multiformats": "^9.9.0", + "p-queue": "^6.6.2", + "pg": "^8.10.0", + "pino": "^8.15.0", + "pino-http": "^8.2.1", + "typed-emitter": "^2.1.0", + "uint8arrays": "3.0.0" + }, + "devDependencies": { + "@atproto/api": "workspace:^", + "@atproto/dev-env": "workspace:^", + "@atproto/lex-cli": "workspace:^", + "@atproto/pds": "workspace:^", + "@atproto/xrpc": "workspace:^", + "@did-plc/server": "^0.0.1", + "@types/cors": "^2.8.12", + "@types/express": "^4.17.13", + "@types/express-serve-static-core": "^4.17.36", + "@types/pg": "^8.6.6", + "@types/qs": "^6.9.7", + "axios": "^0.27.2" + } +} diff --git a/packages/ozone/src/api/admin/emitModerationEvent.ts b/packages/ozone/src/api/admin/emitModerationEvent.ts new file mode 100644 index 00000000000..7aed324608b --- /dev/null +++ b/packages/ozone/src/api/admin/emitModerationEvent.ts @@ -0,0 +1,130 @@ +import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' +import { Server } from '../../lexicon' +import AppContext from '../../context' +import { + isModEventLabel, + isModEventReverseTakedown, + isModEventTakedown, +} from '../../lexicon/types/com/atproto/admin/defs' +import { subjectFromInput } from '../../mod-service/subject' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.admin.emitModerationEvent({ + auth: ctx.roleVerifier, + handler: async ({ input, auth }) => { + const access = auth.credentials + const db = ctx.db + const moderationService = ctx.modService(db) + const { createdBy, event } = input.body + const isTakedownEvent = isModEventTakedown(event) + const isReverseTakedownEvent = isModEventReverseTakedown(event) + const isLabelEvent = isModEventLabel(event) + const subject = subjectFromInput( + input.body.subject, + input.body.subjectBlobCids, + ) + + // apply access rules + + // if less than moderator access then can not takedown an account + if (!access.moderator && isTakedownEvent && subject.isRepo()) { + throw new AuthRequiredError( + 'Must be a full moderator to perform an account takedown', + ) + } + // if less than moderator access then can only take ack and escalation actions + if (!access.moderator && (isTakedownEvent || isReverseTakedownEvent)) { + throw new AuthRequiredError( + 'Must be a full moderator to take this type of action', + ) + } + // if less than moderator access then can not apply labels + if (!access.moderator && isLabelEvent) { + throw new AuthRequiredError('Must be a full moderator to label content') + } + + if (isLabelEvent) { + validateLabels([ + ...(event.createLabelVals ?? []), + ...(event.negateLabelVals ?? []), + ]) + } + + if (isTakedownEvent || isReverseTakedownEvent) { + const isSubjectTakendown = await moderationService.isSubjectTakendown( + subject, + ) + + if (isSubjectTakendown && isTakedownEvent) { + throw new InvalidRequestError(`Subject is already taken down`) + } + + if (!isSubjectTakendown && isReverseTakedownEvent) { + throw new InvalidRequestError(`Subject is not taken down`) + } + } + + const moderationEvent = await db.transaction(async (dbTxn) => { + const moderationTxn = ctx.modService(dbTxn) + + const result = await moderationTxn.logEvent({ + event, + subject, + createdBy, + }) + + if (subject.isRepo()) { + if (isTakedownEvent) { + const isSuspend = !!result.durationInHours + await moderationTxn.takedownRepo(subject, result.id, isSuspend) + } else if (isReverseTakedownEvent) { + await moderationTxn.reverseTakedownRepo(subject) + } + } + + if (subject.isRecord()) { + if (isTakedownEvent) { + await moderationTxn.takedownRecord(subject, result.id) + } else if (isReverseTakedownEvent) { + await moderationTxn.reverseTakedownRecord(subject) + } + } + + if (isLabelEvent) { + await moderationTxn.formatAndCreateLabels( + ctx.cfg.service.did, + result.subjectUri ?? result.subjectDid, + result.subjectCid, + { + create: result.createLabelVals?.length + ? result.createLabelVals.split(' ') + : undefined, + negate: result.negateLabelVals?.length + ? result.negateLabelVals.split(' ') + : undefined, + }, + ) + } + + return result + }) + + return { + encoding: 'application/json', + body: moderationService.views.formatEvent(moderationEvent), + } + }, + }) +} + +const validateLabels = (labels: string[]) => { + for (const label of labels) { + for (const char of badChars) { + if (label.includes(char)) { + throw new InvalidRequestError(`Invalid label: ${label}`) + } + } + } +} + +const badChars = [' ', ',', ';', `'`, `"`] diff --git a/packages/ozone/src/api/admin/getModerationEvent.ts b/packages/ozone/src/api/admin/getModerationEvent.ts new file mode 100644 index 00000000000..e02757c79a3 --- /dev/null +++ b/packages/ozone/src/api/admin/getModerationEvent.ts @@ -0,0 +1,19 @@ +import { Server } from '../../lexicon' +import AppContext from '../../context' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.admin.getModerationEvent({ + auth: ctx.roleVerifier, + handler: async ({ params }) => { + const { id } = params + const db = ctx.db + const modService = ctx.modService(db) + const event = await modService.getEventOrThrow(id) + const eventDetail = await modService.views.eventDetail(event) + return { + encoding: 'application/json', + body: eventDetail, + } + }, + }) +} diff --git a/packages/bsky/src/api/com/atproto/admin/getRecord.ts b/packages/ozone/src/api/admin/getRecord.ts similarity index 57% rename from packages/bsky/src/api/com/atproto/admin/getRecord.ts rename to packages/ozone/src/api/admin/getRecord.ts index 8e459910806..fbe9a1229d5 100644 --- a/packages/bsky/src/api/com/atproto/admin/getRecord.ts +++ b/packages/ozone/src/api/admin/getRecord.ts @@ -1,29 +1,24 @@ import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' +import { Server } from '../../lexicon' +import AppContext from '../../context' import { addAccountInfoToRepoView, getPdsAccountInfo } from './util' +import { AtUri } from '@atproto/syntax' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getRecord({ auth: ctx.roleVerifier, handler: async ({ params, auth }) => { - const { uri, cid } = params - const db = ctx.db.getPrimary() - const result = await db.db - .selectFrom('record') - .selectAll() - .where('uri', '=', uri) - .if(!!cid, (qb) => qb.where('cid', '=', cid ?? '')) - .executeTakeFirst() - if (!result) { - throw new InvalidRequestError('Record not found', 'RecordNotFound') - } + const db = ctx.db const [record, accountInfo] = await Promise.all([ - ctx.services.moderation(db).views.recordDetail(result), - getPdsAccountInfo(ctx, result.did), + ctx.modService(db).views.recordDetail(params), + getPdsAccountInfo(ctx, new AtUri(params.uri).hostname), ]) + if (!record) { + throw new InvalidRequestError('Record not found', 'RecordNotFound') + } + record.repo = addAccountInfoToRepoView( record.repo, accountInfo, diff --git a/packages/bsky/src/api/com/atproto/admin/getRepo.ts b/packages/ozone/src/api/admin/getRepo.ts similarity index 68% rename from packages/bsky/src/api/com/atproto/admin/getRepo.ts rename to packages/ozone/src/api/admin/getRepo.ts index 314b345b5e9..5da30f24524 100644 --- a/packages/bsky/src/api/com/atproto/admin/getRepo.ts +++ b/packages/ozone/src/api/admin/getRepo.ts @@ -1,6 +1,6 @@ import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' +import { Server } from '../../lexicon' +import AppContext from '../../context' import { addAccountInfoToRepoViewDetail, getPdsAccountInfo } from './util' export default function (server: Server, ctx: AppContext) { @@ -8,15 +8,14 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.roleVerifier, handler: async ({ params, auth }) => { const { did } = params - const db = ctx.db.getPrimary() - const result = await ctx.services.actor(db).getActor(did, true) - if (!result) { - throw new InvalidRequestError('Repo not found', 'RepoNotFound') - } + const db = ctx.db const [partialRepo, accountInfo] = await Promise.all([ - ctx.services.moderation(db).views.repoDetail(result), - getPdsAccountInfo(ctx, result.did), + ctx.modService(db).views.repoDetail(did), + getPdsAccountInfo(ctx, did), ]) + if (!partialRepo) { + throw new InvalidRequestError('Repo not found', 'RepoNotFound') + } const repo = addAccountInfoToRepoViewDetail( partialRepo, diff --git a/packages/bsky/src/api/com/atproto/admin/queryModerationEvents.ts b/packages/ozone/src/api/admin/queryModerationEvents.ts similarity index 70% rename from packages/bsky/src/api/com/atproto/admin/queryModerationEvents.ts rename to packages/ozone/src/api/admin/queryModerationEvents.ts index 1868533295c..4c0cbdd1500 100644 --- a/packages/bsky/src/api/com/atproto/admin/queryModerationEvents.ts +++ b/packages/ozone/src/api/admin/queryModerationEvents.ts @@ -1,5 +1,5 @@ -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' +import { Server } from '../../lexicon' +import AppContext from '../../context' import { getEventType } from '../moderation/util' export default function (server: Server, ctx: AppContext) { @@ -15,9 +15,9 @@ export default function (server: Server, ctx: AppContext) { includeAllUserRecords = false, createdBy, } = params - const db = ctx.db.getPrimary() - const moderationService = ctx.services.moderation(db) - const results = await moderationService.getEvents({ + const db = ctx.db + const modService = ctx.modService(db) + const results = await modService.getEvents({ types: types?.length ? types.map(getEventType) : [], subject, createdBy, @@ -30,7 +30,9 @@ export default function (server: Server, ctx: AppContext) { encoding: 'application/json', body: { cursor: results.cursor, - events: await moderationService.views.event(results.events), + events: results.events.map((evt) => + modService.views.formatEvent(evt), + ), }, } }, diff --git a/packages/bsky/src/api/com/atproto/admin/queryModerationStatuses.ts b/packages/ozone/src/api/admin/queryModerationStatuses.ts similarity index 76% rename from packages/bsky/src/api/com/atproto/admin/queryModerationStatuses.ts rename to packages/ozone/src/api/admin/queryModerationStatuses.ts index e664e90343c..fc935e5917a 100644 --- a/packages/bsky/src/api/com/atproto/admin/queryModerationStatuses.ts +++ b/packages/ozone/src/api/admin/queryModerationStatuses.ts @@ -1,5 +1,5 @@ -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' +import { Server } from '../../lexicon' +import AppContext from '../../context' import { getReviewState } from '../moderation/util' export default function (server: Server, ctx: AppContext) { @@ -23,9 +23,9 @@ export default function (server: Server, ctx: AppContext) { limit = 50, cursor, } = params - const db = ctx.db.getPrimary() - const moderationService = ctx.services.moderation(db) - const results = await moderationService.getSubjectStatuses({ + const db = ctx.db + const modService = ctx.modService(db) + const results = await modService.getSubjectStatuses({ reviewState: getReviewState(reviewState), subject, takendown, @@ -42,8 +42,8 @@ export default function (server: Server, ctx: AppContext) { limit, cursor, }) - const subjectStatuses = moderationService.views.subjectStatus( - results.statuses, + const subjectStatuses = results.statuses.map((status) => + modService.views.formatSubjectStatus(status), ) return { encoding: 'application/json', diff --git a/packages/ozone/src/api/admin/searchRepos.ts b/packages/ozone/src/api/admin/searchRepos.ts new file mode 100644 index 00000000000..fcdfc1b6d85 --- /dev/null +++ b/packages/ozone/src/api/admin/searchRepos.ts @@ -0,0 +1,42 @@ +import { Server } from '../../lexicon' +import AppContext from '../../context' +import { mapDefined } from '@atproto/common' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.admin.searchRepos({ + auth: ctx.roleVerifier, + handler: async ({ params }) => { + const modService = ctx.modService(ctx.db) + + // prefer new 'q' query param over deprecated 'term' + const query = params.q ?? params.term + + // special case for did searches - do exact match + if (query?.startsWith('did:')) { + const repos = await modService.views.repos([query]) + const found = repos.get(query) + return { + encoding: 'application/json', + body: { + repos: found ? [found] : [], + }, + } + } + + const res = await ctx.appviewAgent.api.app.bsky.actor.searchActors(params) + const repoMap = await modService.views.repos( + res.data.actors.map((a) => a.did), + ) + const repos = mapDefined(res.data.actors, (actor) => + repoMap.get(actor.did), + ) + return { + encoding: 'application/json', + body: { + cursor: res.data.cursor, + repos, + }, + } + }, + }) +} diff --git a/packages/bsky/src/api/com/atproto/admin/util.ts b/packages/ozone/src/api/admin/util.ts similarity index 86% rename from packages/bsky/src/api/com/atproto/admin/util.ts rename to packages/ozone/src/api/admin/util.ts index 7dfd10cce5c..b4df0664327 100644 --- a/packages/bsky/src/api/com/atproto/admin/util.ts +++ b/packages/ozone/src/api/admin/util.ts @@ -1,18 +1,20 @@ -import AppContext from '../../../../context' +import AppContext from '../../context' import { RepoView, RepoViewDetail, AccountView, -} from '../../../../lexicon/types/com/atproto/admin/defs' +} from '../../lexicon/types/com/atproto/admin/defs' export const getPdsAccountInfo = async ( ctx: AppContext, did: string, ): Promise => { - const agent = ctx.moderationPushAgent + const agent = ctx.pdsAgent if (!agent) return null + const auth = await ctx.pdsAuth() + if (!auth) return null try { - const res = await agent.api.com.atproto.admin.getAccountInfo({ did }) + const res = await agent.api.com.atproto.admin.getAccountInfo({ did }, auth) return res.data } catch (err) { return null diff --git a/packages/ozone/src/api/health.ts b/packages/ozone/src/api/health.ts new file mode 100644 index 00000000000..60328f333f0 --- /dev/null +++ b/packages/ozone/src/api/health.ts @@ -0,0 +1,20 @@ +import express from 'express' +import { sql } from 'kysely' +import AppContext from '../context' + +export const createRouter = (ctx: AppContext): express.Router => { + const router = express.Router() + + router.get('/xrpc/_health', async function (req, res) { + const { version } = ctx.cfg.service + try { + await sql`select 1`.execute(ctx.db.db) + } catch (err) { + req.log.error(err, 'failed health check') + return res.status(503).send({ version, error: 'Service Unavailable' }) + } + res.send({ version }) + }) + + return router +} diff --git a/packages/ozone/src/api/index.ts b/packages/ozone/src/api/index.ts new file mode 100644 index 00000000000..a07b9c23b5b --- /dev/null +++ b/packages/ozone/src/api/index.ts @@ -0,0 +1,28 @@ +import { Server } from '../lexicon' +import AppContext from '../context' +import createReport from './moderation/createReport' +import emitModerationEvent from './admin/emitModerationEvent' +import searchRepos from './admin/searchRepos' +import adminGetRecord from './admin/getRecord' +import getRepo from './admin/getRepo' +import queryModerationStatuses from './admin/queryModerationStatuses' +import queryModerationEvents from './admin/queryModerationEvents' +import getModerationEvent from './admin/getModerationEvent' +import fetchLabels from './temp/fetchLabels' + +export * as health from './health' + +export * as wellKnown from './well-known' + +export default function (server: Server, ctx: AppContext) { + createReport(server, ctx) + emitModerationEvent(server, ctx) + searchRepos(server, ctx) + adminGetRecord(server, ctx) + getRepo(server, ctx) + getModerationEvent(server, ctx) + queryModerationEvents(server, ctx) + queryModerationStatuses(server, ctx) + fetchLabels(server, ctx) + return server +} diff --git a/packages/ozone/src/api/moderation/createReport.ts b/packages/ozone/src/api/moderation/createReport.ts new file mode 100644 index 00000000000..6ede6dcd0e4 --- /dev/null +++ b/packages/ozone/src/api/moderation/createReport.ts @@ -0,0 +1,41 @@ +import { Server } from '../../lexicon' +import AppContext from '../../context' +import { getReasonType } from './util' +import { subjectFromInput } from '../../mod-service/subject' +import { REASONAPPEAL } from '../../lexicon/types/com/atproto/moderation/defs' +import { ForbiddenError } from '@atproto/xrpc-server' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.moderation.createReport({ + // @TODO anonymous reports w/ optional auth are a temporary measure + auth: ctx.authOptionalAccessOrRoleVerifier, + handler: async ({ input, auth }) => { + const requester = + 'did' in auth.credentials ? auth.credentials.did : ctx.cfg.service.did + const { reasonType, reason } = input.body + const subject = subjectFromInput(input.body.subject) + + // If the report is an appeal, the requester must be the author of the subject + if (reasonType === REASONAPPEAL && requester !== subject.did) { + throw new ForbiddenError('You cannot appeal this report') + } + + const db = ctx.db + const report = await db.transaction(async (dbTxn) => { + const moderationTxn = ctx.modService(dbTxn) + return moderationTxn.report({ + reasonType: getReasonType(reasonType), + reason, + subject, + reportedBy: requester || ctx.cfg.service.did, + }) + }) + + const body = ctx.modService(db).views.formatReport(report) + return { + encoding: 'application/json', + body, + } + }, + }) +} diff --git a/packages/bsky/src/api/com/atproto/moderation/util.ts b/packages/ozone/src/api/moderation/util.ts similarity index 59% rename from packages/bsky/src/api/com/atproto/moderation/util.ts rename to packages/ozone/src/api/moderation/util.ts index fbb144b1c0a..040007d5e79 100644 --- a/packages/bsky/src/api/com/atproto/moderation/util.ts +++ b/packages/ozone/src/api/moderation/util.ts @@ -1,8 +1,5 @@ -import { CID } from 'multiformats/cid' import { InvalidRequestError } from '@atproto/xrpc-server' -import { AtUri } from '@atproto/syntax' -import { InputSchema as ReportInput } from '../../../../lexicon/types/com/atproto/moderation/createReport' -import { InputSchema as ActionInput } from '../../../../lexicon/types/com/atproto/admin/emitModerationEvent' +import { InputSchema as ReportInput } from '../../lexicon/types/com/atproto/moderation/createReport' import { REASONOTHER, REASONSPAM, @@ -11,37 +8,14 @@ import { REASONSEXUAL, REASONVIOLATION, REASONAPPEAL, -} from '../../../../lexicon/types/com/atproto/moderation/defs' +} from '../../lexicon/types/com/atproto/moderation/defs' import { REVIEWCLOSED, REVIEWESCALATED, REVIEWOPEN, -} from '../../../../lexicon/types/com/atproto/admin/defs' -import { ModerationEvent } from '../../../../db/tables/moderation' -import { ModerationSubjectStatusRow } from '../../../../services/moderation/types' - -type SubjectInput = ReportInput['subject'] | ActionInput['subject'] - -export const getSubject = (subject: SubjectInput) => { - if ( - subject.$type === 'com.atproto.admin.defs#repoRef' && - typeof subject.did === 'string' - ) { - return { did: subject.did } - } - if ( - subject.$type === 'com.atproto.repo.strongRef' && - typeof subject.uri === 'string' && - typeof subject.cid === 'string' - ) { - const uri = new AtUri(subject.uri) - return { - uri, - cid: CID.parse(subject.cid), - } - } - throw new InvalidRequestError('Invalid subject') -} +} from '../../lexicon/types/com/atproto/admin/defs' +import { ModerationEvent } from '../../db/schema/moderation_event' +import { ModerationSubjectStatusRow } from '../../mod-service/types' export const getReasonType = (reasonType: ReportInput['reasonType']) => { if (reasonTypes.has(reasonType)) { diff --git a/packages/ozone/src/api/temp/fetchLabels.ts b/packages/ozone/src/api/temp/fetchLabels.ts new file mode 100644 index 00000000000..69ead2cb28e --- /dev/null +++ b/packages/ozone/src/api/temp/fetchLabels.ts @@ -0,0 +1,29 @@ +import { Server } from '../../lexicon' +import AppContext from '../../context' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.temp.fetchLabels(async ({ params }) => { + const { limit } = params + const since = + params.since !== undefined ? new Date(params.since).toISOString() : '' + const labelRes = await ctx.db.db + .selectFrom('label') + .selectAll() + .orderBy('label.cts', 'asc') + .where('cts', '>', since) + .limit(limit) + .execute() + + const labels = labelRes.map((l) => ({ + ...l, + cid: l.cid === '' ? undefined : l.cid, + })) + + return { + encoding: 'application/json', + body: { + labels, + }, + } + }) +} diff --git a/packages/ozone/src/api/well-known.ts b/packages/ozone/src/api/well-known.ts new file mode 100644 index 00000000000..9cbfa9efe53 --- /dev/null +++ b/packages/ozone/src/api/well-known.ts @@ -0,0 +1,35 @@ +import express from 'express' +import AppContext from '../context' + +export const createRouter = (ctx: AppContext): express.Router => { + const router = express.Router() + + router.get('/.well-known/did.json', (_req, res) => { + const hostname = + ctx.cfg.service.publicUrl && new URL(ctx.cfg.service.publicUrl).hostname + if (!hostname || ctx.cfg.service.did !== `did:web:${hostname}`) { + return res.sendStatus(404) + } + res.json({ + '@context': ['https://www.w3.org/ns/did/v1'], + id: ctx.cfg.service.did, + verificationMethod: [ + { + id: `${ctx.cfg.service.did}#atproto`, + type: 'Multikey', + controller: ctx.cfg.service.did, + publicKeyMultibase: ctx.signingKey.did().replace('did:key:', ''), + }, + ], + service: [ + { + id: '#atproto_mod', + type: 'AtprotoModerationService', + serviceEndpoint: `https://${hostname}`, + }, + ], + }) + }) + + return router +} diff --git a/packages/bsky/src/auth.ts b/packages/ozone/src/auth.ts similarity index 86% rename from packages/bsky/src/auth.ts rename to packages/ozone/src/auth.ts index ba58638d4f9..e996f068c49 100644 --- a/packages/bsky/src/auth.ts +++ b/packages/ozone/src/auth.ts @@ -2,7 +2,7 @@ import express from 'express' import * as uint8arrays from 'uint8arrays' import { AuthRequiredError, verifyJwt } from '@atproto/xrpc-server' import { IdResolver } from '@atproto/identity' -import { ServerConfig } from './config' +import { OzoneSecrets } from './config' const BASIC = 'Basic ' const BEARER = 'Bearer ' @@ -47,10 +47,11 @@ export const authOptionalVerifier = ( export const authOptionalAccessOrRoleVerifier = ( idResolver: IdResolver, - cfg: ServerConfig, + secrets: OzoneSecrets, + serverDid: string, ) => { - const verifyAccess = authVerifier(idResolver, { aud: cfg.serverDid }) - const verifyRole = roleVerifier(cfg) + const verifyAccess = authVerifier(idResolver, { aud: serverDid }) + const verifyRole = roleVerifier(secrets) return async (ctx: { req: express.Request; res: express.Response }) => { const defaultUnAuthorizedCredentials = { credentials: { did: null, type: 'unauthed' as const }, @@ -82,25 +83,28 @@ export const authOptionalAccessOrRoleVerifier = ( } export const roleVerifier = - (cfg: ServerConfig) => + (secrets: OzoneSecrets) => async (reqCtx: { req: express.Request; res: express.Response }) => { - const credentials = getRoleCredentials(cfg, reqCtx.req) + const credentials = getRoleCredentials(secrets, reqCtx.req) if (!credentials.valid) { throw new AuthRequiredError() } return { credentials } } -export const getRoleCredentials = (cfg: ServerConfig, req: express.Request) => { +export const getRoleCredentials = ( + secrets: OzoneSecrets, + req: express.Request, +) => { const parsed = parseBasicAuth(req.headers.authorization || '') const { username, password } = parsed ?? {} - if (username === 'admin' && password === cfg.triagePassword) { + if (username === 'admin' && password === secrets.triagePassword) { return { valid: true, admin: false, moderator: false, triage: true } } - if (username === 'admin' && password === cfg.moderatorPassword) { + if (username === 'admin' && password === secrets.moderatorPassword) { return { valid: true, admin: false, moderator: true, triage: true } } - if (username === 'admin' && password === cfg.adminPassword) { + if (username === 'admin' && password === secrets.adminPassword) { return { valid: true, admin: true, moderator: true, triage: true } } return { valid: false, admin: false, moderator: false, triage: false } diff --git a/packages/ozone/src/background.ts b/packages/ozone/src/background.ts new file mode 100644 index 00000000000..78cd790e779 --- /dev/null +++ b/packages/ozone/src/background.ts @@ -0,0 +1,35 @@ +import PQueue from 'p-queue' +import { Database } from './db' +import { dbLogger } from './logger' + +// A simple queue for in-process, out-of-band/backgrounded work + +export class BackgroundQueue { + queue = new PQueue({ concurrency: 20 }) + destroyed = false + constructor(public db: Database) {} + + add(task: Task) { + if (this.destroyed) { + return + } + this.queue + .add(() => task(this.db)) + .catch((err) => { + dbLogger.error(err, 'background queue task failed') + }) + } + + async processAll() { + await this.queue.onIdle() + } + + // On destroy we stop accepting new tasks, but complete all pending/in-progress tasks. + // The application calls this only once http connections have drained (tasks no longer being added). + async destroy() { + this.destroyed = true + await this.queue.onIdle() + } +} + +type Task = (db: Database) => Promise diff --git a/packages/ozone/src/config/config.ts b/packages/ozone/src/config/config.ts new file mode 100644 index 00000000000..caa799b2a90 --- /dev/null +++ b/packages/ozone/src/config/config.ts @@ -0,0 +1,84 @@ +import assert from 'node:assert' +import { OzoneEnvironment } from './env' + +// off-config but still from env: +// logging: LOG_LEVEL, LOG_SYSTEMS, LOG_ENABLED, LOG_DESTINATION + +export const envToCfg = (env: OzoneEnvironment): OzoneConfig => { + const port = env.port ?? 3000 + assert(env.publicUrl) + assert(env.serverDid) + const serviceCfg: OzoneConfig['service'] = { + port, + publicUrl: env.publicUrl, + did: env.serverDid, + version: env.version, + } + + assert(env.dbPostgresUrl) + const dbCfg: OzoneConfig['db'] = { + postgresUrl: env.dbPostgresUrl, + postgresSchema: env.dbPostgresSchema, + } + + assert(env.appviewUrl) + assert(env.appviewDid) + const appviewCfg: OzoneConfig['appview'] = { + url: env.appviewUrl, + did: env.appviewDid, + } + + assert(env.pdsUrl) + assert(env.pdsDid) + const pdsCfg: OzoneConfig['pds'] = { + url: env.pdsUrl, + did: env.pdsDid, + } + + assert(env.didPlcUrl) + const identityCfg: OzoneConfig['identity'] = { + plcUrl: env.didPlcUrl, + } + + return { + service: serviceCfg, + db: dbCfg, + appview: appviewCfg, + pds: pdsCfg, + identity: identityCfg, + } +} + +export type OzoneConfig = { + service: ServiceConfig + db: DatabaseConfig + appview: AppviewConfig + pds: PdsConfig | null + identity: IdentityConfig +} + +export type ServiceConfig = { + port: number + publicUrl: string + did: string + version?: string +} + +export type DatabaseConfig = { + postgresUrl: string + postgresSchema?: string +} + +export type AppviewConfig = { + url: string + did: string +} + +export type PdsConfig = { + url: string + did: string +} + +export type IdentityConfig = { + plcUrl: string +} diff --git a/packages/ozone/src/config/env.ts b/packages/ozone/src/config/env.ts new file mode 100644 index 00000000000..4f96ba63d53 --- /dev/null +++ b/packages/ozone/src/config/env.ts @@ -0,0 +1,41 @@ +import { envInt, envStr } from '@atproto/common' + +export const readEnv = (): OzoneEnvironment => { + return { + nodeEnv: envStr('NODE_ENV'), + version: envStr('OZONE_VERSION'), + port: envInt('OZONE_PORT'), + publicUrl: envStr('OZONE_PUBLIC_URL'), + serverDid: envStr('OZONE_SERVER_DID'), + appviewUrl: envStr('OZONE_APPVIEW_URL'), + appviewDid: envStr('OZONE_APPVIEW_DID'), + pdsUrl: envStr('OZONE_PDS_URL'), + pdsDid: envStr('OZONE_PDS_DID'), + dbPostgresUrl: envStr('OZONE_DB_POSTGRES_URL'), + dbPostgresSchema: envStr('OZONE_DB_POSTGRES_SCHEMA'), + didPlcUrl: envStr('OZONE_DID_PLC_URL'), + adminPassword: envStr('OZONE_ADMIN_PASSWORD'), + moderatorPassword: envStr('OZONE_MODERATOR_PASSWORD'), + triagePassword: envStr('OZONE_TRIAGE_PASSWORD'), + signingKeyHex: envStr('OZONE_SIGNING_KEY_HEX'), + } +} + +export type OzoneEnvironment = { + nodeEnv?: string + version?: string + port?: number + publicUrl?: string + serverDid?: string + appviewUrl?: string + appviewDid?: string + pdsUrl?: string + pdsDid?: string + dbPostgresUrl?: string + dbPostgresSchema?: string + didPlcUrl?: string + adminPassword?: string + moderatorPassword?: string + triagePassword?: string + signingKeyHex?: string +} diff --git a/packages/ozone/src/config/index.ts b/packages/ozone/src/config/index.ts new file mode 100644 index 00000000000..cd02efb6c87 --- /dev/null +++ b/packages/ozone/src/config/index.ts @@ -0,0 +1,3 @@ +export * from './config' +export * from './env' +export * from './secrets' diff --git a/packages/ozone/src/config/secrets.ts b/packages/ozone/src/config/secrets.ts new file mode 100644 index 00000000000..22593bcec26 --- /dev/null +++ b/packages/ozone/src/config/secrets.ts @@ -0,0 +1,23 @@ +import assert from 'assert' +import { OzoneEnvironment } from './env' + +export const envToSecrets = (env: OzoneEnvironment): OzoneSecrets => { + assert(env.adminPassword) + assert(env.moderatorPassword) + assert(env.triagePassword) + assert(env.signingKeyHex) + + return { + adminPassword: env.adminPassword, + moderatorPassword: env.moderatorPassword, + triagePassword: env.triagePassword, + signingKeyHex: env.signingKeyHex, + } +} + +export type OzoneSecrets = { + adminPassword: string + moderatorPassword: string + triagePassword: string + signingKeyHex: string +} diff --git a/packages/ozone/src/context.ts b/packages/ozone/src/context.ts new file mode 100644 index 00000000000..30f356a38e7 --- /dev/null +++ b/packages/ozone/src/context.ts @@ -0,0 +1,180 @@ +import * as plc from '@did-plc/lib' +import { IdResolver } from '@atproto/identity' +import { AtpAgent } from '@atproto/api' +import { Keypair, Secp256k1Keypair } from '@atproto/crypto' +import { createServiceAuthHeaders } from '@atproto/xrpc-server' +import { Database } from './db' +import { OzoneConfig, OzoneSecrets } from './config' +import { ModerationService, ModerationServiceCreator } from './mod-service' +import * as auth from './auth' +import { BackgroundQueue } from './background' +import assert from 'assert' +import { EventPusher } from './daemon' + +export type AppContextOptions = { + db: Database + cfg: OzoneConfig + modService: ModerationServiceCreator + appviewAgent: AtpAgent + pdsAgent: AtpAgent | undefined + signingKey: Keypair + idResolver: IdResolver + backgroundQueue: BackgroundQueue +} + +export class AppContext { + constructor(private opts: AppContextOptions, private secrets: OzoneSecrets) {} + + static async fromConfig( + cfg: OzoneConfig, + secrets: OzoneSecrets, + overrides?: Partial, + ): Promise { + const db = new Database({ + url: cfg.db.postgresUrl, + schema: cfg.db.postgresSchema, + }) + const signingKey = await Secp256k1Keypair.import(secrets.signingKeyHex) + const appviewAgent = new AtpAgent({ service: cfg.appview.url }) + const pdsAgent = cfg.pds + ? new AtpAgent({ service: cfg.pds.url }) + : undefined + + const createAuthHeaders = (aud: string) => + createServiceAuthHeaders({ + iss: cfg.service.did, + aud, + keypair: signingKey, + }) + const appviewAuth = async () => + cfg.appview.did ? createAuthHeaders(cfg.appview.did) : undefined + + const backgroundQueue = new BackgroundQueue(db) + const eventPusher = new EventPusher(db, createAuthHeaders, { + appview: cfg.appview, + pds: cfg.pds ?? undefined, + }) + + const modService = ModerationService.creator( + backgroundQueue, + eventPusher, + appviewAgent, + appviewAuth, + ) + + const idResolver = new IdResolver({ + plcUrl: cfg.identity.plcUrl, + }) + + return new AppContext( + { + db, + cfg, + modService, + appviewAgent, + pdsAgent, + signingKey, + idResolver, + backgroundQueue, + ...(overrides ?? {}), + }, + secrets, + ) + } + + assignPort(port: number) { + assert( + !this.cfg.service.port || this.cfg.service.port === port, + 'Conflicting port in config', + ) + this.opts.cfg.service.port = port + } + + get db(): Database { + return this.opts.db + } + + get cfg(): OzoneConfig { + return this.opts.cfg + } + + get modService(): ModerationServiceCreator { + return this.opts.modService + } + + get appviewAgent(): AtpAgent { + return this.opts.appviewAgent + } + + get pdsAgent(): AtpAgent | undefined { + return this.opts.pdsAgent + } + + get signingKey(): Keypair { + return this.opts.signingKey + } + + get plcClient(): plc.Client { + return new plc.Client(this.cfg.identity.plcUrl) + } + + get idResolver(): IdResolver { + return this.opts.idResolver + } + + get backgroundQueue(): BackgroundQueue { + return this.opts.backgroundQueue + } + + get authVerifier() { + return auth.authVerifier(this.idResolver, { aud: this.cfg.service.did }) + } + + get authVerifierAnyAudience() { + return auth.authVerifier(this.idResolver, { aud: null }) + } + + get authOptionalVerifierAnyAudience() { + return auth.authOptionalVerifier(this.idResolver, { aud: null }) + } + + get authOptionalVerifier() { + return auth.authOptionalVerifier(this.idResolver, { + aud: this.cfg.service.did, + }) + } + + get authOptionalAccessOrRoleVerifier() { + return auth.authOptionalAccessOrRoleVerifier( + this.idResolver, + this.secrets, + this.cfg.service.did, + ) + } + + get roleVerifier() { + return auth.roleVerifier(this.secrets) + } + + async serviceAuthHeaders(aud: string) { + const iss = this.cfg.service.did + return createServiceAuthHeaders({ + iss, + aud, + keypair: this.signingKey, + }) + } + + async pdsAuth() { + if (!this.cfg.pds) { + return undefined + } + return this.serviceAuthHeaders(this.cfg.pds.did) + } + + async appviewAuth() { + return this.serviceAuthHeaders(this.cfg.appview.did) + } +} + +export default AppContext diff --git a/packages/ozone/src/daemon/context.ts b/packages/ozone/src/daemon/context.ts new file mode 100644 index 00000000000..42c2a54cea5 --- /dev/null +++ b/packages/ozone/src/daemon/context.ts @@ -0,0 +1,90 @@ +import { Keypair, Secp256k1Keypair } from '@atproto/crypto' +import { createServiceAuthHeaders } from '@atproto/xrpc-server' +import AtpAgent from '@atproto/api' +import { OzoneConfig, OzoneSecrets } from '../config' +import { Database } from '../db' +import { EventPusher } from './event-pusher' +import { EventReverser } from './event-reverser' +import { ModerationService, ModerationServiceCreator } from '../mod-service' +import { BackgroundQueue } from '../background' + +export type DaemonContextOptions = { + db: Database + cfg: OzoneConfig + modService: ModerationServiceCreator + signingKey: Keypair + eventPusher: EventPusher + eventReverser: EventReverser +} + +export class DaemonContext { + constructor(private opts: DaemonContextOptions) {} + + static async fromConfig( + cfg: OzoneConfig, + secrets: OzoneSecrets, + overrides?: Partial, + ): Promise { + const db = new Database({ + url: cfg.db.postgresUrl, + schema: cfg.db.postgresSchema, + }) + const signingKey = await Secp256k1Keypair.import(secrets.signingKeyHex) + + const appviewAgent = new AtpAgent({ service: cfg.appview.url }) + const createAuthHeaders = (aud: string) => + createServiceAuthHeaders({ + iss: cfg.service.did, + aud, + keypair: signingKey, + }) + + const appviewAuth = async () => + cfg.appview.did ? createAuthHeaders(cfg.appview.did) : undefined + + const eventPusher = new EventPusher(db, createAuthHeaders, { + appview: cfg.appview, + pds: cfg.pds ?? undefined, + }) + const backgroundQueue = new BackgroundQueue(db) + const modService = ModerationService.creator( + backgroundQueue, + eventPusher, + appviewAgent, + appviewAuth, + ) + const eventReverser = new EventReverser(db, modService) + + return new DaemonContext({ + db, + cfg, + modService, + signingKey, + eventPusher, + eventReverser, + ...(overrides ?? {}), + }) + } + + get db(): Database { + return this.opts.db + } + + get cfg(): OzoneConfig { + return this.opts.cfg + } + + get modService(): ModerationServiceCreator { + return this.opts.modService + } + + get eventPusher(): EventPusher { + return this.opts.eventPusher + } + + get eventReverser(): EventReverser { + return this.opts.eventReverser + } +} + +export default DaemonContext diff --git a/packages/ozone/src/daemon/event-pusher.ts b/packages/ozone/src/daemon/event-pusher.ts new file mode 100644 index 00000000000..faaee4529ed --- /dev/null +++ b/packages/ozone/src/daemon/event-pusher.ts @@ -0,0 +1,296 @@ +import AtpAgent from '@atproto/api' +import { SECOND } from '@atproto/common' +import Database from '../db' +import { retryHttp } from '../util' +import { dbLogger } from '../logger' +import { InputSchema } from '../lexicon/types/com/atproto/admin/updateSubjectStatus' +import assert from 'assert' + +type EventSubject = InputSchema['subject'] + +type PollState = { + timer?: NodeJS.Timer + promise: Promise +} + +type AuthHeaders = { + headers: { + authorization: string + } +} + +type Service = { + agent: AtpAgent + did: string +} + +export class EventPusher { + destroyed = false + + repoPollState: PollState = { + promise: Promise.resolve(), + } + recordPollState: PollState = { + promise: Promise.resolve(), + } + blobPollState: PollState = { + promise: Promise.resolve(), + } + + appview: Service | undefined + pds: Service | undefined + + constructor( + public db: Database, + public createAuthHeaders: (aud: string) => Promise, + services: { + appview?: { + url: string + did: string + } + pds?: { + url: string + did: string + } + }, + ) { + if (services.appview) { + this.appview = { + agent: new AtpAgent({ service: services.appview.url }), + did: services.appview.did, + } + } + if (services.pds) { + this.pds = { + agent: new AtpAgent({ service: services.pds.url }), + did: services.pds.did, + } + } + } + + start() { + this.poll(this.repoPollState, () => this.pushRepoEvents()) + this.poll(this.recordPollState, () => this.pushRecordEvents()) + this.poll(this.blobPollState, () => this.pushBlobEvents()) + } + + poll(state: PollState, fn: () => Promise) { + if (this.destroyed) return + state.promise = fn() + .catch((err) => { + dbLogger.error({ err }, 'event push failed') + }) + .finally(() => { + state.timer = setTimeout(() => this.poll(state, fn), 30 * SECOND) + }) + } + + async processAll() { + await Promise.all([ + this.pushRepoEvents(), + this.pushRecordEvents(), + this.pushBlobEvents(), + this.repoPollState.promise, + this.recordPollState.promise, + this.blobPollState.promise, + ]) + } + + async destroy() { + this.destroyed = true + const destroyState = (state: PollState) => { + if (state.timer) { + clearTimeout(state.timer) + } + return state.promise + } + await Promise.all([ + destroyState(this.repoPollState), + destroyState(this.recordPollState), + destroyState(this.blobPollState), + ]) + } + + async pushRepoEvents() { + const toPush = await this.db.db + .selectFrom('repo_push_event') + .select('id') + .forUpdate() + .skipLocked() + .where('confirmedAt', 'is', null) + .where('attempts', '<', 10) + .execute() + await Promise.all(toPush.map((evt) => this.attemptRepoEvent(evt.id))) + } + + async pushRecordEvents() { + const toPush = await this.db.db + .selectFrom('record_push_event') + .select('id') + .forUpdate() + .skipLocked() + .where('confirmedAt', 'is', null) + .where('attempts', '<', 10) + .execute() + await Promise.all(toPush.map((evt) => this.attemptRecordEvent(evt.id))) + } + + async pushBlobEvents() { + const toPush = await this.db.db + .selectFrom('blob_push_event') + .select('id') + .forUpdate() + .skipLocked() + .where('confirmedAt', 'is', null) + .where('attempts', '<', 10) + .execute() + await Promise.all(toPush.map((evt) => this.attemptBlobEvent(evt.id))) + } + + private async updateSubjectOnService( + service: Service, + subject: EventSubject, + takedownRef: string | null, + ): Promise { + const auth = await this.createAuthHeaders(service.did) + try { + await retryHttp(() => + service.agent.com.atproto.admin.updateSubjectStatus( + { + subject, + takedown: { + applied: !!takedownRef, + ref: takedownRef ?? undefined, + }, + }, + { + ...auth, + encoding: 'application/json', + }, + ), + ) + return true + } catch (err) { + dbLogger.error({ err, subject, takedownRef }, 'failed to push out event') + return false + } + } + + async attemptRepoEvent(id: number) { + await this.db.transaction(async (dbTxn) => { + const evt = await dbTxn.db + .selectFrom('repo_push_event') + .selectAll() + .forUpdate() + .skipLocked() + .where('id', '=', id) + .where('confirmedAt', 'is', null) + .executeTakeFirst() + if (!evt) return + const service = evt.eventType === 'pds_takedown' ? this.pds : this.appview + assert(service) + const subject = { + $type: 'com.atproto.admin.defs#repoRef', + did: evt.subjectDid, + } + const succeeded = await this.updateSubjectOnService( + service, + subject, + evt.takedownRef, + ) + await dbTxn.db + .updateTable('repo_push_event') + .set( + succeeded + ? { confirmedAt: new Date() } + : { + lastAttempted: new Date(), + attempts: evt.attempts ?? 0 + 1, + }, + ) + .where('subjectDid', '=', evt.subjectDid) + .where('eventType', '=', evt.eventType) + .execute() + }) + } + + async attemptRecordEvent(id: number) { + await this.db.transaction(async (dbTxn) => { + const evt = await dbTxn.db + .selectFrom('record_push_event') + .selectAll() + .forUpdate() + .skipLocked() + .where('id', '=', id) + .where('confirmedAt', 'is', null) + .executeTakeFirst() + if (!evt) return + const service = evt.eventType === 'pds_takedown' ? this.pds : this.appview + assert(service) + const subject = { + $type: 'com.atproto.repo.strongRef', + uri: evt.subjectUri, + cid: evt.subjectCid, + } + const succeeded = await this.updateSubjectOnService( + service, + subject, + evt.takedownRef, + ) + await dbTxn.db + .updateTable('record_push_event') + .set( + succeeded + ? { confirmedAt: new Date() } + : { + lastAttempted: new Date(), + attempts: evt.attempts ?? 0 + 1, + }, + ) + .where('subjectUri', '=', evt.subjectUri) + .where('eventType', '=', evt.eventType) + .execute() + }) + } + + async attemptBlobEvent(id: number) { + await this.db.transaction(async (dbTxn) => { + const evt = await dbTxn.db + .selectFrom('blob_push_event') + .selectAll() + .forUpdate() + .skipLocked() + .where('id', '=', id) + .where('confirmedAt', 'is', null) + .executeTakeFirst() + if (!evt) return + + const service = evt.eventType === 'pds_takedown' ? this.pds : this.appview + assert(service) + const subject = { + $type: 'com.atproto.admin.defs#repoBlobRef', + did: evt.subjectDid, + cid: evt.subjectBlobCid, + } + const succeeded = await this.updateSubjectOnService( + service, + subject, + evt.takedownRef, + ) + await dbTxn.db + .updateTable('blob_push_event') + .set( + succeeded + ? { confirmedAt: new Date() } + : { + lastAttempted: new Date(), + attempts: evt.attempts ?? 0 + 1, + }, + ) + .where('subjectDid', '=', evt.subjectDid) + .where('subjectBlobCid', '=', evt.subjectBlobCid) + .where('eventType', '=', evt.eventType) + .execute() + }) + } +} diff --git a/packages/ozone/src/daemon/event-reverser.ts b/packages/ozone/src/daemon/event-reverser.ts new file mode 100644 index 00000000000..ba3f3cb40f7 --- /dev/null +++ b/packages/ozone/src/daemon/event-reverser.ts @@ -0,0 +1,74 @@ +import { MINUTE } from '@atproto/common' +import { dbLogger } from '../logger' +import { ModerationServiceCreator, ReversalSubject } from '../mod-service' +import Database from '../db' + +export class EventReverser { + destroyed = false + reversalPromise: Promise = Promise.resolve() + timer: NodeJS.Timer | undefined + + constructor( + private db: Database, + private modService: ModerationServiceCreator, + ) {} + + start() { + this.poll() + } + + poll() { + if (this.destroyed) return + this.reversalPromise = this.findAndRevertDueActions() + .catch((err) => + dbLogger.error({ err }, 'moderation action reversal errored'), + ) + .finally(() => { + this.timer = setTimeout(() => this.poll(), getInterval()) + }) + } + + async destroy() { + this.destroyed = true + if (this.timer) { + clearTimeout(this.timer) + } + await this.reversalPromise + } + + async revertState(subject: ReversalSubject) { + await this.db.transaction(async (dbTxn) => { + const moderationTxn = this.modService(dbTxn) + const originalEvent = + await moderationTxn.getLastReversibleEventForSubject(subject) + if (originalEvent) { + await moderationTxn.revertState({ + action: originalEvent.action, + createdBy: originalEvent.createdBy, + comment: + '[SCHEDULED_REVERSAL] Reverting action as originally scheduled', + subject: subject.subject, + createdAt: new Date(), + }) + } + }) + } + + async findAndRevertDueActions() { + const moderationService = this.modService(this.db) + const subjectsDueForReversal = + await moderationService.getSubjectsDueForReversal() + + // We shouldn't have too many actions due for reversal at any given time, so running in parallel is probably fine + // Internally, each reversal runs within its own transaction + await Promise.all(subjectsDueForReversal.map(this.revertState.bind(this))) + } +} + +const getInterval = (): number => { + // super basic synchronization by agreeing when the intervals land relative to unix timestamp + const now = Date.now() + const intervalMs = MINUTE + const nextIteration = Math.ceil(now / intervalMs) + return nextIteration * intervalMs - now +} diff --git a/packages/ozone/src/daemon/index.ts b/packages/ozone/src/daemon/index.ts new file mode 100644 index 00000000000..aa5d7b12734 --- /dev/null +++ b/packages/ozone/src/daemon/index.ts @@ -0,0 +1,33 @@ +import { OzoneConfig, OzoneSecrets } from '../config' +import DaemonContext from './context' +import { AppContextOptions } from '../context' + +export { EventPusher } from './event-pusher' +export { EventReverser } from './event-reverser' + +export class OzoneDaemon { + constructor(public ctx: DaemonContext) {} + static async create( + cfg: OzoneConfig, + secrets: OzoneSecrets, + overrides?: Partial, + ): Promise { + const ctx = await DaemonContext.fromConfig(cfg, secrets, overrides) + return new OzoneDaemon(ctx) + } + + async start() { + this.ctx.eventPusher.start() + this.ctx.eventReverser.start() + } + + async processAll() { + await this.ctx.eventPusher.processAll() + } + + async destroy() { + await this.ctx.eventReverser.destroy() + await this.ctx.eventPusher.destroy() + await this.ctx.db.close() + } +} diff --git a/packages/ozone/src/db/index.ts b/packages/ozone/src/db/index.ts new file mode 100644 index 00000000000..85702af2b79 --- /dev/null +++ b/packages/ozone/src/db/index.ts @@ -0,0 +1,197 @@ +import assert from 'assert' +import { + Kysely, + PostgresDialect, + Migrator, + KyselyPlugin, + PluginTransformQueryArgs, + PluginTransformResultArgs, + RootOperationNode, + QueryResult, + UnknownRow, +} from 'kysely' +import TypedEmitter from 'typed-emitter' +import { Pool as PgPool, types as pgTypes } from 'pg' +import DatabaseSchema, { DatabaseSchemaType } from './schema' +import { PgOptions } from './types' +import { dbLogger } from '../logger' +import { EventEmitter } from 'stream' +import * as migrations from './migrations' +import { CtxMigrationProvider } from './migrations/provider' + +export class Database { + pool: PgPool + db: DatabaseSchema + migrator: Migrator + txEvt = new EventEmitter() as TxnEmitter + destroyed = false + isPrimary = false + + constructor( + public opts: PgOptions, + instances?: { db: DatabaseSchema; pool: PgPool }, + ) { + // if instances are provided, use those + if (instances) { + this.db = instances.db + this.pool = instances.pool + return + } + + // else create a pool & connect + const { schema, url } = opts + const pool = + opts.pool ?? + new PgPool({ + connectionString: url, + max: opts.poolSize, + maxUses: opts.poolMaxUses, + idleTimeoutMillis: opts.poolIdleTimeoutMs, + }) + + // Select count(*) and other pg bigints as js integer + pgTypes.setTypeParser(pgTypes.builtins.INT8, (n) => parseInt(n, 10)) + + // Setup schema usage, primarily for test parallelism (each test suite runs in its own pg schema) + if (schema && !/^[a-z_]+$/i.test(schema)) { + throw new Error(`Postgres schema must only contain [A-Za-z_]: ${schema}`) + } + + pool.on('error', onPoolError) + pool.on('connect', (client) => { + client.on('error', onClientError) + // Used for trigram indexes, e.g. on actor search + client.query('SET pg_trgm.word_similarity_threshold TO .4;') + if (schema) { + // Shared objects such as extensions will go in the public schema + client.query(`SET search_path TO "${schema}",public;`) + } + }) + + this.pool = pool + this.db = new Kysely({ + dialect: new PostgresDialect({ pool }), + }) + this.migrator = new Migrator({ + db: this.db, + migrationTableSchema: opts.schema, + provider: new CtxMigrationProvider(migrations, 'pg'), + }) + } + + get schema(): string | undefined { + return this.opts.schema + } + + get isTransaction() { + return this.db.isTransaction + } + + assertTransaction() { + assert(this.isTransaction, 'Transaction required') + } + + assertNotTransaction() { + assert(!this.isTransaction, 'Cannot be in a transaction') + } + + async transaction(fn: (db: Database) => Promise): Promise { + const leakyTxPlugin = new LeakyTxPlugin() + const { dbTxn, txRes } = await this.db + .withPlugin(leakyTxPlugin) + .transaction() + .execute(async (txn) => { + const dbTxn = new Database(this.opts, { + db: txn, + pool: this.pool, + }) + const txRes = await fn(dbTxn) + .catch(async (err) => { + leakyTxPlugin.endTx() + // ensure that all in-flight queries are flushed & the connection is open + await dbTxn.db.getExecutor().provideConnection(noopAsync) + throw err + }) + .finally(() => leakyTxPlugin.endTx()) + return { dbTxn, txRes } + }) + dbTxn?.txEvt.emit('commit') + return txRes + } + + onCommit(fn: () => void) { + this.assertTransaction() + this.txEvt.once('commit', fn) + } + + async close(): Promise { + if (this.destroyed) return + await this.db.destroy() + this.destroyed = true + } + + async migrateToOrThrow(migration: string) { + if (this.schema) { + await this.db.schema.createSchema(this.schema).ifNotExists().execute() + } + const { error, results } = await this.migrator.migrateTo(migration) + if (error) { + throw error + } + if (!results) { + throw new Error('An unknown failure occurred while migrating') + } + return results + } + + async migrateToLatestOrThrow() { + if (this.schema) { + await this.db.schema.createSchema(this.schema).ifNotExists().execute() + } + const { error, results } = await this.migrator.migrateToLatest() + if (error) { + throw error + } + if (!results) { + throw new Error('An unknown failure occurred while migrating') + } + return results + } +} + +export default Database + +const onPoolError = (err: Error) => dbLogger.error({ err }, 'db pool error') +const onClientError = (err: Error) => dbLogger.error({ err }, 'db client error') + +// utils +// ------- + +class LeakyTxPlugin implements KyselyPlugin { + private txOver: boolean + + endTx() { + this.txOver = true + } + + transformQuery(args: PluginTransformQueryArgs): RootOperationNode { + if (this.txOver) { + throw new Error('tx already failed') + } + return args.node + } + + async transformResult( + args: PluginTransformResultArgs, + ): Promise> { + return args.result + } +} + +type TxnEmitter = TypedEmitter + +type TxnEvents = { + commit: () => void +} + +const noopAsync = async () => {} diff --git a/packages/ozone/src/db/migrations/20231219T205730722Z-init.ts b/packages/ozone/src/db/migrations/20231219T205730722Z-init.ts new file mode 100644 index 00000000000..f636f40a3f4 --- /dev/null +++ b/packages/ozone/src/db/migrations/20231219T205730722Z-init.ts @@ -0,0 +1,164 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + // Moderation event + await db.schema + .createTable('moderation_event') + .addColumn('id', 'serial', (col) => col.primaryKey()) + .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('comment', 'text') + .addColumn('meta', 'jsonb') + .addColumn('createdAt', 'varchar', (col) => col.notNull()) + .addColumn('createdBy', 'varchar', (col) => col.notNull()) + .addColumn('reversedAt', 'varchar') + .addColumn('reversedBy', 'varchar') + .addColumn('durationInHours', 'integer') + .addColumn('expiresAt', 'varchar') + .addColumn('reversedReason', 'text') + .addColumn('createLabelVals', 'varchar') + .addColumn('negateLabelVals', 'varchar') + .addColumn('legacyRefId', 'integer') + .execute() + + // Moderation subject status + await db.schema + .createTable('moderation_subject_status') + .addColumn('id', 'serial', (col) => col.primaryKey()) + + // Identifiers + .addColumn('did', 'varchar', (col) => col.notNull()) + // Default to '' so that we can apply unique constraints on did and recordPath columns + .addColumn('recordPath', 'varchar', (col) => col.notNull().defaultTo('')) + .addColumn('blobCids', 'jsonb') + .addColumn('recordCid', 'varchar') + + // human review team state + .addColumn('reviewState', 'varchar', (col) => col.notNull()) + .addColumn('comment', 'varchar') + .addColumn('muteUntil', 'varchar') + .addColumn('lastReviewedAt', 'varchar') + .addColumn('lastReviewedBy', 'varchar') + + // report state + .addColumn('lastReportedAt', 'varchar') + .addColumn('lastAppealedAt', 'varchar') + + // visibility/intervention state + .addColumn('takendown', 'boolean', (col) => col.defaultTo(false).notNull()) + .addColumn('suspendUntil', 'varchar') + .addColumn('appealed', 'boolean') + + // timestamps + .addColumn('createdAt', 'varchar', (col) => col.notNull()) + .addColumn('updatedAt', 'varchar', (col) => col.notNull()) + .addUniqueConstraint('moderation_status_unique_idx', ['did', 'recordPath']) + .execute() + + await db.schema + .createIndex('moderation_subject_status_blob_cids_idx') + .on('moderation_subject_status') + .using('gin') + .column('blobCids') + .execute() + + // Label + 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', 'boolean', (col) => col.notNull()) + .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() + + // Push Events + await db.schema + .createTable('repo_push_event') + .addColumn('id', 'serial', (col) => col.primaryKey()) + .addColumn('eventType', 'varchar', (col) => col.notNull()) + .addColumn('subjectDid', 'varchar', (col) => col.notNull()) + .addColumn('takedownRef', 'varchar') + .addColumn('confirmedAt', 'timestamptz') + .addColumn('lastAttempted', 'timestamptz') + .addColumn('attempts', 'integer', (col) => col.notNull().defaultTo(0)) + .addUniqueConstraint('repo_push_event_unique_evt', [ + 'subjectDid', + 'eventType', + ]) + .execute() + await db.schema + .createIndex('repo_push_confirmation_idx') + .on('repo_push_event') + .columns(['confirmedAt', 'attempts']) + .execute() + + await db.schema + .createTable('record_push_event') + .addColumn('id', 'serial', (col) => col.primaryKey()) + .addColumn('eventType', 'varchar', (col) => col.notNull()) + .addColumn('subjectDid', 'varchar', (col) => col.notNull()) + .addColumn('subjectUri', 'varchar', (col) => col.notNull()) + .addColumn('subjectCid', 'varchar') + .addColumn('takedownRef', 'varchar') + .addColumn('confirmedAt', 'timestamptz') + .addColumn('lastAttempted', 'timestamptz') + .addColumn('attempts', 'integer', (col) => col.notNull().defaultTo(0)) + .addUniqueConstraint('record_push_event_unique_evt', [ + 'subjectUri', + 'eventType', + ]) + .execute() + await db.schema + .createIndex('record_push_event_did_type_idx') + .on('record_push_event') + .columns(['subjectDid', 'eventType']) + .execute() + await db.schema + .createIndex('record_push_confirmation_idx') + .on('record_push_event') + .columns(['confirmedAt', 'attempts']) + .execute() + + await db.schema + .createTable('blob_push_event') + .addColumn('id', 'serial', (col) => col.primaryKey()) + .addColumn('eventType', 'varchar', (col) => col.notNull()) + .addColumn('subjectDid', 'varchar', (col) => col.notNull()) + .addColumn('subjectBlobCid', 'varchar', (col) => col.notNull()) + .addColumn('subjectUri', 'varchar') + .addColumn('takedownRef', 'varchar') + .addColumn('confirmedAt', 'timestamptz') + .addColumn('lastAttempted', 'timestamptz') + .addColumn('attempts', 'integer', (col) => col.notNull().defaultTo(0)) + .addUniqueConstraint('blob_push_event_unique_evt', [ + 'subjectDid', + 'subjectBlobCid', + 'eventType', + ]) + .execute() + await db.schema + .createIndex('blob_push_confirmation_idx') + .on('blob_push_event') + .columns(['confirmedAt', 'attempts']) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('moderation_event').execute() + await db.schema.dropTable('moderation_subject_status').execute() + await db.schema.dropTable('label').execute() + await db.schema.dropTable('repo_push_event').execute() + await db.schema.dropTable('record_push_event').execute() + await db.schema.dropTable('blob_push_event').execute() +} diff --git a/packages/ozone/src/db/migrations/index.ts b/packages/ozone/src/db/migrations/index.ts new file mode 100644 index 00000000000..de73f4b118f --- /dev/null +++ b/packages/ozone/src/db/migrations/index.ts @@ -0,0 +1,5 @@ +// NOTE this file can be edited by hand, but it is also appended to by the migration:create command. +// 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 _20231219T205730722Z from './20231219T205730722Z-init' diff --git a/packages/ozone/src/db/migrations/provider.ts b/packages/ozone/src/db/migrations/provider.ts new file mode 100644 index 00000000000..f5e77eec871 --- /dev/null +++ b/packages/ozone/src/db/migrations/provider.ts @@ -0,0 +1,25 @@ +import { Kysely, Migration, MigrationProvider } from 'kysely' + +// Passes a context argument to migrations. We use this to thread the dialect into migrations + +export class CtxMigrationProvider implements MigrationProvider { + constructor( + private migrations: Record>, + private ctx: T, + ) {} + async getMigrations(): Promise> { + const ctxMigrations: Record = {} + Object.entries(this.migrations).forEach(([name, migration]) => { + ctxMigrations[name] = { + up: async (db) => await migration.up(db, this.ctx), + down: async (db) => await migration.down?.(db, this.ctx), + } + }) + return ctxMigrations + } +} + +export interface CtxMigration { + up(db: Kysely, ctx: T): Promise + down?(db: Kysely, ctx: T): Promise +} diff --git a/packages/ozone/src/db/pagination.ts b/packages/ozone/src/db/pagination.ts new file mode 100644 index 00000000000..672da3450dd --- /dev/null +++ b/packages/ozone/src/db/pagination.ts @@ -0,0 +1,216 @@ +import { sql, DynamicModule } from 'kysely' +import { InvalidRequestError } from '@atproto/xrpc-server' +import { AnyQb, DbRef } from './types' + +export type Cursor = { primary: string; secondary: string } +export type LabeledResult = { + primary: string | number + secondary: string | number +} + +/** + * The GenericKeyset is an abstract class that sets-up the interface and partial implementation + * of a keyset-paginated cursor with two parts. There are three types involved: + * - Result: a raw result (i.e. a row from the db) containing data that will make-up a cursor. + * - E.g. { createdAt: '2022-01-01T12:00:00Z', cid: 'bafyx' } + * - LabeledResult: a Result processed such that the "primary" and "secondary" parts of the cursor are labeled. + * - E.g. { primary: '2022-01-01T12:00:00Z', secondary: 'bafyx' } + * - Cursor: the two string parts that make-up the packed/string cursor. + * - E.g. packed cursor '1641038400000::bafyx' in parts { primary: '1641038400000', secondary: 'bafyx' } + * + * These types relate as such. Implementers define the relations marked with a *: + * Result -*-> LabeledResult <-*-> Cursor <--> packed/string cursor + * ↳ SQL Condition + */ +export abstract class GenericKeyset { + constructor(public primary: DbRef, public secondary: DbRef) {} + abstract labelResult(result: R): LR + abstract labeledResultToCursor(labeled: LR): Cursor + abstract cursorToLabeledResult(cursor: Cursor): LR + packFromResult(results: R | R[]): string | undefined { + const result = Array.isArray(results) ? results.at(-1) : results + if (!result) return + return this.pack(this.labelResult(result)) + } + pack(labeled?: LR): string | undefined { + if (!labeled) return + const cursor = this.labeledResultToCursor(labeled) + return this.packCursor(cursor) + } + unpack(cursorStr?: string): LR | undefined { + const cursor = this.unpackCursor(cursorStr) + if (!cursor) return + return this.cursorToLabeledResult(cursor) + } + packCursor(cursor?: Cursor): string | undefined { + if (!cursor) return + return `${cursor.primary}::${cursor.secondary}` + } + unpackCursor(cursorStr?: string): Cursor | undefined { + if (!cursorStr) return + const result = cursorStr.split('::') + const [primary, secondary, ...others] = result + if (!primary || !secondary || others.length > 0) { + throw new InvalidRequestError('Malformed cursor') + } + return { + primary, + secondary, + } + } + getSql(labeled?: LR, direction?: 'asc' | 'desc', tryIndex?: boolean) { + if (labeled === undefined) return + if (tryIndex) { + // The tryIndex param will likely disappear and become the default implementation: here for now for gradual rollout query-by-query. + if (direction === 'asc') { + return sql`((${this.primary}, ${this.secondary}) > (${labeled.primary}, ${labeled.secondary}))` + } else { + return sql`((${this.primary}, ${this.secondary}) < (${labeled.primary}, ${labeled.secondary}))` + } + } else { + // @NOTE this implementation can struggle to use an index on (primary, secondary) for pagination due to the "or" usage. + if (direction === 'asc') { + return sql`((${this.primary} > ${labeled.primary}) or (${this.primary} = ${labeled.primary} and ${this.secondary} > ${labeled.secondary}))` + } else { + return sql`((${this.primary} < ${labeled.primary}) or (${this.primary} = ${labeled.primary} and ${this.secondary} < ${labeled.secondary}))` + } + } + } +} + +type StatusKeysetParam = { + lastReviewedAt: string | null + lastReportedAt: string | null + id: number +} + +export class StatusKeyset extends GenericKeyset { + labelResult(result: StatusKeysetParam): Cursor + labelResult(result: StatusKeysetParam) { + const primaryField = ( + this.primary as ReturnType + ).dynamicReference.includes('lastReviewedAt') + ? 'lastReviewedAt' + : 'lastReportedAt' + + return { + primary: result[primaryField] + ? new Date(`${result[primaryField]}`).getTime().toString() + : '', + secondary: result.id.toString(), + } + } + labeledResultToCursor(labeled: Cursor) { + return { + primary: labeled.primary, + secondary: labeled.secondary, + } + } + cursorToLabeledResult(cursor: Cursor) { + return { + primary: cursor.primary + ? new Date(parseInt(cursor.primary, 10)).toISOString() + : '', + secondary: cursor.secondary, + } + } + unpackCursor(cursorStr?: string): Cursor | undefined { + if (!cursorStr) return + const result = cursorStr.split('::') + const [primary, secondary, ...others] = result + if (!secondary || others.length > 0) { + throw new InvalidRequestError('Malformed cursor') + } + return { + primary, + secondary, + } + } + // This is specifically built to handle nullable columns as primary sorting column + getSql(labeled?: Cursor, direction?: 'asc' | 'desc') { + if (labeled === undefined) return + if (direction === 'asc') { + return !labeled.primary + ? sql`(${this.primary} IS NULL AND ${this.secondary} > ${labeled.secondary})` + : sql`((${this.primary}, ${this.secondary}) > (${labeled.primary}, ${labeled.secondary}) OR (${this.primary} is null))` + } else { + return !labeled.primary + ? sql`(${this.primary} IS NULL AND ${this.secondary} < ${labeled.secondary})` + : sql`((${this.primary}, ${this.secondary}) < (${labeled.primary}, ${labeled.secondary}) OR (${this.primary} is null))` + } + } +} + +type TimeIdKeysetParam = { + id: number + createdAt: string +} +type TimeIdResult = TimeIdKeysetParam + +export class TimeIdKeyset extends GenericKeyset { + labelResult(result: TimeIdResult): Cursor + labelResult(result: TimeIdResult) { + return { primary: result.createdAt, secondary: result.id.toString() } + } + labeledResultToCursor(labeled: Cursor) { + return { + primary: new Date(labeled.primary).getTime().toString(), + secondary: labeled.secondary, + } + } + cursorToLabeledResult(cursor: Cursor) { + const primaryDate = new Date(parseInt(cursor.primary, 10)) + if (isNaN(primaryDate.getTime())) { + throw new InvalidRequestError('Malformed cursor') + } + return { + primary: primaryDate.toISOString(), + secondary: cursor.secondary, + } + } +} + +export const paginate = < + QB extends AnyQb, + K extends GenericKeyset, +>( + qb: QB, + opts: { + limit?: number + cursor?: string + direction?: 'asc' | 'desc' + keyset: K + tryIndex?: boolean + // By default, pg does nullsFirst + nullsLast?: boolean + }, +): QB => { + const { + limit, + cursor, + keyset, + direction = 'desc', + tryIndex, + nullsLast, + } = opts + const keysetSql = keyset.getSql(keyset.unpack(cursor), direction, tryIndex) + return qb + .if(!!limit, (q) => q.limit(limit as number)) + .if(!nullsLast, (q) => + q.orderBy(keyset.primary, direction).orderBy(keyset.secondary, direction), + ) + .if(!!nullsLast, (q) => + q + .orderBy( + direction === 'asc' + ? sql`${keyset.primary} asc nulls last` + : sql`${keyset.primary} desc nulls last`, + ) + .orderBy( + direction === 'asc' + ? sql`${keyset.secondary} asc nulls last` + : sql`${keyset.secondary} desc nulls last`, + ), + ) + .if(!!keysetSql, (qb) => (keysetSql ? qb.where(keysetSql) : qb)) as QB +} diff --git a/packages/ozone/src/db/schema/blob_push_event.ts b/packages/ozone/src/db/schema/blob_push_event.ts new file mode 100644 index 00000000000..f38649e675c --- /dev/null +++ b/packages/ozone/src/db/schema/blob_push_event.ts @@ -0,0 +1,21 @@ +import { Generated } from 'kysely' + +export const eventTableName = 'blob_push_event' + +export type BlobPushEventType = 'pds_takedown' | 'appview_takedown' + +export interface BlobPushEvent { + id: Generated + eventType: BlobPushEventType + subjectDid: string + subjectBlobCid: string + subjectUri: string | null + takedownRef: string | null + confirmedAt: Date | null + lastAttempted: Date | null + attempts: Generated +} + +export type PartialDB = { + [eventTableName]: BlobPushEvent +} diff --git a/packages/ozone/src/db/schema/index.ts b/packages/ozone/src/db/schema/index.ts new file mode 100644 index 00000000000..cee3124ea7c --- /dev/null +++ b/packages/ozone/src/db/schema/index.ts @@ -0,0 +1,18 @@ +import { Kysely } from 'kysely' +import * as modEvent from './moderation_event' +import * as modSubjectStatus from './moderation_subject_status' +import * as repoPushEvent from './repo_push_event' +import * as recordPushEvent from './record_push_event' +import * as blobPushEvent from './blob_push_event' +import * as label from './label' + +export type DatabaseSchemaType = modEvent.PartialDB & + modSubjectStatus.PartialDB & + label.PartialDB & + repoPushEvent.PartialDB & + recordPushEvent.PartialDB & + blobPushEvent.PartialDB + +export type DatabaseSchema = Kysely + +export default DatabaseSchema diff --git a/packages/ozone/src/db/schema/label.ts b/packages/ozone/src/db/schema/label.ts new file mode 100644 index 00000000000..0c8a398a7db --- /dev/null +++ b/packages/ozone/src/db/schema/label.ts @@ -0,0 +1,12 @@ +export const tableName = 'label' + +export interface Label { + src: string + uri: string + cid: string + val: string + neg: boolean + cts: string +} + +export type PartialDB = { [tableName]: Label } diff --git a/packages/ozone/src/db/schema/moderation_event.ts b/packages/ozone/src/db/schema/moderation_event.ts new file mode 100644 index 00000000000..0cf7d07c1e5 --- /dev/null +++ b/packages/ozone/src/db/schema/moderation_event.ts @@ -0,0 +1,35 @@ +import { Generated } from 'kysely' + +export const eventTableName = 'moderation_event' + +export interface ModerationEvent { + id: Generated + action: + | 'com.atproto.admin.defs#modEventTakedown' + | 'com.atproto.admin.defs#modEventAcknowledge' + | 'com.atproto.admin.defs#modEventEscalate' + | 'com.atproto.admin.defs#modEventComment' + | 'com.atproto.admin.defs#modEventLabel' + | 'com.atproto.admin.defs#modEventReport' + | 'com.atproto.admin.defs#modEventMute' + | 'com.atproto.admin.defs#modEventReverseTakedown' + | 'com.atproto.admin.defs#modEventEmail' + | 'com.atproto.admin.defs#modEventResolveAppeal' + subjectType: 'com.atproto.admin.defs#repoRef' | 'com.atproto.repo.strongRef' + subjectDid: string + subjectUri: string | null + subjectCid: string | null + createLabelVals: string | null + negateLabelVals: string | null + comment: string | null + createdAt: string + createdBy: string + durationInHours: number | null + expiresAt: string | null + meta: Record | null + legacyRefId: number | null +} + +export type PartialDB = { + [eventTableName]: ModerationEvent +} diff --git a/packages/ozone/src/db/schema/moderation_subject_status.ts b/packages/ozone/src/db/schema/moderation_subject_status.ts new file mode 100644 index 00000000000..6e67082f31c --- /dev/null +++ b/packages/ozone/src/db/schema/moderation_subject_status.ts @@ -0,0 +1,32 @@ +import { Generated } from 'kysely' +import { + REVIEWCLOSED, + REVIEWOPEN, + REVIEWESCALATED, +} from '../../lexicon/types/com/atproto/admin/defs' + +export const subjectStatusTableName = 'moderation_subject_status' + +export interface ModerationSubjectStatus { + id: Generated + did: string + recordPath: string + recordCid: string | null + blobCids: string[] | null + reviewState: typeof REVIEWCLOSED | typeof REVIEWOPEN | typeof REVIEWESCALATED + createdAt: string + updatedAt: string + lastReviewedBy: string | null + lastReviewedAt: string | null + lastReportedAt: string | null + lastAppealedAt: string | null + muteUntil: string | null + suspendUntil: string | null + takendown: boolean + appealed: boolean | null + comment: string | null +} + +export type PartialDB = { + [subjectStatusTableName]: ModerationSubjectStatus +} diff --git a/packages/ozone/src/db/schema/record_push_event.ts b/packages/ozone/src/db/schema/record_push_event.ts new file mode 100644 index 00000000000..05b24a7ac23 --- /dev/null +++ b/packages/ozone/src/db/schema/record_push_event.ts @@ -0,0 +1,21 @@ +import { Generated } from 'kysely' + +export const eventTableName = 'record_push_event' + +export type RecordPushEventType = 'pds_takedown' | 'appview_takedown' + +export interface RecordPushEvent { + id: Generated + eventType: RecordPushEventType + subjectDid: string + subjectUri: string + subjectCid: string + takedownRef: string | null + confirmedAt: Date | null + lastAttempted: Date | null + attempts: Generated +} + +export type PartialDB = { + [eventTableName]: RecordPushEvent +} diff --git a/packages/ozone/src/db/schema/repo_push_event.ts b/packages/ozone/src/db/schema/repo_push_event.ts new file mode 100644 index 00000000000..fcbca128108 --- /dev/null +++ b/packages/ozone/src/db/schema/repo_push_event.ts @@ -0,0 +1,19 @@ +import { Generated } from 'kysely' + +export const eventTableName = 'repo_push_event' + +export type RepoPushEventType = 'pds_takedown' | 'appview_takedown' + +export interface RepoPushEvent { + id: Generated + eventType: RepoPushEventType + subjectDid: string + takedownRef: string | null + confirmedAt: Date | null + lastAttempted: Date | null + attempts: Generated +} + +export type PartialDB = { + [eventTableName]: RepoPushEvent +} diff --git a/packages/ozone/src/db/types.ts b/packages/ozone/src/db/types.ts new file mode 100644 index 00000000000..c38271ee119 --- /dev/null +++ b/packages/ozone/src/db/types.ts @@ -0,0 +1,15 @@ +import { Pool as PgPool } from 'pg' +import { DynamicModule, RawBuilder, SelectQueryBuilder } from 'kysely' + +export type DbRef = RawBuilder | ReturnType + +export type AnyQb = SelectQueryBuilder + +export type PgOptions = { + url: string + pool?: PgPool + schema?: string + poolSize?: number + poolMaxUses?: number + poolIdleTimeoutMs?: number +} diff --git a/packages/ozone/src/error.ts b/packages/ozone/src/error.ts new file mode 100644 index 00000000000..a4de90f580e --- /dev/null +++ b/packages/ozone/src/error.ts @@ -0,0 +1,12 @@ +import { XRPCError } from '@atproto/xrpc-server' +import { ErrorRequestHandler } from 'express' +import { httpLogger as log } from './logger' + +export const handler: ErrorRequestHandler = (err, _req, res, next) => { + log.error(err, 'unexpected internal server error') + if (res.headersSent) { + return next(err) + } + const serverError = XRPCError.fromError(err) + res.status(serverError.type).json(serverError.payload) +} diff --git a/packages/ozone/src/index.ts b/packages/ozone/src/index.ts new file mode 100644 index 00000000000..9ab7c13a333 --- /dev/null +++ b/packages/ozone/src/index.ts @@ -0,0 +1,102 @@ +import express from 'express' +import http from 'http' +import { AddressInfo } from 'net' +import events from 'events' +import { createHttpTerminator, HttpTerminator } from 'http-terminator' +import cors from 'cors' +import compression from 'compression' +import API, { health, wellKnown } from './api' +import * as error from './error' +import { dbLogger, loggerMiddleware } from './logger' +import { OzoneConfig, OzoneSecrets } from './config' +import { createServer } from './lexicon' +import AppContext, { AppContextOptions } from './context' + +export * from './config' +export { Database } from './db' +export { OzoneDaemon, EventPusher, EventReverser } from './daemon' +export { AppContext } from './context' +export { httpLogger } from './logger' + +export class OzoneService { + public ctx: AppContext + public app: express.Application + public server?: http.Server + private terminator?: HttpTerminator + private dbStatsInterval: NodeJS.Timer + + constructor(opts: { ctx: AppContext; app: express.Application }) { + this.ctx = opts.ctx + this.app = opts.app + } + + static async create( + cfg: OzoneConfig, + secrets: OzoneSecrets, + overrides?: Partial, + ): Promise { + const app = express() + app.set('trust proxy', true) + app.use(cors()) + app.use(loggerMiddleware) + app.use(compression()) + + const ctx = await AppContext.fromConfig(cfg, secrets, overrides) + + let server = createServer({ + validateResponse: false, + payload: { + jsonLimit: 100 * 1024, // 100kb + textLimit: 100 * 1024, // 100kb + blobLimit: 5 * 1024 * 1024, // 5mb + }, + }) + + server = API(server, ctx) + + app.use(health.createRouter(ctx)) + app.use(wellKnown.createRouter(ctx)) + app.use(server.xrpc.router) + app.use(error.handler) + + return new OzoneService({ ctx, app }) + } + + async start(): Promise { + const { db, backgroundQueue } = this.ctx + this.dbStatsInterval = setInterval(() => { + dbLogger.info( + { + idleCount: db.pool.idleCount, + totalCount: db.pool.totalCount, + waitingCount: db.pool.waitingCount, + }, + 'db pool stats', + ) + dbLogger.info( + { + runningCount: backgroundQueue.queue.pending, + waitingCount: backgroundQueue.queue.size, + }, + 'background queue stats', + ) + }, 10000) + const server = this.app.listen(this.ctx.cfg.service.port) + this.server = server + server.keepAliveTimeout = 90000 + this.terminator = createHttpTerminator({ server }) + await events.once(server, 'listening') + const { port } = server.address() as AddressInfo + this.ctx.assignPort(port) + return server + } + + async destroy(): Promise { + await this.terminator?.terminate() + await this.ctx.backgroundQueue.destroy() + await this.ctx.db.close() + clearInterval(this.dbStatsInterval) + } +} + +export default OzoneService diff --git a/packages/ozone/src/lexicon/index.ts b/packages/ozone/src/lexicon/index.ts new file mode 100644 index 00000000000..386f77196e7 --- /dev/null +++ b/packages/ozone/src/lexicon/index.ts @@ -0,0 +1,1626 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { + createServer as createXrpcServer, + Server as XrpcServer, + Options as XrpcOptions, + AuthVerifier, + StreamAuthVerifier, +} from '@atproto/xrpc-server' +import { schemas } from './lexicons' +import * as ComAtprotoAdminDeleteAccount from './types/com/atproto/admin/deleteAccount' +import * as ComAtprotoAdminDisableAccountInvites from './types/com/atproto/admin/disableAccountInvites' +import * as ComAtprotoAdminDisableInviteCodes from './types/com/atproto/admin/disableInviteCodes' +import * as ComAtprotoAdminEmitModerationEvent from './types/com/atproto/admin/emitModerationEvent' +import * as ComAtprotoAdminEnableAccountInvites from './types/com/atproto/admin/enableAccountInvites' +import * as ComAtprotoAdminGetAccountInfo from './types/com/atproto/admin/getAccountInfo' +import * as ComAtprotoAdminGetAccountInfos from './types/com/atproto/admin/getAccountInfos' +import * as ComAtprotoAdminGetInviteCodes from './types/com/atproto/admin/getInviteCodes' +import * as ComAtprotoAdminGetModerationEvent from './types/com/atproto/admin/getModerationEvent' +import * as ComAtprotoAdminGetRecord from './types/com/atproto/admin/getRecord' +import * as ComAtprotoAdminGetRepo from './types/com/atproto/admin/getRepo' +import * as ComAtprotoAdminGetSubjectStatus from './types/com/atproto/admin/getSubjectStatus' +import * as ComAtprotoAdminQueryModerationEvents from './types/com/atproto/admin/queryModerationEvents' +import * as ComAtprotoAdminQueryModerationStatuses from './types/com/atproto/admin/queryModerationStatuses' +import * as ComAtprotoAdminSearchRepos from './types/com/atproto/admin/searchRepos' +import * as ComAtprotoAdminSendEmail from './types/com/atproto/admin/sendEmail' +import * as ComAtprotoAdminUpdateAccountEmail from './types/com/atproto/admin/updateAccountEmail' +import * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/updateAccountHandle' +import * as ComAtprotoAdminUpdateSubjectStatus from './types/com/atproto/admin/updateSubjectStatus' +import * as ComAtprotoIdentityResolveHandle from './types/com/atproto/identity/resolveHandle' +import * as ComAtprotoIdentityUpdateHandle from './types/com/atproto/identity/updateHandle' +import * as ComAtprotoLabelQueryLabels from './types/com/atproto/label/queryLabels' +import * as ComAtprotoLabelSubscribeLabels from './types/com/atproto/label/subscribeLabels' +import * as ComAtprotoModerationCreateReport from './types/com/atproto/moderation/createReport' +import * as ComAtprotoRepoApplyWrites from './types/com/atproto/repo/applyWrites' +import * as ComAtprotoRepoCreateRecord from './types/com/atproto/repo/createRecord' +import * as ComAtprotoRepoDeleteRecord from './types/com/atproto/repo/deleteRecord' +import * as ComAtprotoRepoDescribeRepo from './types/com/atproto/repo/describeRepo' +import * as ComAtprotoRepoGetRecord from './types/com/atproto/repo/getRecord' +import * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords' +import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord' +import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' +import * as ComAtprotoServerConfirmEmail from './types/com/atproto/server/confirmEmail' +import * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount' +import * as ComAtprotoServerCreateAppPassword from './types/com/atproto/server/createAppPassword' +import * as ComAtprotoServerCreateInviteCode from './types/com/atproto/server/createInviteCode' +import * as ComAtprotoServerCreateInviteCodes from './types/com/atproto/server/createInviteCodes' +import * as ComAtprotoServerCreateSession from './types/com/atproto/server/createSession' +import * as ComAtprotoServerDeleteAccount from './types/com/atproto/server/deleteAccount' +import * as ComAtprotoServerDeleteSession from './types/com/atproto/server/deleteSession' +import * as ComAtprotoServerDescribeServer from './types/com/atproto/server/describeServer' +import * as ComAtprotoServerGetAccountInviteCodes from './types/com/atproto/server/getAccountInviteCodes' +import * as ComAtprotoServerGetSession from './types/com/atproto/server/getSession' +import * as ComAtprotoServerListAppPasswords from './types/com/atproto/server/listAppPasswords' +import * as ComAtprotoServerRefreshSession from './types/com/atproto/server/refreshSession' +import * as ComAtprotoServerRequestAccountDelete from './types/com/atproto/server/requestAccountDelete' +import * as ComAtprotoServerRequestEmailConfirmation from './types/com/atproto/server/requestEmailConfirmation' +import * as ComAtprotoServerRequestEmailUpdate from './types/com/atproto/server/requestEmailUpdate' +import * as ComAtprotoServerRequestPasswordReset from './types/com/atproto/server/requestPasswordReset' +import * as ComAtprotoServerReserveSigningKey from './types/com/atproto/server/reserveSigningKey' +import * as ComAtprotoServerResetPassword from './types/com/atproto/server/resetPassword' +import * as ComAtprotoServerRevokeAppPassword from './types/com/atproto/server/revokeAppPassword' +import * as ComAtprotoServerUpdateEmail from './types/com/atproto/server/updateEmail' +import * as ComAtprotoSyncGetBlob from './types/com/atproto/sync/getBlob' +import * as ComAtprotoSyncGetBlocks from './types/com/atproto/sync/getBlocks' +import * as ComAtprotoSyncGetCheckout from './types/com/atproto/sync/getCheckout' +import * as ComAtprotoSyncGetHead from './types/com/atproto/sync/getHead' +import * as ComAtprotoSyncGetLatestCommit from './types/com/atproto/sync/getLatestCommit' +import * as ComAtprotoSyncGetRecord from './types/com/atproto/sync/getRecord' +import * as ComAtprotoSyncGetRepo from './types/com/atproto/sync/getRepo' +import * as ComAtprotoSyncListBlobs from './types/com/atproto/sync/listBlobs' +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 ComAtprotoTempFetchLabels from './types/com/atproto/temp/fetchLabels' +import * as ComAtprotoTempImportRepo from './types/com/atproto/temp/importRepo' +import * as ComAtprotoTempPushBlob from './types/com/atproto/temp/pushBlob' +import * as ComAtprotoTempTransferAccount from './types/com/atproto/temp/transferAccount' +import * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences' +import * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile' +import * as AppBskyActorGetProfiles from './types/app/bsky/actor/getProfiles' +import * as AppBskyActorGetSuggestions from './types/app/bsky/actor/getSuggestions' +import * as AppBskyActorPutPreferences from './types/app/bsky/actor/putPreferences' +import * as AppBskyActorSearchActors from './types/app/bsky/actor/searchActors' +import * as AppBskyActorSearchActorsTypeahead from './types/app/bsky/actor/searchActorsTypeahead' +import * as AppBskyFeedDescribeFeedGenerator from './types/app/bsky/feed/describeFeedGenerator' +import * as AppBskyFeedGetActorFeeds from './types/app/bsky/feed/getActorFeeds' +import * as AppBskyFeedGetActorLikes from './types/app/bsky/feed/getActorLikes' +import * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed' +import * as AppBskyFeedGetFeed from './types/app/bsky/feed/getFeed' +import * as AppBskyFeedGetFeedGenerator from './types/app/bsky/feed/getFeedGenerator' +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' +import * as AppBskyGraphGetList from './types/app/bsky/graph/getList' +import * as AppBskyGraphGetListBlocks from './types/app/bsky/graph/getListBlocks' +import * as AppBskyGraphGetListMutes from './types/app/bsky/graph/getListMutes' +import * as AppBskyGraphGetLists from './types/app/bsky/graph/getLists' +import * as AppBskyGraphGetMutes from './types/app/bsky/graph/getMutes' +import * as AppBskyGraphGetSuggestedFollowsByActor from './types/app/bsky/graph/getSuggestedFollowsByActor' +import * as AppBskyGraphMuteActor from './types/app/bsky/graph/muteActor' +import * as AppBskyGraphMuteActorList from './types/app/bsky/graph/muteActorList' +import * as AppBskyGraphUnmuteActor from './types/app/bsky/graph/unmuteActor' +import * as AppBskyGraphUnmuteActorList from './types/app/bsky/graph/unmuteActorList' +import * as AppBskyNotificationGetUnreadCount from './types/app/bsky/notification/getUnreadCount' +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 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 = { + DefsReviewOpen: 'com.atproto.admin.defs#reviewOpen', + DefsReviewEscalated: 'com.atproto.admin.defs#reviewEscalated', + DefsReviewClosed: 'com.atproto.admin.defs#reviewClosed', +} +export const COM_ATPROTO_MODERATION = { + DefsReasonSpam: 'com.atproto.moderation.defs#reasonSpam', + DefsReasonViolation: 'com.atproto.moderation.defs#reasonViolation', + DefsReasonMisleading: 'com.atproto.moderation.defs#reasonMisleading', + DefsReasonSexual: 'com.atproto.moderation.defs#reasonSexual', + DefsReasonRude: 'com.atproto.moderation.defs#reasonRude', + DefsReasonOther: 'com.atproto.moderation.defs#reasonOther', + DefsReasonAppeal: 'com.atproto.moderation.defs#reasonAppeal', +} +export const APP_BSKY_GRAPH = { + DefsModlist: 'app.bsky.graph.defs#modlist', + DefsCuratelist: 'app.bsky.graph.defs#curatelist', +} + +export function createServer(options?: XrpcOptions): Server { + return new Server(options) +} + +export class Server { + xrpc: XrpcServer + com: ComNS + app: AppNS + + constructor(options?: XrpcOptions) { + this.xrpc = createXrpcServer(schemas, options) + this.com = new ComNS(this) + this.app = new AppNS(this) + } +} + +export class ComNS { + _server: Server + atproto: AtprotoNS + + constructor(server: Server) { + this._server = server + this.atproto = new AtprotoNS(server) + } +} + +export class AtprotoNS { + _server: Server + admin: AdminNS + identity: IdentityNS + label: LabelNS + moderation: ModerationNS + repo: RepoNS + server: ServerNS + sync: SyncNS + temp: TempNS + + constructor(server: Server) { + this._server = server + this.admin = new AdminNS(server) + this.identity = new IdentityNS(server) + this.label = new LabelNS(server) + this.moderation = new ModerationNS(server) + this.repo = new RepoNS(server) + this.server = new ServerNS(server) + this.sync = new SyncNS(server) + this.temp = new TempNS(server) + } +} + +export class AdminNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + deleteAccount( + cfg: ConfigOf< + AV, + ComAtprotoAdminDeleteAccount.Handler>, + ComAtprotoAdminDeleteAccount.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.deleteAccount' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + disableAccountInvites( + cfg: ConfigOf< + AV, + ComAtprotoAdminDisableAccountInvites.Handler>, + ComAtprotoAdminDisableAccountInvites.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.disableAccountInvites' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + disableInviteCodes( + cfg: ConfigOf< + AV, + ComAtprotoAdminDisableInviteCodes.Handler>, + ComAtprotoAdminDisableInviteCodes.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.disableInviteCodes' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + emitModerationEvent( + cfg: ConfigOf< + AV, + ComAtprotoAdminEmitModerationEvent.Handler>, + ComAtprotoAdminEmitModerationEvent.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.emitModerationEvent' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + enableAccountInvites( + cfg: ConfigOf< + AV, + ComAtprotoAdminEnableAccountInvites.Handler>, + ComAtprotoAdminEnableAccountInvites.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.enableAccountInvites' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getAccountInfo( + cfg: ConfigOf< + AV, + ComAtprotoAdminGetAccountInfo.Handler>, + ComAtprotoAdminGetAccountInfo.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.getAccountInfo' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getAccountInfos( + cfg: ConfigOf< + AV, + ComAtprotoAdminGetAccountInfos.Handler>, + ComAtprotoAdminGetAccountInfos.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.getAccountInfos' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getInviteCodes( + cfg: ConfigOf< + AV, + ComAtprotoAdminGetInviteCodes.Handler>, + ComAtprotoAdminGetInviteCodes.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.getInviteCodes' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getModerationEvent( + cfg: ConfigOf< + AV, + ComAtprotoAdminGetModerationEvent.Handler>, + ComAtprotoAdminGetModerationEvent.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.getModerationEvent' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getRecord( + cfg: ConfigOf< + AV, + ComAtprotoAdminGetRecord.Handler>, + ComAtprotoAdminGetRecord.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.getRecord' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getRepo( + cfg: ConfigOf< + AV, + ComAtprotoAdminGetRepo.Handler>, + ComAtprotoAdminGetRepo.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.getRepo' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getSubjectStatus( + cfg: ConfigOf< + AV, + ComAtprotoAdminGetSubjectStatus.Handler>, + ComAtprotoAdminGetSubjectStatus.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.getSubjectStatus' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + queryModerationEvents( + cfg: ConfigOf< + AV, + ComAtprotoAdminQueryModerationEvents.Handler>, + ComAtprotoAdminQueryModerationEvents.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.queryModerationEvents' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + queryModerationStatuses( + cfg: ConfigOf< + AV, + ComAtprotoAdminQueryModerationStatuses.Handler>, + ComAtprotoAdminQueryModerationStatuses.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.queryModerationStatuses' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + searchRepos( + cfg: ConfigOf< + AV, + ComAtprotoAdminSearchRepos.Handler>, + ComAtprotoAdminSearchRepos.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.searchRepos' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + sendEmail( + cfg: ConfigOf< + AV, + ComAtprotoAdminSendEmail.Handler>, + ComAtprotoAdminSendEmail.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.sendEmail' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + updateAccountEmail( + cfg: ConfigOf< + AV, + ComAtprotoAdminUpdateAccountEmail.Handler>, + ComAtprotoAdminUpdateAccountEmail.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.updateAccountEmail' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + updateAccountHandle( + cfg: ConfigOf< + AV, + ComAtprotoAdminUpdateAccountHandle.Handler>, + ComAtprotoAdminUpdateAccountHandle.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.updateAccountHandle' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + updateSubjectStatus( + cfg: ConfigOf< + AV, + ComAtprotoAdminUpdateSubjectStatus.Handler>, + ComAtprotoAdminUpdateSubjectStatus.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.updateSubjectStatus' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + +export class IdentityNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + resolveHandle( + cfg: ConfigOf< + AV, + ComAtprotoIdentityResolveHandle.Handler>, + ComAtprotoIdentityResolveHandle.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.identity.resolveHandle' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + updateHandle( + cfg: ConfigOf< + AV, + ComAtprotoIdentityUpdateHandle.Handler>, + ComAtprotoIdentityUpdateHandle.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.identity.updateHandle' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + +export class LabelNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + queryLabels( + cfg: ConfigOf< + AV, + ComAtprotoLabelQueryLabels.Handler>, + ComAtprotoLabelQueryLabels.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.label.queryLabels' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + subscribeLabels( + cfg: ConfigOf< + AV, + ComAtprotoLabelSubscribeLabels.Handler>, + ComAtprotoLabelSubscribeLabels.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.label.subscribeLabels' // @ts-ignore + return this._server.xrpc.streamMethod(nsid, cfg) + } +} + +export class ModerationNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + createReport( + cfg: ConfigOf< + AV, + ComAtprotoModerationCreateReport.Handler>, + ComAtprotoModerationCreateReport.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.moderation.createReport' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + +export class RepoNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + applyWrites( + cfg: ConfigOf< + AV, + ComAtprotoRepoApplyWrites.Handler>, + ComAtprotoRepoApplyWrites.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.repo.applyWrites' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + createRecord( + cfg: ConfigOf< + AV, + ComAtprotoRepoCreateRecord.Handler>, + ComAtprotoRepoCreateRecord.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.repo.createRecord' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + deleteRecord( + cfg: ConfigOf< + AV, + ComAtprotoRepoDeleteRecord.Handler>, + ComAtprotoRepoDeleteRecord.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.repo.deleteRecord' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + describeRepo( + cfg: ConfigOf< + AV, + ComAtprotoRepoDescribeRepo.Handler>, + ComAtprotoRepoDescribeRepo.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.repo.describeRepo' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getRecord( + cfg: ConfigOf< + AV, + ComAtprotoRepoGetRecord.Handler>, + ComAtprotoRepoGetRecord.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.repo.getRecord' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + listRecords( + cfg: ConfigOf< + AV, + ComAtprotoRepoListRecords.Handler>, + ComAtprotoRepoListRecords.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.repo.listRecords' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + putRecord( + cfg: ConfigOf< + AV, + ComAtprotoRepoPutRecord.Handler>, + ComAtprotoRepoPutRecord.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.repo.putRecord' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + uploadBlob( + cfg: ConfigOf< + AV, + ComAtprotoRepoUploadBlob.Handler>, + ComAtprotoRepoUploadBlob.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.repo.uploadBlob' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + +export class ServerNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + confirmEmail( + cfg: ConfigOf< + AV, + ComAtprotoServerConfirmEmail.Handler>, + ComAtprotoServerConfirmEmail.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.confirmEmail' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + createAccount( + cfg: ConfigOf< + AV, + ComAtprotoServerCreateAccount.Handler>, + ComAtprotoServerCreateAccount.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.createAccount' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + createAppPassword( + cfg: ConfigOf< + AV, + ComAtprotoServerCreateAppPassword.Handler>, + ComAtprotoServerCreateAppPassword.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.createAppPassword' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + createInviteCode( + cfg: ConfigOf< + AV, + ComAtprotoServerCreateInviteCode.Handler>, + ComAtprotoServerCreateInviteCode.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.createInviteCode' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + createInviteCodes( + cfg: ConfigOf< + AV, + ComAtprotoServerCreateInviteCodes.Handler>, + ComAtprotoServerCreateInviteCodes.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.createInviteCodes' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + createSession( + cfg: ConfigOf< + AV, + ComAtprotoServerCreateSession.Handler>, + ComAtprotoServerCreateSession.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.createSession' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + deleteAccount( + cfg: ConfigOf< + AV, + ComAtprotoServerDeleteAccount.Handler>, + ComAtprotoServerDeleteAccount.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.deleteAccount' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + deleteSession( + cfg: ConfigOf< + AV, + ComAtprotoServerDeleteSession.Handler>, + ComAtprotoServerDeleteSession.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.deleteSession' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + describeServer( + cfg: ConfigOf< + AV, + ComAtprotoServerDescribeServer.Handler>, + ComAtprotoServerDescribeServer.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.describeServer' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getAccountInviteCodes( + cfg: ConfigOf< + AV, + ComAtprotoServerGetAccountInviteCodes.Handler>, + ComAtprotoServerGetAccountInviteCodes.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.getAccountInviteCodes' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getSession( + cfg: ConfigOf< + AV, + ComAtprotoServerGetSession.Handler>, + ComAtprotoServerGetSession.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.getSession' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + listAppPasswords( + cfg: ConfigOf< + AV, + ComAtprotoServerListAppPasswords.Handler>, + ComAtprotoServerListAppPasswords.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.listAppPasswords' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + refreshSession( + cfg: ConfigOf< + AV, + ComAtprotoServerRefreshSession.Handler>, + ComAtprotoServerRefreshSession.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.refreshSession' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + requestAccountDelete( + cfg: ConfigOf< + AV, + ComAtprotoServerRequestAccountDelete.Handler>, + ComAtprotoServerRequestAccountDelete.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.requestAccountDelete' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + requestEmailConfirmation( + cfg: ConfigOf< + AV, + ComAtprotoServerRequestEmailConfirmation.Handler>, + ComAtprotoServerRequestEmailConfirmation.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.requestEmailConfirmation' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + requestEmailUpdate( + cfg: ConfigOf< + AV, + ComAtprotoServerRequestEmailUpdate.Handler>, + ComAtprotoServerRequestEmailUpdate.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.requestEmailUpdate' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + requestPasswordReset( + cfg: ConfigOf< + AV, + ComAtprotoServerRequestPasswordReset.Handler>, + ComAtprotoServerRequestPasswordReset.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.requestPasswordReset' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + reserveSigningKey( + cfg: ConfigOf< + AV, + ComAtprotoServerReserveSigningKey.Handler>, + ComAtprotoServerReserveSigningKey.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.reserveSigningKey' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + resetPassword( + cfg: ConfigOf< + AV, + ComAtprotoServerResetPassword.Handler>, + ComAtprotoServerResetPassword.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.resetPassword' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + revokeAppPassword( + cfg: ConfigOf< + AV, + ComAtprotoServerRevokeAppPassword.Handler>, + ComAtprotoServerRevokeAppPassword.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.revokeAppPassword' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + updateEmail( + cfg: ConfigOf< + AV, + ComAtprotoServerUpdateEmail.Handler>, + ComAtprotoServerUpdateEmail.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.server.updateEmail' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + +export class SyncNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + getBlob( + cfg: ConfigOf< + AV, + ComAtprotoSyncGetBlob.Handler>, + ComAtprotoSyncGetBlob.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.sync.getBlob' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getBlocks( + cfg: ConfigOf< + AV, + ComAtprotoSyncGetBlocks.Handler>, + ComAtprotoSyncGetBlocks.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.sync.getBlocks' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getCheckout( + cfg: ConfigOf< + AV, + ComAtprotoSyncGetCheckout.Handler>, + ComAtprotoSyncGetCheckout.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.sync.getCheckout' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getHead( + cfg: ConfigOf< + AV, + ComAtprotoSyncGetHead.Handler>, + ComAtprotoSyncGetHead.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.sync.getHead' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getLatestCommit( + cfg: ConfigOf< + AV, + ComAtprotoSyncGetLatestCommit.Handler>, + ComAtprotoSyncGetLatestCommit.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.sync.getLatestCommit' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getRecord( + cfg: ConfigOf< + AV, + ComAtprotoSyncGetRecord.Handler>, + ComAtprotoSyncGetRecord.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.sync.getRecord' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getRepo( + cfg: ConfigOf< + AV, + ComAtprotoSyncGetRepo.Handler>, + ComAtprotoSyncGetRepo.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.sync.getRepo' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + listBlobs( + cfg: ConfigOf< + AV, + ComAtprotoSyncListBlobs.Handler>, + ComAtprotoSyncListBlobs.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.sync.listBlobs' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + listRepos( + cfg: ConfigOf< + AV, + ComAtprotoSyncListRepos.Handler>, + ComAtprotoSyncListRepos.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.sync.listRepos' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + notifyOfUpdate( + cfg: ConfigOf< + AV, + ComAtprotoSyncNotifyOfUpdate.Handler>, + ComAtprotoSyncNotifyOfUpdate.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.sync.notifyOfUpdate' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + requestCrawl( + cfg: ConfigOf< + AV, + ComAtprotoSyncRequestCrawl.Handler>, + ComAtprotoSyncRequestCrawl.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.sync.requestCrawl' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + subscribeRepos( + cfg: ConfigOf< + AV, + ComAtprotoSyncSubscribeRepos.Handler>, + ComAtprotoSyncSubscribeRepos.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.sync.subscribeRepos' // @ts-ignore + return this._server.xrpc.streamMethod(nsid, cfg) + } +} + +export class TempNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + fetchLabels( + cfg: ConfigOf< + AV, + ComAtprotoTempFetchLabels.Handler>, + ComAtprotoTempFetchLabels.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.temp.fetchLabels' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + importRepo( + cfg: ConfigOf< + AV, + ComAtprotoTempImportRepo.Handler>, + ComAtprotoTempImportRepo.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.temp.importRepo' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + pushBlob( + cfg: ConfigOf< + AV, + ComAtprotoTempPushBlob.Handler>, + ComAtprotoTempPushBlob.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.temp.pushBlob' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + transferAccount( + cfg: ConfigOf< + AV, + ComAtprotoTempTransferAccount.Handler>, + ComAtprotoTempTransferAccount.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.temp.transferAccount' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + +export class AppNS { + _server: Server + bsky: BskyNS + + constructor(server: Server) { + this._server = server + this.bsky = new BskyNS(server) + } +} + +export class BskyNS { + _server: Server + actor: ActorNS + embed: EmbedNS + feed: FeedNS + graph: GraphNS + notification: NotificationNS + richtext: RichtextNS + unspecced: UnspeccedNS + + constructor(server: Server) { + this._server = server + this.actor = new ActorNS(server) + this.embed = new EmbedNS(server) + this.feed = new FeedNS(server) + this.graph = new GraphNS(server) + this.notification = new NotificationNS(server) + this.richtext = new RichtextNS(server) + this.unspecced = new UnspeccedNS(server) + } +} + +export class ActorNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + getPreferences( + cfg: ConfigOf< + AV, + AppBskyActorGetPreferences.Handler>, + AppBskyActorGetPreferences.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.actor.getPreferences' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getProfile( + cfg: ConfigOf< + AV, + AppBskyActorGetProfile.Handler>, + AppBskyActorGetProfile.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.actor.getProfile' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getProfiles( + cfg: ConfigOf< + AV, + AppBskyActorGetProfiles.Handler>, + AppBskyActorGetProfiles.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.actor.getProfiles' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getSuggestions( + cfg: ConfigOf< + AV, + AppBskyActorGetSuggestions.Handler>, + AppBskyActorGetSuggestions.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.actor.getSuggestions' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + putPreferences( + cfg: ConfigOf< + AV, + AppBskyActorPutPreferences.Handler>, + AppBskyActorPutPreferences.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.actor.putPreferences' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + searchActors( + cfg: ConfigOf< + AV, + AppBskyActorSearchActors.Handler>, + AppBskyActorSearchActors.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.actor.searchActors' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + searchActorsTypeahead( + cfg: ConfigOf< + AV, + AppBskyActorSearchActorsTypeahead.Handler>, + AppBskyActorSearchActorsTypeahead.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.actor.searchActorsTypeahead' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + +export class EmbedNS { + _server: Server + + constructor(server: Server) { + this._server = server + } +} + +export class FeedNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + describeFeedGenerator( + cfg: ConfigOf< + AV, + AppBskyFeedDescribeFeedGenerator.Handler>, + AppBskyFeedDescribeFeedGenerator.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.feed.describeFeedGenerator' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getActorFeeds( + cfg: ConfigOf< + AV, + AppBskyFeedGetActorFeeds.Handler>, + AppBskyFeedGetActorFeeds.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.feed.getActorFeeds' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getActorLikes( + cfg: ConfigOf< + AV, + AppBskyFeedGetActorLikes.Handler>, + AppBskyFeedGetActorLikes.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.feed.getActorLikes' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getAuthorFeed( + cfg: ConfigOf< + AV, + AppBskyFeedGetAuthorFeed.Handler>, + AppBskyFeedGetAuthorFeed.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.feed.getAuthorFeed' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getFeed( + cfg: ConfigOf< + AV, + AppBskyFeedGetFeed.Handler>, + AppBskyFeedGetFeed.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.feed.getFeed' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getFeedGenerator( + cfg: ConfigOf< + AV, + AppBskyFeedGetFeedGenerator.Handler>, + AppBskyFeedGetFeedGenerator.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.feed.getFeedGenerator' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getFeedGenerators( + cfg: ConfigOf< + AV, + AppBskyFeedGetFeedGenerators.Handler>, + AppBskyFeedGetFeedGenerators.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.feed.getFeedGenerators' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getFeedSkeleton( + cfg: ConfigOf< + AV, + AppBskyFeedGetFeedSkeleton.Handler>, + AppBskyFeedGetFeedSkeleton.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.feed.getFeedSkeleton' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getLikes( + cfg: ConfigOf< + AV, + AppBskyFeedGetLikes.Handler>, + AppBskyFeedGetLikes.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.feed.getLikes' // @ts-ignore + 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, + AppBskyFeedGetPostThread.Handler>, + AppBskyFeedGetPostThread.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.feed.getPostThread' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getPosts( + cfg: ConfigOf< + AV, + AppBskyFeedGetPosts.Handler>, + AppBskyFeedGetPosts.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.feed.getPosts' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getRepostedBy( + cfg: ConfigOf< + AV, + AppBskyFeedGetRepostedBy.Handler>, + AppBskyFeedGetRepostedBy.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.feed.getRepostedBy' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getSuggestedFeeds( + cfg: ConfigOf< + AV, + AppBskyFeedGetSuggestedFeeds.Handler>, + AppBskyFeedGetSuggestedFeeds.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.feed.getSuggestedFeeds' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getTimeline( + cfg: ConfigOf< + AV, + AppBskyFeedGetTimeline.Handler>, + AppBskyFeedGetTimeline.HandlerReqCtx> + >, + ) { + 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 { + _server: Server + + constructor(server: Server) { + this._server = server + } + + getBlocks( + cfg: ConfigOf< + AV, + AppBskyGraphGetBlocks.Handler>, + AppBskyGraphGetBlocks.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.graph.getBlocks' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getFollowers( + cfg: ConfigOf< + AV, + AppBskyGraphGetFollowers.Handler>, + AppBskyGraphGetFollowers.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.graph.getFollowers' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getFollows( + cfg: ConfigOf< + AV, + AppBskyGraphGetFollows.Handler>, + AppBskyGraphGetFollows.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.graph.getFollows' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getList( + cfg: ConfigOf< + AV, + AppBskyGraphGetList.Handler>, + AppBskyGraphGetList.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.graph.getList' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getListBlocks( + cfg: ConfigOf< + AV, + AppBskyGraphGetListBlocks.Handler>, + AppBskyGraphGetListBlocks.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.graph.getListBlocks' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getListMutes( + cfg: ConfigOf< + AV, + AppBskyGraphGetListMutes.Handler>, + AppBskyGraphGetListMutes.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.graph.getListMutes' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getLists( + cfg: ConfigOf< + AV, + AppBskyGraphGetLists.Handler>, + AppBskyGraphGetLists.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.graph.getLists' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getMutes( + cfg: ConfigOf< + AV, + AppBskyGraphGetMutes.Handler>, + AppBskyGraphGetMutes.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.graph.getMutes' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getSuggestedFollowsByActor( + cfg: ConfigOf< + AV, + AppBskyGraphGetSuggestedFollowsByActor.Handler>, + AppBskyGraphGetSuggestedFollowsByActor.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.graph.getSuggestedFollowsByActor' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + muteActor( + cfg: ConfigOf< + AV, + AppBskyGraphMuteActor.Handler>, + AppBskyGraphMuteActor.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.graph.muteActor' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + muteActorList( + cfg: ConfigOf< + AV, + AppBskyGraphMuteActorList.Handler>, + AppBskyGraphMuteActorList.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.graph.muteActorList' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + unmuteActor( + cfg: ConfigOf< + AV, + AppBskyGraphUnmuteActor.Handler>, + AppBskyGraphUnmuteActor.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.graph.unmuteActor' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + unmuteActorList( + cfg: ConfigOf< + AV, + AppBskyGraphUnmuteActorList.Handler>, + AppBskyGraphUnmuteActorList.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.graph.unmuteActorList' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + +export class NotificationNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + getUnreadCount( + cfg: ConfigOf< + AV, + AppBskyNotificationGetUnreadCount.Handler>, + AppBskyNotificationGetUnreadCount.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.notification.getUnreadCount' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + listNotifications( + cfg: ConfigOf< + AV, + AppBskyNotificationListNotifications.Handler>, + AppBskyNotificationListNotifications.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.notification.listNotifications' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + registerPush( + cfg: ConfigOf< + AV, + AppBskyNotificationRegisterPush.Handler>, + AppBskyNotificationRegisterPush.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.notification.registerPush' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + updateSeen( + cfg: ConfigOf< + AV, + AppBskyNotificationUpdateSeen.Handler>, + AppBskyNotificationUpdateSeen.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.notification.updateSeen' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + +export class RichtextNS { + _server: Server + + constructor(server: Server) { + this._server = server + } +} + +export class UnspeccedNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + getPopular( + cfg: ConfigOf< + AV, + AppBskyUnspeccedGetPopular.Handler>, + AppBskyUnspeccedGetPopular.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.unspecced.getPopular' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getPopularFeedGenerators( + cfg: ConfigOf< + AV, + AppBskyUnspeccedGetPopularFeedGenerators.Handler>, + AppBskyUnspeccedGetPopularFeedGenerators.HandlerReqCtx> + >, + ) { + const nsid = 'app.bsky.unspecced.getPopularFeedGenerators' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getTimelineSkeleton( + cfg: ConfigOf< + AV, + AppBskyUnspeccedGetTimelineSkeleton.Handler>, + AppBskyUnspeccedGetTimelineSkeleton.HandlerReqCtx> + >, + ) { + 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 = { + name: string + calcKey?: (ctx: T) => string + calcPoints?: (ctx: T) => number +} +type RouteRateLimitOpts = { + durationMs: number + points: number + calcKey?: (ctx: T) => string + calcPoints?: (ctx: T) => number +} +type HandlerOpts = { blobLimit?: number } +type HandlerRateLimitOpts = SharedRateLimitOpts | RouteRateLimitOpts +type ConfigOf = + | Handler + | { + auth?: Auth + opts?: HandlerOpts + rateLimit?: HandlerRateLimitOpts | HandlerRateLimitOpts[] + handler: Handler + } +type ExtractAuth = Extract< + Awaited>, + { credentials: unknown } +> diff --git a/packages/ozone/src/lexicon/lexicons.ts b/packages/ozone/src/lexicon/lexicons.ts new file mode 100644 index 00000000000..258d297c69e --- /dev/null +++ b/packages/ozone/src/lexicon/lexicons.ts @@ -0,0 +1,8065 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { LexiconDoc, Lexicons } from '@atproto/lexicon' + +export const schemaDict = { + ComAtprotoAdminDefs: { + lexicon: 1, + id: 'com.atproto.admin.defs', + defs: { + statusAttr: { + type: 'object', + required: ['applied'], + properties: { + applied: { + type: 'boolean', + }, + ref: { + type: 'string', + }, + }, + }, + modEventView: { + type: 'object', + required: [ + 'id', + 'event', + 'subject', + 'subjectBlobCids', + 'createdBy', + 'createdAt', + ], + properties: { + id: { + type: 'integer', + }, + event: { + type: 'union', + refs: [ + 'lex:com.atproto.admin.defs#modEventTakedown', + 'lex:com.atproto.admin.defs#modEventReverseTakedown', + 'lex:com.atproto.admin.defs#modEventComment', + 'lex:com.atproto.admin.defs#modEventReport', + 'lex:com.atproto.admin.defs#modEventLabel', + 'lex:com.atproto.admin.defs#modEventAcknowledge', + 'lex:com.atproto.admin.defs#modEventEscalate', + 'lex:com.atproto.admin.defs#modEventMute', + 'lex:com.atproto.admin.defs#modEventEmail', + ], + }, + subject: { + type: 'union', + refs: [ + 'lex:com.atproto.admin.defs#repoRef', + 'lex:com.atproto.repo.strongRef', + ], + }, + subjectBlobCids: { + type: 'array', + items: { + type: 'string', + }, + }, + createdBy: { + type: 'string', + format: 'did', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + creatorHandle: { + type: 'string', + }, + subjectHandle: { + type: 'string', + }, + }, + }, + modEventViewDetail: { + type: 'object', + required: [ + 'id', + 'event', + 'subject', + 'subjectBlobs', + 'createdBy', + 'createdAt', + ], + properties: { + id: { + type: 'integer', + }, + event: { + type: 'union', + refs: [ + 'lex:com.atproto.admin.defs#modEventTakedown', + 'lex:com.atproto.admin.defs#modEventReverseTakedown', + 'lex:com.atproto.admin.defs#modEventComment', + 'lex:com.atproto.admin.defs#modEventReport', + 'lex:com.atproto.admin.defs#modEventLabel', + 'lex:com.atproto.admin.defs#modEventAcknowledge', + 'lex:com.atproto.admin.defs#modEventEscalate', + 'lex:com.atproto.admin.defs#modEventMute', + 'lex:com.atproto.admin.defs#modEventResolveAppeal', + ], + }, + subject: { + type: 'union', + refs: [ + 'lex:com.atproto.admin.defs#repoView', + 'lex:com.atproto.admin.defs#repoViewNotFound', + 'lex:com.atproto.admin.defs#recordView', + 'lex:com.atproto.admin.defs#recordViewNotFound', + ], + }, + subjectBlobs: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#blobView', + }, + }, + createdBy: { + type: 'string', + format: 'did', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + reportView: { + type: 'object', + required: [ + 'id', + 'reasonType', + 'subject', + 'reportedBy', + 'createdAt', + 'resolvedByActionIds', + ], + properties: { + id: { + type: 'integer', + }, + reasonType: { + type: 'ref', + ref: 'lex:com.atproto.moderation.defs#reasonType', + }, + comment: { + type: 'string', + }, + subjectRepoHandle: { + type: 'string', + }, + subject: { + type: 'union', + refs: [ + 'lex:com.atproto.admin.defs#repoRef', + 'lex:com.atproto.repo.strongRef', + ], + }, + reportedBy: { + type: 'string', + format: 'did', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + resolvedByActionIds: { + type: 'array', + items: { + type: 'integer', + }, + }, + }, + }, + subjectStatusView: { + type: 'object', + required: ['id', 'subject', 'createdAt', 'updatedAt', 'reviewState'], + properties: { + id: { + type: 'integer', + }, + subject: { + type: 'union', + refs: [ + 'lex:com.atproto.admin.defs#repoRef', + 'lex:com.atproto.repo.strongRef', + ], + }, + subjectBlobCids: { + type: 'array', + items: { + type: 'string', + format: 'cid', + }, + }, + subjectRepoHandle: { + type: 'string', + }, + updatedAt: { + type: 'string', + format: 'datetime', + description: + 'Timestamp referencing when the last update was made to the moderation status of the subject', + }, + createdAt: { + type: 'string', + format: 'datetime', + description: + 'Timestamp referencing the first moderation status impacting event was emitted on the subject', + }, + reviewState: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#subjectReviewState', + }, + comment: { + type: 'string', + description: 'Sticky comment on the subject.', + }, + muteUntil: { + type: 'string', + format: 'datetime', + }, + lastReviewedBy: { + type: 'string', + format: 'did', + }, + lastReviewedAt: { + type: 'string', + format: 'datetime', + }, + lastReportedAt: { + type: 'string', + format: 'datetime', + }, + lastAppealedAt: { + type: 'string', + format: 'datetime', + description: + 'Timestamp referencing when the author of the subject appealed a moderation action', + }, + takendown: { + type: 'boolean', + }, + appealed: { + type: 'boolean', + description: + 'True indicates that the a previously taken moderator action was appealed against, by the author of the content. False indicates last appeal was resolved by moderators.', + }, + suspendUntil: { + type: 'string', + format: 'datetime', + }, + }, + }, + reportViewDetail: { + type: 'object', + required: [ + 'id', + 'reasonType', + 'subject', + 'reportedBy', + 'createdAt', + 'resolvedByActions', + ], + properties: { + id: { + type: 'integer', + }, + reasonType: { + type: 'ref', + ref: 'lex:com.atproto.moderation.defs#reasonType', + }, + comment: { + type: 'string', + }, + subject: { + type: 'union', + refs: [ + 'lex:com.atproto.admin.defs#repoView', + 'lex:com.atproto.admin.defs#repoViewNotFound', + 'lex:com.atproto.admin.defs#recordView', + 'lex:com.atproto.admin.defs#recordViewNotFound', + ], + }, + subjectStatus: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#subjectStatusView', + }, + reportedBy: { + type: 'string', + format: 'did', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + resolvedByActions: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#modEventView', + }, + }, + }, + }, + repoView: { + type: 'object', + required: [ + 'did', + 'handle', + 'relatedRecords', + 'indexedAt', + 'moderation', + ], + properties: { + did: { + type: 'string', + format: 'did', + }, + handle: { + type: 'string', + format: 'handle', + }, + email: { + type: 'string', + }, + relatedRecords: { + type: 'array', + items: { + type: 'unknown', + }, + }, + indexedAt: { + type: 'string', + format: 'datetime', + }, + moderation: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#moderation', + }, + invitedBy: { + type: 'ref', + ref: 'lex:com.atproto.server.defs#inviteCode', + }, + invitesDisabled: { + type: 'boolean', + }, + inviteNote: { + type: 'string', + }, + }, + }, + repoViewDetail: { + type: 'object', + required: [ + 'did', + 'handle', + 'relatedRecords', + 'indexedAt', + 'moderation', + ], + properties: { + did: { + type: 'string', + format: 'did', + }, + handle: { + type: 'string', + format: 'handle', + }, + email: { + type: 'string', + }, + relatedRecords: { + type: 'array', + items: { + type: 'unknown', + }, + }, + indexedAt: { + type: 'string', + format: 'datetime', + }, + moderation: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#moderationDetail', + }, + labels: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.label.defs#label', + }, + }, + invitedBy: { + type: 'ref', + ref: 'lex:com.atproto.server.defs#inviteCode', + }, + invites: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.server.defs#inviteCode', + }, + }, + invitesDisabled: { + type: 'boolean', + }, + inviteNote: { + type: 'string', + }, + emailConfirmedAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + accountView: { + type: 'object', + required: ['did', 'handle', 'indexedAt'], + properties: { + did: { + type: 'string', + format: 'did', + }, + handle: { + type: 'string', + format: 'handle', + }, + email: { + type: 'string', + }, + relatedRecords: { + type: 'array', + items: { + type: 'unknown', + }, + }, + indexedAt: { + type: 'string', + format: 'datetime', + }, + invitedBy: { + type: 'ref', + ref: 'lex:com.atproto.server.defs#inviteCode', + }, + invites: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.server.defs#inviteCode', + }, + }, + invitesDisabled: { + type: 'boolean', + }, + emailConfirmedAt: { + type: 'string', + format: 'datetime', + }, + inviteNote: { + type: 'string', + }, + }, + }, + repoViewNotFound: { + type: 'object', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + }, + }, + }, + repoRef: { + type: 'object', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + }, + }, + }, + repoBlobRef: { + type: 'object', + required: ['did', 'cid'], + properties: { + did: { + type: 'string', + format: 'did', + }, + cid: { + type: 'string', + format: 'cid', + }, + recordUri: { + type: 'string', + format: 'at-uri', + }, + }, + }, + recordView: { + type: 'object', + required: [ + 'uri', + 'cid', + 'value', + 'blobCids', + 'indexedAt', + 'moderation', + 'repo', + ], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + value: { + type: 'unknown', + }, + blobCids: { + type: 'array', + items: { + type: 'string', + format: 'cid', + }, + }, + indexedAt: { + type: 'string', + format: 'datetime', + }, + moderation: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#moderation', + }, + repo: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#repoView', + }, + }, + }, + recordViewDetail: { + type: 'object', + required: [ + 'uri', + 'cid', + 'value', + 'blobs', + 'indexedAt', + 'moderation', + 'repo', + ], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + value: { + type: 'unknown', + }, + blobs: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#blobView', + }, + }, + labels: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.label.defs#label', + }, + }, + indexedAt: { + type: 'string', + format: 'datetime', + }, + moderation: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#moderationDetail', + }, + repo: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#repoView', + }, + }, + }, + recordViewNotFound: { + type: 'object', + required: ['uri'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + }, + }, + moderation: { + type: 'object', + properties: { + subjectStatus: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#subjectStatusView', + }, + }, + }, + moderationDetail: { + type: 'object', + properties: { + subjectStatus: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#subjectStatusView', + }, + }, + }, + blobView: { + type: 'object', + required: ['cid', 'mimeType', 'size', 'createdAt'], + properties: { + cid: { + type: 'string', + format: 'cid', + }, + mimeType: { + type: 'string', + }, + size: { + type: 'integer', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + details: { + type: 'union', + refs: [ + 'lex:com.atproto.admin.defs#imageDetails', + 'lex:com.atproto.admin.defs#videoDetails', + ], + }, + moderation: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#moderation', + }, + }, + }, + imageDetails: { + type: 'object', + required: ['width', 'height'], + properties: { + width: { + type: 'integer', + }, + height: { + type: 'integer', + }, + }, + }, + videoDetails: { + type: 'object', + required: ['width', 'height', 'length'], + properties: { + width: { + type: 'integer', + }, + height: { + type: 'integer', + }, + length: { + type: 'integer', + }, + }, + }, + subjectReviewState: { + type: 'string', + knownValues: [ + 'lex:com.atproto.admin.defs#reviewOpen', + 'lex:com.atproto.admin.defs#reviewEscalated', + 'lex:com.atproto.admin.defs#reviewClosed', + ], + }, + reviewOpen: { + type: 'token', + description: + 'Moderator review status of a subject: Open. Indicates that the subject needs to be reviewed by a moderator', + }, + reviewEscalated: { + type: 'token', + description: + 'Moderator review status of a subject: Escalated. Indicates that the subject was escalated for review by a moderator', + }, + reviewClosed: { + type: 'token', + description: + 'Moderator review status of a subject: Closed. Indicates that the subject was already reviewed and resolved by a moderator', + }, + modEventTakedown: { + type: 'object', + description: 'Take down a subject permanently or temporarily', + properties: { + comment: { + type: 'string', + }, + durationInHours: { + type: 'integer', + description: + 'Indicates how long the takedown should be in effect before automatically expiring.', + }, + }, + }, + modEventReverseTakedown: { + type: 'object', + description: 'Revert take down action on a subject', + properties: { + comment: { + type: 'string', + description: 'Describe reasoning behind the reversal.', + }, + }, + }, + modEventResolveAppeal: { + type: 'object', + description: 'Resolve appeal on a subject', + properties: { + comment: { + type: 'string', + description: 'Describe resolution.', + }, + }, + }, + modEventComment: { + type: 'object', + description: 'Add a comment to a subject', + required: ['comment'], + properties: { + comment: { + type: 'string', + }, + sticky: { + type: 'boolean', + description: 'Make the comment persistent on the subject', + }, + }, + }, + modEventReport: { + type: 'object', + description: 'Report a subject', + required: ['reportType'], + properties: { + comment: { + type: 'string', + }, + reportType: { + type: 'ref', + ref: 'lex:com.atproto.moderation.defs#reasonType', + }, + }, + }, + modEventLabel: { + type: 'object', + description: 'Apply/Negate labels on a subject', + required: ['createLabelVals', 'negateLabelVals'], + properties: { + comment: { + type: 'string', + }, + createLabelVals: { + type: 'array', + items: { + type: 'string', + }, + }, + negateLabelVals: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }, + modEventAcknowledge: { + type: 'object', + properties: { + comment: { + type: 'string', + }, + }, + }, + modEventEscalate: { + type: 'object', + properties: { + comment: { + type: 'string', + }, + }, + }, + modEventMute: { + type: 'object', + description: 'Mute incoming reports on a subject', + required: ['durationInHours'], + properties: { + comment: { + type: 'string', + }, + durationInHours: { + type: 'integer', + description: 'Indicates how long the subject should remain muted.', + }, + }, + }, + modEventUnmute: { + type: 'object', + description: 'Unmute action on a subject', + properties: { + comment: { + type: 'string', + description: 'Describe reasoning behind the reversal.', + }, + }, + }, + modEventEmail: { + type: 'object', + description: 'Keep a log of outgoing email to a user', + required: ['subjectLine'], + properties: { + subjectLine: { + type: 'string', + description: 'The subject line of the email sent to the user.', + }, + comment: { + type: 'string', + description: 'Additional comment about the outgoing comm.', + }, + }, + }, + }, + }, + ComAtprotoAdminDeleteAccount: { + lexicon: 1, + id: 'com.atproto.admin.deleteAccount', + defs: { + main: { + type: 'procedure', + description: 'Delete a user account as an administrator.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoAdminDisableAccountInvites: { + lexicon: 1, + id: 'com.atproto.admin.disableAccountInvites', + defs: { + main: { + type: 'procedure', + description: + 'Disable an account from receiving new invite codes, but does not invalidate existing codes.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['account'], + properties: { + account: { + type: 'string', + format: 'did', + }, + note: { + type: 'string', + description: 'Optional reason for disabled invites.', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoAdminDisableInviteCodes: { + lexicon: 1, + id: 'com.atproto.admin.disableInviteCodes', + defs: { + main: { + type: 'procedure', + description: + 'Disable some set of codes and/or all codes associated with a set of users.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + properties: { + codes: { + type: 'array', + items: { + type: 'string', + }, + }, + accounts: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + ComAtprotoAdminEmitModerationEvent: { + lexicon: 1, + id: 'com.atproto.admin.emitModerationEvent', + defs: { + main: { + type: 'procedure', + description: 'Take a moderation action on an actor.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['event', 'subject', 'createdBy'], + properties: { + event: { + type: 'union', + refs: [ + 'lex:com.atproto.admin.defs#modEventTakedown', + 'lex:com.atproto.admin.defs#modEventAcknowledge', + 'lex:com.atproto.admin.defs#modEventEscalate', + 'lex:com.atproto.admin.defs#modEventComment', + 'lex:com.atproto.admin.defs#modEventLabel', + 'lex:com.atproto.admin.defs#modEventReport', + 'lex:com.atproto.admin.defs#modEventMute', + 'lex:com.atproto.admin.defs#modEventReverseTakedown', + 'lex:com.atproto.admin.defs#modEventUnmute', + 'lex:com.atproto.admin.defs#modEventEmail', + ], + }, + subject: { + type: 'union', + refs: [ + 'lex:com.atproto.admin.defs#repoRef', + 'lex:com.atproto.repo.strongRef', + ], + }, + subjectBlobCids: { + type: 'array', + items: { + type: 'string', + format: 'cid', + }, + }, + createdBy: { + type: 'string', + format: 'did', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#modEventView', + }, + }, + errors: [ + { + name: 'SubjectHasAction', + }, + ], + }, + }, + }, + ComAtprotoAdminEnableAccountInvites: { + lexicon: 1, + id: 'com.atproto.admin.enableAccountInvites', + defs: { + main: { + type: 'procedure', + description: "Re-enable an account's ability to receive invite codes.", + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['account'], + properties: { + account: { + type: 'string', + format: 'did', + }, + note: { + type: 'string', + description: 'Optional reason for enabled invites.', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoAdminGetAccountInfo: { + lexicon: 1, + id: 'com.atproto.admin.getAccountInfo', + defs: { + main: { + type: 'query', + description: 'Get details about an account.', + parameters: { + type: 'params', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#accountView', + }, + }, + }, + }, + }, + ComAtprotoAdminGetAccountInfos: { + lexicon: 1, + id: 'com.atproto.admin.getAccountInfos', + defs: { + main: { + type: 'query', + description: 'Get details about some accounts.', + parameters: { + type: 'params', + required: ['dids'], + properties: { + dids: { + type: 'array', + items: { + type: 'string', + format: 'did', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['infos'], + properties: { + infos: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#accountView', + }, + }, + }, + }, + }, + }, + }, + }, + ComAtprotoAdminGetInviteCodes: { + lexicon: 1, + id: 'com.atproto.admin.getInviteCodes', + defs: { + main: { + type: 'query', + description: 'Get an admin view of invite codes.', + parameters: { + type: 'params', + properties: { + sort: { + type: 'string', + knownValues: ['recent', 'usage'], + default: 'recent', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 500, + default: 100, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['codes'], + properties: { + cursor: { + type: 'string', + }, + codes: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.server.defs#inviteCode', + }, + }, + }, + }, + }, + }, + }, + }, + ComAtprotoAdminGetModerationEvent: { + lexicon: 1, + id: 'com.atproto.admin.getModerationEvent', + defs: { + main: { + type: 'query', + description: 'Get details about a moderation event.', + parameters: { + type: 'params', + required: ['id'], + properties: { + id: { + type: 'integer', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#modEventViewDetail', + }, + }, + }, + }, + }, + ComAtprotoAdminGetRecord: { + lexicon: 1, + id: 'com.atproto.admin.getRecord', + defs: { + main: { + type: 'query', + description: 'Get details about a record.', + parameters: { + type: 'params', + required: ['uri'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#recordViewDetail', + }, + }, + errors: [ + { + name: 'RecordNotFound', + }, + ], + }, + }, + }, + ComAtprotoAdminGetRepo: { + lexicon: 1, + id: 'com.atproto.admin.getRepo', + defs: { + main: { + type: 'query', + description: 'Get details about a repository.', + parameters: { + type: 'params', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#repoViewDetail', + }, + }, + errors: [ + { + name: 'RepoNotFound', + }, + ], + }, + }, + }, + ComAtprotoAdminGetSubjectStatus: { + lexicon: 1, + id: 'com.atproto.admin.getSubjectStatus', + defs: { + main: { + type: 'query', + description: + 'Get the service-specific admin status of a subject (account, record, or blob).', + parameters: { + type: 'params', + properties: { + did: { + type: 'string', + format: 'did', + }, + uri: { + type: 'string', + format: 'at-uri', + }, + blob: { + type: 'string', + format: 'cid', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['subject'], + properties: { + subject: { + type: 'union', + refs: [ + 'lex:com.atproto.admin.defs#repoRef', + 'lex:com.atproto.repo.strongRef', + 'lex:com.atproto.admin.defs#repoBlobRef', + ], + }, + takedown: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#statusAttr', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoAdminQueryModerationEvents: { + lexicon: 1, + id: 'com.atproto.admin.queryModerationEvents', + defs: { + main: { + type: 'query', + description: 'List moderation events related to a subject.', + parameters: { + type: 'params', + properties: { + types: { + type: 'array', + items: { + type: 'string', + }, + description: + 'The types of events (fully qualified string in the format of com.atproto.admin#modEvent) to filter by. If not specified, all events are returned.', + }, + createdBy: { + type: 'string', + format: 'did', + }, + sortDirection: { + type: 'string', + default: 'desc', + enum: ['asc', 'desc'], + description: + 'Sort direction for the events. Defaults to descending order of created at timestamp.', + }, + subject: { + type: 'string', + format: 'uri', + }, + includeAllUserRecords: { + type: 'boolean', + default: false, + description: + 'If true, events on all record types (posts, lists, profile etc.) owned by the did are returned', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['events'], + properties: { + cursor: { + type: 'string', + }, + events: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#modEventView', + }, + }, + }, + }, + }, + }, + }, + }, + ComAtprotoAdminQueryModerationStatuses: { + lexicon: 1, + id: 'com.atproto.admin.queryModerationStatuses', + defs: { + main: { + type: 'query', + description: 'View moderation statuses of subjects (record or repo).', + parameters: { + type: 'params', + properties: { + subject: { + type: 'string', + format: 'uri', + }, + comment: { + type: 'string', + description: 'Search subjects by keyword from comments', + }, + reportedAfter: { + type: 'string', + format: 'datetime', + description: 'Search subjects reported after a given timestamp', + }, + reportedBefore: { + type: 'string', + format: 'datetime', + description: 'Search subjects reported before a given timestamp', + }, + reviewedAfter: { + type: 'string', + format: 'datetime', + description: 'Search subjects reviewed after a given timestamp', + }, + reviewedBefore: { + type: 'string', + format: 'datetime', + description: 'Search subjects reviewed before a given timestamp', + }, + includeMuted: { + type: 'boolean', + description: + "By default, we don't include muted subjects in the results. Set this to true to include them.", + }, + reviewState: { + type: 'string', + description: 'Specify when fetching subjects in a certain state', + }, + ignoreSubjects: { + type: 'array', + items: { + type: 'string', + format: 'uri', + }, + }, + lastReviewedBy: { + type: 'string', + format: 'did', + description: + 'Get all subject statuses that were reviewed by a specific moderator', + }, + sortField: { + type: 'string', + default: 'lastReportedAt', + enum: ['lastReviewedAt', 'lastReportedAt'], + }, + sortDirection: { + type: 'string', + default: 'desc', + enum: ['asc', 'desc'], + }, + takendown: { + type: 'boolean', + description: 'Get subjects that were taken down', + }, + appealed: { + type: 'boolean', + description: 'Get subjects in unresolved appealed status', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['subjectStatuses'], + properties: { + cursor: { + type: 'string', + }, + subjectStatuses: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#subjectStatusView', + }, + }, + }, + }, + }, + }, + }, + }, + ComAtprotoAdminSearchRepos: { + lexicon: 1, + id: 'com.atproto.admin.searchRepos', + defs: { + main: { + type: 'query', + description: 'Find repositories based on a search term.', + parameters: { + type: 'params', + properties: { + term: { + type: 'string', + description: "DEPRECATED: use 'q' instead", + }, + q: { + type: 'string', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['repos'], + properties: { + cursor: { + type: 'string', + }, + repos: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#repoView', + }, + }, + }, + }, + }, + }, + }, + }, + ComAtprotoAdminSendEmail: { + lexicon: 1, + id: 'com.atproto.admin.sendEmail', + defs: { + main: { + type: 'procedure', + description: "Send email to a user's account email address.", + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['recipientDid', 'content', 'senderDid'], + properties: { + recipientDid: { + type: 'string', + format: 'did', + }, + content: { + type: 'string', + }, + subject: { + type: 'string', + }, + senderDid: { + type: 'string', + format: 'did', + }, + comment: { + type: 'string', + description: + "Additional comment by the sender that won't be used in the email itself but helpful to provide more context for moderators/reviewers", + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['sent'], + properties: { + sent: { + type: 'boolean', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoAdminUpdateAccountEmail: { + lexicon: 1, + id: 'com.atproto.admin.updateAccountEmail', + defs: { + main: { + type: 'procedure', + description: "Administrative action to update an account's email.", + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['account', 'email'], + properties: { + account: { + type: 'string', + format: 'at-identifier', + description: 'The handle or DID of the repo.', + }, + email: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoAdminUpdateAccountHandle: { + lexicon: 1, + id: 'com.atproto.admin.updateAccountHandle', + defs: { + main: { + type: 'procedure', + description: "Administrative action to update an account's handle.", + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['did', 'handle'], + properties: { + did: { + type: 'string', + format: 'did', + }, + handle: { + type: 'string', + format: 'handle', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoAdminUpdateSubjectStatus: { + lexicon: 1, + id: 'com.atproto.admin.updateSubjectStatus', + defs: { + main: { + type: 'procedure', + description: + 'Update the service-specific admin status of a subject (account, record, or blob).', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['subject'], + properties: { + subject: { + type: 'union', + refs: [ + 'lex:com.atproto.admin.defs#repoRef', + 'lex:com.atproto.repo.strongRef', + 'lex:com.atproto.admin.defs#repoBlobRef', + ], + }, + takedown: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#statusAttr', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['subject'], + properties: { + subject: { + type: 'union', + refs: [ + 'lex:com.atproto.admin.defs#repoRef', + 'lex:com.atproto.repo.strongRef', + 'lex:com.atproto.admin.defs#repoBlobRef', + ], + }, + takedown: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#statusAttr', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoIdentityResolveHandle: { + lexicon: 1, + id: 'com.atproto.identity.resolveHandle', + defs: { + main: { + type: 'query', + description: 'Provides the DID of a repo.', + parameters: { + type: 'params', + required: ['handle'], + properties: { + handle: { + type: 'string', + format: 'handle', + description: 'The handle to resolve.', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoIdentityUpdateHandle: { + lexicon: 1, + id: 'com.atproto.identity.updateHandle', + defs: { + main: { + type: 'procedure', + description: 'Updates the handle of the account.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['handle'], + properties: { + handle: { + type: 'string', + format: 'handle', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoLabelDefs: { + lexicon: 1, + id: 'com.atproto.label.defs', + defs: { + label: { + type: 'object', + description: + 'Metadata tag on an atproto resource (eg, repo or record).', + required: ['src', 'uri', 'val', 'cts'], + properties: { + src: { + type: 'string', + format: 'did', + description: 'DID of the actor who created this label.', + }, + uri: { + type: 'string', + format: 'uri', + description: + 'AT URI of the record, repository (account), or other resource that this label applies to.', + }, + cid: { + type: 'string', + format: 'cid', + description: + "Optionally, CID specifying the specific version of 'uri' resource this label applies to.", + }, + val: { + type: 'string', + maxLength: 128, + description: + 'The short string name of the value or type of this label.', + }, + neg: { + type: 'boolean', + description: + 'If true, this is a negation label, overwriting a previous label.', + }, + cts: { + type: 'string', + format: 'datetime', + description: 'Timestamp when this label was created.', + }, + }, + }, + selfLabels: { + type: 'object', + description: + 'Metadata tags on an atproto record, published by the author within the record.', + required: ['values'], + properties: { + values: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.label.defs#selfLabel', + }, + maxLength: 10, + }, + }, + }, + selfLabel: { + type: 'object', + description: + 'Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.', + required: ['val'], + properties: { + val: { + type: 'string', + maxLength: 128, + description: + 'The short string name of the value or type of this label.', + }, + }, + }, + }, + }, + ComAtprotoLabelQueryLabels: { + lexicon: 1, + id: 'com.atproto.label.queryLabels', + defs: { + main: { + type: 'query', + description: 'Find labels relevant to the provided URI patterns.', + parameters: { + type: 'params', + required: ['uriPatterns'], + properties: { + uriPatterns: { + type: 'array', + items: { + type: 'string', + }, + description: + "List of AT URI patterns to match (boolean 'OR'). Each may be a prefix (ending with '*'; will match inclusive of the string leading to '*'), or a full URI.", + }, + sources: { + type: 'array', + items: { + type: 'string', + format: 'did', + }, + description: + 'Optional list of label sources (DIDs) to filter on.', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 250, + default: 50, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['labels'], + properties: { + cursor: { + type: 'string', + }, + labels: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.label.defs#label', + }, + }, + }, + }, + }, + }, + }, + }, + ComAtprotoLabelSubscribeLabels: { + lexicon: 1, + id: 'com.atproto.label.subscribeLabels', + defs: { + main: { + type: 'subscription', + description: 'Subscribe to label updates.', + parameters: { + type: 'params', + properties: { + cursor: { + type: 'integer', + description: 'The last known event to backfill from.', + }, + }, + }, + message: { + schema: { + type: 'union', + refs: [ + 'lex:com.atproto.label.subscribeLabels#labels', + 'lex:com.atproto.label.subscribeLabels#info', + ], + }, + }, + errors: [ + { + name: 'FutureCursor', + }, + ], + }, + labels: { + type: 'object', + required: ['seq', 'labels'], + properties: { + seq: { + type: 'integer', + }, + labels: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.label.defs#label', + }, + }, + }, + }, + info: { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string', + knownValues: ['OutdatedCursor'], + }, + message: { + type: 'string', + }, + }, + }, + }, + }, + ComAtprotoModerationCreateReport: { + lexicon: 1, + id: 'com.atproto.moderation.createReport', + defs: { + main: { + type: 'procedure', + description: 'Report a repo or a record.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['reasonType', 'subject'], + properties: { + reasonType: { + type: 'ref', + ref: 'lex:com.atproto.moderation.defs#reasonType', + }, + reason: { + type: 'string', + }, + subject: { + type: 'union', + refs: [ + 'lex:com.atproto.admin.defs#repoRef', + 'lex:com.atproto.repo.strongRef', + ], + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: [ + 'id', + 'reasonType', + 'subject', + 'reportedBy', + 'createdAt', + ], + properties: { + id: { + type: 'integer', + }, + reasonType: { + type: 'ref', + ref: 'lex:com.atproto.moderation.defs#reasonType', + }, + reason: { + type: 'string', + maxGraphemes: 2000, + maxLength: 20000, + }, + subject: { + type: 'union', + refs: [ + 'lex:com.atproto.admin.defs#repoRef', + 'lex:com.atproto.repo.strongRef', + ], + }, + reportedBy: { + type: 'string', + format: 'did', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoModerationDefs: { + lexicon: 1, + id: 'com.atproto.moderation.defs', + defs: { + reasonType: { + type: 'string', + knownValues: [ + 'com.atproto.moderation.defs#reasonSpam', + 'com.atproto.moderation.defs#reasonViolation', + 'com.atproto.moderation.defs#reasonMisleading', + 'com.atproto.moderation.defs#reasonSexual', + 'com.atproto.moderation.defs#reasonRude', + 'com.atproto.moderation.defs#reasonOther', + 'com.atproto.moderation.defs#reasonAppeal', + ], + }, + reasonSpam: { + type: 'token', + description: 'Spam: frequent unwanted promotion, replies, mentions', + }, + reasonViolation: { + type: 'token', + description: 'Direct violation of server rules, laws, terms of service', + }, + reasonMisleading: { + type: 'token', + description: 'Misleading identity, affiliation, or content', + }, + reasonSexual: { + type: 'token', + description: 'Unwanted or mislabeled sexual content', + }, + reasonRude: { + type: 'token', + description: + 'Rude, harassing, explicit, or otherwise unwelcoming behavior', + }, + reasonOther: { + type: 'token', + description: 'Other: reports not falling under another report category', + }, + reasonAppeal: { + type: 'token', + description: 'Appeal: appeal a previously taken moderation action', + }, + }, + }, + ComAtprotoRepoApplyWrites: { + lexicon: 1, + id: 'com.atproto.repo.applyWrites', + defs: { + main: { + type: 'procedure', + description: + 'Apply a batch transaction of creates, updates, and deletes.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['repo', 'writes'], + properties: { + repo: { + type: 'string', + format: 'at-identifier', + description: 'The handle or DID of the repo.', + }, + validate: { + type: 'boolean', + default: true, + description: 'Flag for validating the records.', + }, + writes: { + type: 'array', + items: { + type: 'union', + refs: [ + 'lex:com.atproto.repo.applyWrites#create', + 'lex:com.atproto.repo.applyWrites#update', + 'lex:com.atproto.repo.applyWrites#delete', + ], + closed: true, + }, + }, + swapCommit: { + type: 'string', + format: 'cid', + }, + }, + }, + }, + errors: [ + { + name: 'InvalidSwap', + }, + ], + }, + create: { + type: 'object', + description: 'Create a new record.', + required: ['collection', 'value'], + properties: { + collection: { + type: 'string', + format: 'nsid', + }, + rkey: { + type: 'string', + maxLength: 15, + }, + value: { + type: 'unknown', + }, + }, + }, + update: { + type: 'object', + description: 'Update an existing record.', + required: ['collection', 'rkey', 'value'], + properties: { + collection: { + type: 'string', + format: 'nsid', + }, + rkey: { + type: 'string', + }, + value: { + type: 'unknown', + }, + }, + }, + delete: { + type: 'object', + description: 'Delete an existing record.', + required: ['collection', 'rkey'], + properties: { + collection: { + type: 'string', + format: 'nsid', + }, + rkey: { + type: 'string', + }, + }, + }, + }, + }, + ComAtprotoRepoCreateRecord: { + lexicon: 1, + id: 'com.atproto.repo.createRecord', + defs: { + main: { + type: 'procedure', + description: 'Create a new record.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['repo', 'collection', 'record'], + properties: { + repo: { + type: 'string', + format: 'at-identifier', + description: 'The handle or DID of the repo.', + }, + collection: { + type: 'string', + format: 'nsid', + description: 'The NSID of the record collection.', + }, + rkey: { + type: 'string', + description: 'The key of the record.', + maxLength: 15, + }, + validate: { + type: 'boolean', + default: true, + description: 'Flag for validating the record.', + }, + record: { + type: 'unknown', + description: 'The record to create.', + }, + swapCommit: { + type: 'string', + format: 'cid', + description: + 'Compare and swap with the previous commit by CID.', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['uri', 'cid'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + }, + }, + }, + errors: [ + { + name: 'InvalidSwap', + }, + ], + }, + }, + }, + ComAtprotoRepoDeleteRecord: { + lexicon: 1, + id: 'com.atproto.repo.deleteRecord', + defs: { + main: { + type: 'procedure', + description: "Delete a record, or ensure it doesn't exist.", + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['repo', 'collection', 'rkey'], + properties: { + repo: { + type: 'string', + format: 'at-identifier', + description: 'The handle or DID of the repo.', + }, + collection: { + type: 'string', + format: 'nsid', + description: 'The NSID of the record collection.', + }, + rkey: { + type: 'string', + description: 'The key of the record.', + }, + swapRecord: { + type: 'string', + format: 'cid', + description: + 'Compare and swap with the previous record by CID.', + }, + swapCommit: { + type: 'string', + format: 'cid', + description: + 'Compare and swap with the previous commit by CID.', + }, + }, + }, + }, + errors: [ + { + name: 'InvalidSwap', + }, + ], + }, + }, + }, + ComAtprotoRepoDescribeRepo: { + lexicon: 1, + id: 'com.atproto.repo.describeRepo', + defs: { + main: { + type: 'query', + description: + 'Get information about the repo, including the list of collections.', + parameters: { + type: 'params', + required: ['repo'], + properties: { + repo: { + type: 'string', + format: 'at-identifier', + description: 'The handle or DID of the repo.', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: [ + 'handle', + 'did', + 'didDoc', + 'collections', + 'handleIsCorrect', + ], + properties: { + handle: { + type: 'string', + format: 'handle', + }, + did: { + type: 'string', + format: 'did', + }, + didDoc: { + type: 'unknown', + }, + collections: { + type: 'array', + items: { + type: 'string', + format: 'nsid', + }, + }, + handleIsCorrect: { + type: 'boolean', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoRepoGetRecord: { + lexicon: 1, + id: 'com.atproto.repo.getRecord', + defs: { + main: { + type: 'query', + description: 'Get a record.', + parameters: { + type: 'params', + required: ['repo', 'collection', 'rkey'], + properties: { + repo: { + type: 'string', + format: 'at-identifier', + description: 'The handle or DID of the repo.', + }, + collection: { + type: 'string', + format: 'nsid', + description: 'The NSID of the record collection.', + }, + rkey: { + type: 'string', + description: 'The key of the record.', + }, + cid: { + type: 'string', + format: 'cid', + description: + 'The CID of the version of the record. If not specified, then return the most recent version.', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['uri', 'value'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + value: { + type: 'unknown', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoRepoListRecords: { + lexicon: 1, + id: 'com.atproto.repo.listRecords', + defs: { + main: { + type: 'query', + description: 'List a range of records in a collection.', + parameters: { + type: 'params', + required: ['repo', 'collection'], + properties: { + repo: { + type: 'string', + format: 'at-identifier', + description: 'The handle or DID of the repo.', + }, + collection: { + type: 'string', + format: 'nsid', + description: 'The NSID of the record type.', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + description: 'The number of records to return.', + }, + cursor: { + type: 'string', + }, + rkeyStart: { + type: 'string', + description: + 'DEPRECATED: The lowest sort-ordered rkey to start from (exclusive)', + }, + rkeyEnd: { + type: 'string', + description: + 'DEPRECATED: The highest sort-ordered rkey to stop at (exclusive)', + }, + reverse: { + type: 'boolean', + description: 'Flag to reverse the order of the returned records.', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['records'], + properties: { + cursor: { + type: 'string', + }, + records: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.repo.listRecords#record', + }, + }, + }, + }, + }, + }, + record: { + type: 'object', + required: ['uri', 'cid', 'value'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + value: { + type: 'unknown', + }, + }, + }, + }, + }, + ComAtprotoRepoPutRecord: { + lexicon: 1, + id: 'com.atproto.repo.putRecord', + defs: { + main: { + type: 'procedure', + description: 'Write a record, creating or updating it as needed.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['repo', 'collection', 'rkey', 'record'], + nullable: ['swapRecord'], + properties: { + repo: { + type: 'string', + format: 'at-identifier', + description: 'The handle or DID of the repo.', + }, + collection: { + type: 'string', + format: 'nsid', + description: 'The NSID of the record collection.', + }, + rkey: { + type: 'string', + description: 'The key of the record.', + maxLength: 15, + }, + validate: { + type: 'boolean', + default: true, + description: 'Flag for validating the record.', + }, + record: { + type: 'unknown', + description: 'The record to write.', + }, + swapRecord: { + type: 'string', + format: 'cid', + description: + 'Compare and swap with the previous record by CID.', + }, + swapCommit: { + type: 'string', + format: 'cid', + description: + 'Compare and swap with the previous commit by CID.', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['uri', 'cid'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + }, + }, + }, + errors: [ + { + name: 'InvalidSwap', + }, + ], + }, + }, + }, + ComAtprotoRepoStrongRef: { + lexicon: 1, + id: 'com.atproto.repo.strongRef', + description: 'A URI with a content-hash fingerprint.', + defs: { + main: { + type: 'object', + required: ['uri', 'cid'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + }, + }, + }, + }, + ComAtprotoRepoUploadBlob: { + lexicon: 1, + id: 'com.atproto.repo.uploadBlob', + defs: { + main: { + type: 'procedure', + description: + 'Upload a new blob to be added to repo in a later request.', + input: { + encoding: '*/*', + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['blob'], + properties: { + blob: { + type: 'blob', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoServerConfirmEmail: { + lexicon: 1, + id: 'com.atproto.server.confirmEmail', + defs: { + main: { + type: 'procedure', + description: + 'Confirm an email using a token from com.atproto.server.requestEmailConfirmation.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['email', 'token'], + properties: { + email: { + type: 'string', + }, + token: { + type: 'string', + }, + }, + }, + }, + errors: [ + { + name: 'AccountNotFound', + }, + { + name: 'ExpiredToken', + }, + { + name: 'InvalidToken', + }, + { + name: 'InvalidEmail', + }, + ], + }, + }, + }, + ComAtprotoServerCreateAccount: { + lexicon: 1, + id: 'com.atproto.server.createAccount', + defs: { + main: { + type: 'procedure', + description: 'Create an account.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['handle'], + properties: { + email: { + type: 'string', + }, + handle: { + type: 'string', + format: 'handle', + }, + did: { + type: 'string', + format: 'did', + }, + inviteCode: { + type: 'string', + }, + password: { + type: 'string', + }, + recoveryKey: { + type: 'string', + }, + plcOp: { + type: 'unknown', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['accessJwt', 'refreshJwt', 'handle', 'did'], + properties: { + accessJwt: { + type: 'string', + }, + refreshJwt: { + type: 'string', + }, + handle: { + type: 'string', + format: 'handle', + }, + did: { + type: 'string', + format: 'did', + }, + didDoc: { + type: 'unknown', + }, + }, + }, + }, + errors: [ + { + name: 'InvalidHandle', + }, + { + name: 'InvalidPassword', + }, + { + name: 'InvalidInviteCode', + }, + { + name: 'HandleNotAvailable', + }, + { + name: 'UnsupportedDomain', + }, + { + name: 'UnresolvableDid', + }, + { + name: 'IncompatibleDidDoc', + }, + ], + }, + }, + }, + ComAtprotoServerCreateAppPassword: { + lexicon: 1, + id: 'com.atproto.server.createAppPassword', + defs: { + main: { + type: 'procedure', + description: 'Create an App Password.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'ref', + ref: 'lex:com.atproto.server.createAppPassword#appPassword', + }, + }, + errors: [ + { + name: 'AccountTakedown', + }, + ], + }, + appPassword: { + type: 'object', + required: ['name', 'password', 'createdAt'], + properties: { + name: { + type: 'string', + }, + password: { + type: 'string', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + }, + ComAtprotoServerCreateInviteCode: { + lexicon: 1, + id: 'com.atproto.server.createInviteCode', + defs: { + main: { + type: 'procedure', + description: 'Create an invite code.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['useCount'], + properties: { + useCount: { + type: 'integer', + }, + forAccount: { + type: 'string', + format: 'did', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['code'], + properties: { + code: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoServerCreateInviteCodes: { + lexicon: 1, + id: 'com.atproto.server.createInviteCodes', + defs: { + main: { + type: 'procedure', + description: 'Create invite codes.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['codeCount', 'useCount'], + properties: { + codeCount: { + type: 'integer', + default: 1, + }, + useCount: { + type: 'integer', + }, + forAccounts: { + type: 'array', + items: { + type: 'string', + format: 'did', + }, + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['codes'], + properties: { + codes: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.server.createInviteCodes#accountCodes', + }, + }, + }, + }, + }, + }, + accountCodes: { + type: 'object', + required: ['account', 'codes'], + properties: { + account: { + type: 'string', + }, + codes: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }, + }, + }, + ComAtprotoServerCreateSession: { + lexicon: 1, + id: 'com.atproto.server.createSession', + defs: { + main: { + type: 'procedure', + description: 'Create an authentication session.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['identifier', 'password'], + properties: { + identifier: { + type: 'string', + description: + 'Handle or other identifier supported by the server for the authenticating user.', + }, + password: { + type: 'string', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['accessJwt', 'refreshJwt', 'handle', 'did'], + properties: { + accessJwt: { + type: 'string', + }, + refreshJwt: { + type: 'string', + }, + handle: { + type: 'string', + format: 'handle', + }, + did: { + type: 'string', + format: 'did', + }, + didDoc: { + type: 'unknown', + }, + email: { + type: 'string', + }, + emailConfirmed: { + type: 'boolean', + }, + }, + }, + }, + errors: [ + { + name: 'AccountTakedown', + }, + ], + }, + }, + }, + ComAtprotoServerDefs: { + lexicon: 1, + id: 'com.atproto.server.defs', + defs: { + inviteCode: { + type: 'object', + required: [ + 'code', + 'available', + 'disabled', + 'forAccount', + 'createdBy', + 'createdAt', + 'uses', + ], + properties: { + code: { + type: 'string', + }, + available: { + type: 'integer', + }, + disabled: { + type: 'boolean', + }, + forAccount: { + type: 'string', + }, + createdBy: { + type: 'string', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + uses: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.server.defs#inviteCodeUse', + }, + }, + }, + }, + inviteCodeUse: { + type: 'object', + required: ['usedBy', 'usedAt'], + properties: { + usedBy: { + type: 'string', + format: 'did', + }, + usedAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + }, + ComAtprotoServerDeleteAccount: { + lexicon: 1, + id: 'com.atproto.server.deleteAccount', + defs: { + main: { + type: 'procedure', + description: "Delete an actor's account with a token and password.", + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['did', 'password', 'token'], + properties: { + did: { + type: 'string', + format: 'did', + }, + password: { + type: 'string', + }, + token: { + type: 'string', + }, + }, + }, + }, + errors: [ + { + name: 'ExpiredToken', + }, + { + name: 'InvalidToken', + }, + ], + }, + }, + }, + ComAtprotoServerDeleteSession: { + lexicon: 1, + id: 'com.atproto.server.deleteSession', + defs: { + main: { + type: 'procedure', + description: 'Delete the current session.', + }, + }, + }, + ComAtprotoServerDescribeServer: { + lexicon: 1, + id: 'com.atproto.server.describeServer', + defs: { + main: { + type: 'query', + description: + "Get a document describing the service's accounts configuration.", + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['availableUserDomains'], + properties: { + inviteCodeRequired: { + type: 'boolean', + }, + availableUserDomains: { + type: 'array', + items: { + type: 'string', + }, + }, + links: { + type: 'ref', + ref: 'lex:com.atproto.server.describeServer#links', + }, + }, + }, + }, + }, + links: { + type: 'object', + properties: { + privacyPolicy: { + type: 'string', + }, + termsOfService: { + type: 'string', + }, + }, + }, + }, + }, + ComAtprotoServerGetAccountInviteCodes: { + lexicon: 1, + id: 'com.atproto.server.getAccountInviteCodes', + defs: { + main: { + type: 'query', + description: 'Get all invite codes for a given account.', + parameters: { + type: 'params', + properties: { + includeUsed: { + type: 'boolean', + default: true, + }, + createAvailable: { + type: 'boolean', + default: true, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['codes'], + properties: { + codes: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.server.defs#inviteCode', + }, + }, + }, + }, + }, + errors: [ + { + name: 'DuplicateCreate', + }, + ], + }, + }, + }, + ComAtprotoServerGetSession: { + lexicon: 1, + id: 'com.atproto.server.getSession', + defs: { + main: { + type: 'query', + description: 'Get information about the current session.', + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['handle', 'did'], + properties: { + handle: { + type: 'string', + format: 'handle', + }, + did: { + type: 'string', + format: 'did', + }, + email: { + type: 'string', + }, + emailConfirmed: { + type: 'boolean', + }, + didDoc: { + type: 'unknown', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoServerListAppPasswords: { + lexicon: 1, + id: 'com.atproto.server.listAppPasswords', + defs: { + main: { + type: 'query', + description: 'List all App Passwords.', + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['passwords'], + properties: { + passwords: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.server.listAppPasswords#appPassword', + }, + }, + }, + }, + }, + errors: [ + { + name: 'AccountTakedown', + }, + ], + }, + appPassword: { + type: 'object', + required: ['name', 'createdAt'], + properties: { + name: { + type: 'string', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + }, + ComAtprotoServerRefreshSession: { + lexicon: 1, + id: 'com.atproto.server.refreshSession', + defs: { + main: { + type: 'procedure', + description: 'Refresh an authentication session.', + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['accessJwt', 'refreshJwt', 'handle', 'did'], + properties: { + accessJwt: { + type: 'string', + }, + refreshJwt: { + type: 'string', + }, + handle: { + type: 'string', + format: 'handle', + }, + did: { + type: 'string', + format: 'did', + }, + didDoc: { + type: 'unknown', + }, + }, + }, + }, + errors: [ + { + name: 'AccountTakedown', + }, + ], + }, + }, + }, + ComAtprotoServerRequestAccountDelete: { + lexicon: 1, + id: 'com.atproto.server.requestAccountDelete', + defs: { + main: { + type: 'procedure', + description: 'Initiate a user account deletion via email.', + }, + }, + }, + ComAtprotoServerRequestEmailConfirmation: { + lexicon: 1, + id: 'com.atproto.server.requestEmailConfirmation', + defs: { + main: { + type: 'procedure', + description: + 'Request an email with a code to confirm ownership of email.', + }, + }, + }, + ComAtprotoServerRequestEmailUpdate: { + lexicon: 1, + id: 'com.atproto.server.requestEmailUpdate', + defs: { + main: { + type: 'procedure', + description: 'Request a token in order to update email.', + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['tokenRequired'], + properties: { + tokenRequired: { + type: 'boolean', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoServerRequestPasswordReset: { + lexicon: 1, + id: 'com.atproto.server.requestPasswordReset', + defs: { + main: { + type: 'procedure', + description: 'Initiate a user account password reset via email.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['email'], + properties: { + email: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoServerReserveSigningKey: { + lexicon: 1, + id: 'com.atproto.server.reserveSigningKey', + defs: { + main: { + type: 'procedure', + description: 'Reserve a repo signing key for account creation.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + properties: { + did: { + type: 'string', + description: 'The did to reserve a new did:key for', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['signingKey'], + properties: { + signingKey: { + type: 'string', + description: 'Public signing key in the form of a did:key.', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoServerResetPassword: { + lexicon: 1, + id: 'com.atproto.server.resetPassword', + defs: { + main: { + type: 'procedure', + description: 'Reset a user account password using a token.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['token', 'password'], + properties: { + token: { + type: 'string', + }, + password: { + type: 'string', + }, + }, + }, + }, + errors: [ + { + name: 'ExpiredToken', + }, + { + name: 'InvalidToken', + }, + ], + }, + }, + }, + ComAtprotoServerRevokeAppPassword: { + lexicon: 1, + id: 'com.atproto.server.revokeAppPassword', + defs: { + main: { + type: 'procedure', + description: 'Revoke an App Password by name.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoServerUpdateEmail: { + lexicon: 1, + id: 'com.atproto.server.updateEmail', + defs: { + main: { + type: 'procedure', + description: "Update an account's email.", + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['email'], + properties: { + email: { + type: 'string', + }, + token: { + type: 'string', + description: + "Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.", + }, + }, + }, + }, + errors: [ + { + name: 'ExpiredToken', + }, + { + name: 'InvalidToken', + }, + { + name: 'TokenRequired', + }, + ], + }, + }, + }, + ComAtprotoSyncGetBlob: { + lexicon: 1, + id: 'com.atproto.sync.getBlob', + defs: { + main: { + type: 'query', + description: 'Get a blob associated with a given repo.', + parameters: { + type: 'params', + required: ['did', 'cid'], + properties: { + did: { + type: 'string', + format: 'did', + description: 'The DID of the repo.', + }, + cid: { + type: 'string', + format: 'cid', + description: 'The CID of the blob to fetch', + }, + }, + }, + output: { + encoding: '*/*', + }, + }, + }, + }, + ComAtprotoSyncGetBlocks: { + lexicon: 1, + id: 'com.atproto.sync.getBlocks', + defs: { + main: { + type: 'query', + description: 'Get blocks from a given repo.', + parameters: { + type: 'params', + required: ['did', 'cids'], + properties: { + did: { + type: 'string', + format: 'did', + description: 'The DID of the repo.', + }, + cids: { + type: 'array', + items: { + type: 'string', + format: 'cid', + }, + }, + }, + }, + output: { + encoding: 'application/vnd.ipld.car', + }, + }, + }, + }, + ComAtprotoSyncGetCheckout: { + lexicon: 1, + id: 'com.atproto.sync.getCheckout', + defs: { + main: { + type: 'query', + description: 'DEPRECATED - please use com.atproto.sync.getRepo instead', + parameters: { + type: 'params', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + description: 'The DID of the repo.', + }, + }, + }, + output: { + encoding: 'application/vnd.ipld.car', + }, + }, + }, + }, + ComAtprotoSyncGetHead: { + lexicon: 1, + id: 'com.atproto.sync.getHead', + defs: { + main: { + type: 'query', + description: + 'DEPRECATED - please use com.atproto.sync.getLatestCommit instead', + parameters: { + type: 'params', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + description: 'The DID of the repo.', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['root'], + properties: { + root: { + type: 'string', + format: 'cid', + }, + }, + }, + }, + errors: [ + { + name: 'HeadNotFound', + }, + ], + }, + }, + }, + ComAtprotoSyncGetLatestCommit: { + lexicon: 1, + id: 'com.atproto.sync.getLatestCommit', + defs: { + main: { + type: 'query', + description: 'Get the current commit CID & revision of the repo.', + parameters: { + type: 'params', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + description: 'The DID of the repo.', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['cid', 'rev'], + properties: { + cid: { + type: 'string', + format: 'cid', + }, + rev: { + type: 'string', + }, + }, + }, + }, + errors: [ + { + name: 'RepoNotFound', + }, + ], + }, + }, + }, + ComAtprotoSyncGetRecord: { + lexicon: 1, + id: 'com.atproto.sync.getRecord', + defs: { + main: { + type: 'query', + description: + 'Get blocks needed for existence or non-existence of record.', + parameters: { + type: 'params', + required: ['did', 'collection', 'rkey'], + properties: { + did: { + type: 'string', + format: 'did', + description: 'The DID of the repo.', + }, + collection: { + type: 'string', + format: 'nsid', + }, + rkey: { + type: 'string', + }, + commit: { + type: 'string', + format: 'cid', + description: 'An optional past commit CID.', + }, + }, + }, + output: { + encoding: 'application/vnd.ipld.car', + }, + }, + }, + }, + ComAtprotoSyncGetRepo: { + lexicon: 1, + id: 'com.atproto.sync.getRepo', + defs: { + main: { + type: 'query', + description: + "Gets the DID's repo, optionally catching up from a specific revision.", + parameters: { + type: 'params', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + description: 'The DID of the repo.', + }, + since: { + type: 'string', + description: 'The revision of the repo to catch up from.', + }, + }, + }, + output: { + encoding: 'application/vnd.ipld.car', + }, + }, + }, + }, + ComAtprotoSyncListBlobs: { + lexicon: 1, + id: 'com.atproto.sync.listBlobs', + defs: { + main: { + type: 'query', + description: 'List blob CIDs since some revision.', + parameters: { + type: 'params', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + description: 'The DID of the repo.', + }, + since: { + type: 'string', + description: 'Optional revision of the repo to list blobs since.', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 1000, + default: 500, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['cids'], + properties: { + cursor: { + type: 'string', + }, + cids: { + type: 'array', + items: { + type: 'string', + format: 'cid', + }, + }, + }, + }, + }, + }, + }, + }, + ComAtprotoSyncListRepos: { + lexicon: 1, + id: 'com.atproto.sync.listRepos', + defs: { + main: { + type: 'query', + description: 'List DIDs and root CIDs of hosted repos.', + parameters: { + type: 'params', + properties: { + limit: { + type: 'integer', + minimum: 1, + maximum: 1000, + default: 500, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['repos'], + properties: { + cursor: { + type: 'string', + }, + repos: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.sync.listRepos#repo', + }, + }, + }, + }, + }, + }, + repo: { + type: 'object', + required: ['did', 'head', 'rev'], + properties: { + did: { + type: 'string', + format: 'did', + }, + head: { + type: 'string', + format: 'cid', + }, + rev: { + type: 'string', + }, + }, + }, + }, + }, + ComAtprotoSyncNotifyOfUpdate: { + lexicon: 1, + id: 'com.atproto.sync.notifyOfUpdate', + defs: { + main: { + type: 'procedure', + description: + 'Notify a crawling service of a recent update; often when a long break between updates causes the connection with the crawling service to break.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['hostname'], + properties: { + hostname: { + type: 'string', + description: + 'Hostname of the service that is notifying of update.', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoSyncRequestCrawl: { + lexicon: 1, + id: 'com.atproto.sync.requestCrawl', + defs: { + main: { + type: 'procedure', + description: 'Request a service to persistently crawl hosted repos.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['hostname'], + properties: { + hostname: { + type: 'string', + description: + 'Hostname of the service that is requesting to be crawled.', + }, + }, + }, + }, + }, + }, + }, + ComAtprotoSyncSubscribeRepos: { + lexicon: 1, + id: 'com.atproto.sync.subscribeRepos', + defs: { + main: { + type: 'subscription', + description: 'Subscribe to repo updates.', + parameters: { + type: 'params', + properties: { + cursor: { + type: 'integer', + description: 'The last known event to backfill from.', + }, + }, + }, + message: { + schema: { + type: 'union', + refs: [ + 'lex:com.atproto.sync.subscribeRepos#commit', + 'lex:com.atproto.sync.subscribeRepos#handle', + 'lex:com.atproto.sync.subscribeRepos#migrate', + 'lex:com.atproto.sync.subscribeRepos#tombstone', + 'lex:com.atproto.sync.subscribeRepos#info', + ], + }, + }, + errors: [ + { + name: 'FutureCursor', + }, + { + name: 'ConsumerTooSlow', + }, + ], + }, + commit: { + type: 'object', + required: [ + 'seq', + 'rebase', + 'tooBig', + 'repo', + 'commit', + 'rev', + 'since', + 'blocks', + 'ops', + 'blobs', + 'time', + ], + nullable: ['prev', 'since'], + properties: { + seq: { + type: 'integer', + }, + rebase: { + type: 'boolean', + }, + tooBig: { + type: 'boolean', + }, + repo: { + type: 'string', + format: 'did', + }, + commit: { + type: 'cid-link', + }, + prev: { + type: 'cid-link', + }, + rev: { + type: 'string', + description: 'The rev of the emitted commit.', + }, + since: { + type: 'string', + description: 'The rev of the last emitted commit from this repo.', + }, + blocks: { + type: 'bytes', + description: 'CAR file containing relevant blocks.', + maxLength: 1000000, + }, + ops: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.sync.subscribeRepos#repoOp', + }, + maxLength: 200, + }, + blobs: { + type: 'array', + items: { + type: 'cid-link', + }, + }, + time: { + type: 'string', + format: 'datetime', + }, + }, + }, + handle: { + type: 'object', + required: ['seq', 'did', 'handle', 'time'], + properties: { + seq: { + type: 'integer', + }, + did: { + type: 'string', + format: 'did', + }, + handle: { + type: 'string', + format: 'handle', + }, + time: { + type: 'string', + format: 'datetime', + }, + }, + }, + migrate: { + type: 'object', + required: ['seq', 'did', 'migrateTo', 'time'], + nullable: ['migrateTo'], + properties: { + seq: { + type: 'integer', + }, + did: { + type: 'string', + format: 'did', + }, + migrateTo: { + type: 'string', + }, + time: { + type: 'string', + format: 'datetime', + }, + }, + }, + tombstone: { + type: 'object', + required: ['seq', 'did', 'time'], + properties: { + seq: { + type: 'integer', + }, + did: { + type: 'string', + format: 'did', + }, + time: { + type: 'string', + format: 'datetime', + }, + }, + }, + info: { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string', + knownValues: ['OutdatedCursor'], + }, + message: { + type: 'string', + }, + }, + }, + repoOp: { + type: 'object', + description: + "A repo operation, ie a write of a single record. For creates and updates, CID is the record's CID as of this operation. For deletes, it's null.", + required: ['action', 'path', 'cid'], + nullable: ['cid'], + properties: { + action: { + type: 'string', + knownValues: ['create', 'update', 'delete'], + }, + path: { + type: 'string', + }, + cid: { + type: 'cid-link', + }, + }, + }, + }, + }, + ComAtprotoTempFetchLabels: { + lexicon: 1, + id: 'com.atproto.temp.fetchLabels', + defs: { + main: { + type: 'query', + description: + 'Fetch all labels from a labeler created after a certain date.', + parameters: { + type: 'params', + properties: { + since: { + type: 'integer', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 250, + default: 50, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['labels'], + properties: { + labels: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.label.defs#label', + }, + }, + }, + }, + }, + }, + }, + }, + ComAtprotoTempImportRepo: { + lexicon: 1, + id: 'com.atproto.temp.importRepo', + defs: { + main: { + type: 'procedure', + description: + "Gets the did's repo, optionally catching up from a specific revision.", + parameters: { + type: 'params', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + description: 'The DID of the repo.', + }, + }, + }, + input: { + encoding: 'application/vnd.ipld.car', + }, + output: { + encoding: 'text/plain', + }, + }, + }, + }, + ComAtprotoTempPushBlob: { + lexicon: 1, + id: 'com.atproto.temp.pushBlob', + defs: { + main: { + type: 'procedure', + description: + "Gets the did's repo, optionally catching up from a specific revision.", + parameters: { + type: 'params', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + description: 'The DID of the repo.', + }, + }, + }, + input: { + encoding: '*/*', + }, + }, + }, + }, + ComAtprotoTempTransferAccount: { + lexicon: 1, + id: 'com.atproto.temp.transferAccount', + defs: { + main: { + type: 'procedure', + description: 'Transfer an account.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['handle', 'did', 'plcOp'], + properties: { + handle: { + type: 'string', + format: 'handle', + }, + did: { + type: 'string', + format: 'did', + }, + plcOp: { + type: 'unknown', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['accessJwt', 'refreshJwt', 'handle', 'did'], + properties: { + accessJwt: { + type: 'string', + }, + refreshJwt: { + type: 'string', + }, + handle: { + type: 'string', + format: 'handle', + }, + did: { + type: 'string', + format: 'did', + }, + }, + }, + }, + errors: [ + { + name: 'InvalidHandle', + }, + { + name: 'InvalidPassword', + }, + { + name: 'InvalidInviteCode', + }, + { + name: 'HandleNotAvailable', + }, + { + name: 'UnsupportedDomain', + }, + { + name: 'UnresolvableDid', + }, + { + name: 'IncompatibleDidDoc', + }, + ], + }, + }, + }, + AppBskyActorDefs: { + lexicon: 1, + id: 'app.bsky.actor.defs', + description: 'A reference to an actor in the network.', + defs: { + profileViewBasic: { + type: 'object', + required: ['did', 'handle'], + properties: { + did: { + type: 'string', + format: 'did', + }, + handle: { + type: 'string', + format: 'handle', + }, + displayName: { + type: 'string', + maxGraphemes: 64, + maxLength: 640, + }, + avatar: { + type: 'string', + }, + viewer: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#viewerState', + }, + labels: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.label.defs#label', + }, + }, + }, + }, + profileView: { + type: 'object', + required: ['did', 'handle'], + properties: { + did: { + type: 'string', + format: 'did', + }, + handle: { + type: 'string', + format: 'handle', + }, + displayName: { + type: 'string', + maxGraphemes: 64, + maxLength: 640, + }, + description: { + type: 'string', + maxGraphemes: 256, + maxLength: 2560, + }, + avatar: { + type: 'string', + }, + indexedAt: { + type: 'string', + format: 'datetime', + }, + viewer: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#viewerState', + }, + labels: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.label.defs#label', + }, + }, + }, + }, + profileViewDetailed: { + type: 'object', + required: ['did', 'handle'], + properties: { + did: { + type: 'string', + format: 'did', + }, + handle: { + type: 'string', + format: 'handle', + }, + displayName: { + type: 'string', + maxGraphemes: 64, + maxLength: 640, + }, + description: { + type: 'string', + maxGraphemes: 256, + maxLength: 2560, + }, + avatar: { + type: 'string', + }, + banner: { + type: 'string', + }, + followersCount: { + type: 'integer', + }, + followsCount: { + type: 'integer', + }, + postsCount: { + type: 'integer', + }, + indexedAt: { + type: 'string', + format: 'datetime', + }, + viewer: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#viewerState', + }, + labels: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.label.defs#label', + }, + }, + }, + }, + viewerState: { + type: 'object', + properties: { + muted: { + type: 'boolean', + }, + mutedByList: { + type: 'ref', + ref: 'lex:app.bsky.graph.defs#listViewBasic', + }, + blockedBy: { + type: 'boolean', + }, + blocking: { + type: 'string', + format: 'at-uri', + }, + blockingByList: { + type: 'ref', + ref: 'lex:app.bsky.graph.defs#listViewBasic', + }, + following: { + type: 'string', + format: 'at-uri', + }, + followedBy: { + type: 'string', + format: 'at-uri', + }, + }, + }, + preferences: { + type: 'array', + items: { + type: 'union', + refs: [ + 'lex:app.bsky.actor.defs#adultContentPref', + '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', + ], + }, + }, + adultContentPref: { + type: 'object', + required: ['enabled'], + properties: { + enabled: { + type: 'boolean', + default: false, + }, + }, + }, + contentLabelPref: { + type: 'object', + required: ['label', 'visibility'], + properties: { + label: { + type: 'string', + }, + visibility: { + type: 'string', + knownValues: ['show', 'warn', 'hide'], + }, + }, + }, + savedFeedsPref: { + type: 'object', + required: ['pinned', 'saved'], + properties: { + pinned: { + type: 'array', + items: { + type: 'string', + format: 'at-uri', + }, + }, + saved: { + type: 'array', + items: { + type: 'string', + format: 'at-uri', + }, + }, + }, + }, + personalDetailsPref: { + type: 'object', + properties: { + birthDate: { + type: 'string', + format: 'datetime', + description: 'The birth date of account owner.', + }, + }, + }, + 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 for threads.', + knownValues: ['oldest', 'newest', 'most-likes', 'random'], + }, + prioritizeFollowedUsers: { + type: 'boolean', + description: 'Show followed users at the top of all replies.', + }, + }, + }, + }, + }, + AppBskyActorGetPreferences: { + lexicon: 1, + id: 'app.bsky.actor.getPreferences', + defs: { + main: { + type: 'query', + description: 'Get private preferences attached to the account.', + parameters: { + type: 'params', + properties: {}, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['preferences'], + properties: { + preferences: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#preferences', + }, + }, + }, + }, + }, + }, + }, + AppBskyActorGetProfile: { + lexicon: 1, + id: 'app.bsky.actor.getProfile', + defs: { + main: { + type: 'query', + description: 'Get detailed profile view of an actor.', + parameters: { + type: 'params', + required: ['actor'], + properties: { + actor: { + type: 'string', + format: 'at-identifier', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileViewDetailed', + }, + }, + }, + }, + }, + AppBskyActorGetProfiles: { + lexicon: 1, + id: 'app.bsky.actor.getProfiles', + defs: { + main: { + type: 'query', + description: 'Get detailed profile views of multiple actors.', + parameters: { + type: 'params', + required: ['actors'], + properties: { + actors: { + type: 'array', + items: { + type: 'string', + format: 'at-identifier', + }, + maxLength: 25, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['profiles'], + properties: { + profiles: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileViewDetailed', + }, + }, + }, + }, + }, + }, + }, + }, + AppBskyActorGetSuggestions: { + lexicon: 1, + id: 'app.bsky.actor.getSuggestions', + defs: { + main: { + type: 'query', + description: 'Get a list of suggested actors, used for discovery.', + parameters: { + type: 'params', + properties: { + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['actors'], + properties: { + cursor: { + type: 'string', + }, + actors: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileView', + }, + }, + }, + }, + }, + }, + }, + }, + AppBskyActorProfile: { + lexicon: 1, + id: 'app.bsky.actor.profile', + defs: { + main: { + type: 'record', + description: 'A declaration of a profile.', + key: 'literal:self', + record: { + type: 'object', + properties: { + displayName: { + type: 'string', + maxGraphemes: 64, + maxLength: 640, + }, + description: { + type: 'string', + maxGraphemes: 256, + maxLength: 2560, + }, + avatar: { + type: 'blob', + accept: ['image/png', 'image/jpeg'], + maxSize: 1000000, + }, + banner: { + type: 'blob', + accept: ['image/png', 'image/jpeg'], + maxSize: 1000000, + }, + labels: { + type: 'union', + refs: ['lex:com.atproto.label.defs#selfLabels'], + }, + }, + }, + }, + }, + }, + AppBskyActorPutPreferences: { + lexicon: 1, + id: 'app.bsky.actor.putPreferences', + defs: { + main: { + type: 'procedure', + description: 'Set the private preferences attached to the account.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['preferences'], + properties: { + preferences: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#preferences', + }, + }, + }, + }, + }, + }, + }, + AppBskyActorSearchActors: { + lexicon: 1, + id: 'app.bsky.actor.searchActors', + defs: { + main: { + type: 'query', + 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: 25, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['actors'], + properties: { + cursor: { + type: 'string', + }, + actors: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileView', + }, + }, + }, + }, + }, + }, + }, + }, + AppBskyActorSearchActorsTypeahead: { + lexicon: 1, + id: 'app.bsky.actor.searchActorsTypeahead', + defs: { + main: { + type: 'query', + description: 'Find actor suggestions for a prefix search term.', + parameters: { + type: 'params', + 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: 10, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['actors'], + properties: { + actors: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileViewBasic', + }, + }, + }, + }, + }, + }, + }, + }, + AppBskyEmbedExternal: { + lexicon: 1, + id: 'app.bsky.embed.external', + description: + 'A representation of some externally linked content, embedded in another form of content.', + defs: { + main: { + type: 'object', + required: ['external'], + properties: { + external: { + type: 'ref', + ref: 'lex:app.bsky.embed.external#external', + }, + }, + }, + external: { + type: 'object', + required: ['uri', 'title', 'description'], + properties: { + uri: { + type: 'string', + format: 'uri', + }, + title: { + type: 'string', + }, + description: { + type: 'string', + }, + thumb: { + type: 'blob', + accept: ['image/*'], + maxSize: 1000000, + }, + }, + }, + view: { + type: 'object', + required: ['external'], + properties: { + external: { + type: 'ref', + ref: 'lex:app.bsky.embed.external#viewExternal', + }, + }, + }, + viewExternal: { + type: 'object', + required: ['uri', 'title', 'description'], + properties: { + uri: { + type: 'string', + format: 'uri', + }, + title: { + type: 'string', + }, + description: { + type: 'string', + }, + thumb: { + type: 'string', + }, + }, + }, + }, + }, + AppBskyEmbedImages: { + lexicon: 1, + id: 'app.bsky.embed.images', + description: 'A set of images embedded in some other form of content.', + defs: { + main: { + type: 'object', + required: ['images'], + properties: { + images: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.embed.images#image', + }, + maxLength: 4, + }, + }, + }, + image: { + type: 'object', + required: ['image', 'alt'], + properties: { + image: { + type: 'blob', + accept: ['image/*'], + maxSize: 1000000, + }, + alt: { + type: 'string', + }, + aspectRatio: { + type: 'ref', + ref: 'lex:app.bsky.embed.images#aspectRatio', + }, + }, + }, + aspectRatio: { + type: 'object', + description: + 'width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit.', + required: ['width', 'height'], + properties: { + width: { + type: 'integer', + minimum: 1, + }, + height: { + type: 'integer', + minimum: 1, + }, + }, + }, + view: { + type: 'object', + required: ['images'], + properties: { + images: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.embed.images#viewImage', + }, + maxLength: 4, + }, + }, + }, + viewImage: { + type: 'object', + required: ['thumb', 'fullsize', 'alt'], + properties: { + thumb: { + type: 'string', + }, + fullsize: { + type: 'string', + }, + alt: { + type: 'string', + }, + aspectRatio: { + type: 'ref', + ref: 'lex:app.bsky.embed.images#aspectRatio', + }, + }, + }, + }, + }, + AppBskyEmbedRecord: { + lexicon: 1, + id: 'app.bsky.embed.record', + description: + 'A representation of a record embedded in another form of content.', + defs: { + main: { + type: 'object', + required: ['record'], + properties: { + record: { + type: 'ref', + ref: 'lex:com.atproto.repo.strongRef', + }, + }, + }, + view: { + type: 'object', + required: ['record'], + properties: { + record: { + type: 'union', + refs: [ + 'lex:app.bsky.embed.record#viewRecord', + 'lex:app.bsky.embed.record#viewNotFound', + 'lex:app.bsky.embed.record#viewBlocked', + 'lex:app.bsky.feed.defs#generatorView', + 'lex:app.bsky.graph.defs#listView', + ], + }, + }, + }, + viewRecord: { + type: 'object', + required: ['uri', 'cid', 'author', 'value', 'indexedAt'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + author: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileViewBasic', + }, + value: { + type: 'unknown', + }, + labels: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.label.defs#label', + }, + }, + embeds: { + type: 'array', + items: { + type: 'union', + refs: [ + 'lex:app.bsky.embed.images#view', + 'lex:app.bsky.embed.external#view', + 'lex:app.bsky.embed.record#view', + 'lex:app.bsky.embed.recordWithMedia#view', + ], + }, + }, + indexedAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + viewNotFound: { + type: 'object', + required: ['uri', 'notFound'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + notFound: { + type: 'boolean', + const: true, + }, + }, + }, + viewBlocked: { + type: 'object', + required: ['uri', 'blocked', 'author'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + blocked: { + type: 'boolean', + const: true, + }, + author: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#blockedAuthor', + }, + }, + }, + }, + }, + AppBskyEmbedRecordWithMedia: { + lexicon: 1, + id: 'app.bsky.embed.recordWithMedia', + description: + 'A representation of a record embedded in another form of content, alongside other compatible embeds.', + defs: { + main: { + type: 'object', + required: ['record', 'media'], + properties: { + record: { + type: 'ref', + ref: 'lex:app.bsky.embed.record', + }, + media: { + type: 'union', + refs: ['lex:app.bsky.embed.images', 'lex:app.bsky.embed.external'], + }, + }, + }, + view: { + type: 'object', + required: ['record', 'media'], + properties: { + record: { + type: 'ref', + ref: 'lex:app.bsky.embed.record#view', + }, + media: { + type: 'union', + refs: [ + 'lex:app.bsky.embed.images#view', + 'lex:app.bsky.embed.external#view', + ], + }, + }, + }, + }, + }, + AppBskyFeedDefs: { + lexicon: 1, + id: 'app.bsky.feed.defs', + defs: { + postView: { + type: 'object', + required: ['uri', 'cid', 'author', 'record', 'indexedAt'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + author: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileViewBasic', + }, + record: { + type: 'unknown', + }, + embed: { + type: 'union', + refs: [ + 'lex:app.bsky.embed.images#view', + 'lex:app.bsky.embed.external#view', + 'lex:app.bsky.embed.record#view', + 'lex:app.bsky.embed.recordWithMedia#view', + ], + }, + replyCount: { + type: 'integer', + }, + repostCount: { + type: 'integer', + }, + likeCount: { + type: 'integer', + }, + indexedAt: { + type: 'string', + format: 'datetime', + }, + viewer: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#viewerState', + }, + labels: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.label.defs#label', + }, + }, + threadgate: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#threadgateView', + }, + }, + }, + viewerState: { + type: 'object', + properties: { + repost: { + type: 'string', + format: 'at-uri', + }, + like: { + type: 'string', + format: 'at-uri', + }, + replyDisabled: { + type: 'boolean', + }, + }, + }, + feedViewPost: { + type: 'object', + required: ['post'], + properties: { + post: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#postView', + }, + reply: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#replyRef', + }, + reason: { + type: 'union', + refs: ['lex:app.bsky.feed.defs#reasonRepost'], + }, + }, + }, + replyRef: { + type: 'object', + required: ['root', 'parent'], + properties: { + root: { + type: 'union', + refs: [ + 'lex:app.bsky.feed.defs#postView', + 'lex:app.bsky.feed.defs#notFoundPost', + 'lex:app.bsky.feed.defs#blockedPost', + ], + }, + parent: { + type: 'union', + refs: [ + 'lex:app.bsky.feed.defs#postView', + 'lex:app.bsky.feed.defs#notFoundPost', + 'lex:app.bsky.feed.defs#blockedPost', + ], + }, + }, + }, + reasonRepost: { + type: 'object', + required: ['by', 'indexedAt'], + properties: { + by: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileViewBasic', + }, + indexedAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + threadViewPost: { + type: 'object', + required: ['post'], + properties: { + post: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#postView', + }, + parent: { + type: 'union', + refs: [ + 'lex:app.bsky.feed.defs#threadViewPost', + 'lex:app.bsky.feed.defs#notFoundPost', + 'lex:app.bsky.feed.defs#blockedPost', + ], + }, + replies: { + type: 'array', + items: { + type: 'union', + refs: [ + 'lex:app.bsky.feed.defs#threadViewPost', + 'lex:app.bsky.feed.defs#notFoundPost', + 'lex:app.bsky.feed.defs#blockedPost', + ], + }, + }, + }, + }, + notFoundPost: { + type: 'object', + required: ['uri', 'notFound'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + notFound: { + type: 'boolean', + const: true, + }, + }, + }, + blockedPost: { + type: 'object', + required: ['uri', 'blocked', 'author'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + blocked: { + type: 'boolean', + const: true, + }, + author: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#blockedAuthor', + }, + }, + }, + blockedAuthor: { + type: 'object', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + }, + viewer: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#viewerState', + }, + }, + }, + generatorView: { + type: 'object', + required: ['uri', 'cid', 'did', 'creator', 'displayName', 'indexedAt'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + did: { + type: 'string', + format: 'did', + }, + creator: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileView', + }, + displayName: { + type: 'string', + }, + description: { + type: 'string', + maxGraphemes: 300, + maxLength: 3000, + }, + descriptionFacets: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.richtext.facet', + }, + }, + avatar: { + type: 'string', + }, + likeCount: { + type: 'integer', + minimum: 0, + }, + viewer: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#generatorViewerState', + }, + indexedAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + generatorViewerState: { + type: 'object', + properties: { + like: { + type: 'string', + format: 'at-uri', + }, + }, + }, + skeletonFeedPost: { + type: 'object', + required: ['post'], + properties: { + post: { + type: 'string', + format: 'at-uri', + }, + reason: { + type: 'union', + refs: ['lex:app.bsky.feed.defs#skeletonReasonRepost'], + }, + }, + }, + skeletonReasonRepost: { + type: 'object', + required: ['repost'], + 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: 'lex:app.bsky.graph.defs#listViewBasic', + }, + }, + }, + }, + }, + }, + AppBskyFeedDescribeFeedGenerator: { + lexicon: 1, + id: 'app.bsky.feed.describeFeedGenerator', + defs: { + main: { + type: 'query', + description: + 'Get information about a feed generator, including policies and offered feed URIs.', + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['did', 'feeds'], + properties: { + did: { + type: 'string', + format: 'did', + }, + feeds: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.feed.describeFeedGenerator#feed', + }, + }, + links: { + type: 'ref', + ref: 'lex:app.bsky.feed.describeFeedGenerator#links', + }, + }, + }, + }, + }, + feed: { + type: 'object', + required: ['uri'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + }, + }, + links: { + type: 'object', + properties: { + privacyPolicy: { + type: 'string', + }, + termsOfService: { + type: 'string', + }, + }, + }, + }, + }, + AppBskyFeedGenerator: { + lexicon: 1, + id: 'app.bsky.feed.generator', + defs: { + main: { + type: 'record', + description: 'A declaration of the existence of a feed generator.', + key: 'any', + record: { + type: 'object', + required: ['did', 'displayName', 'createdAt'], + properties: { + did: { + type: 'string', + format: 'did', + }, + displayName: { + type: 'string', + maxGraphemes: 24, + maxLength: 240, + }, + description: { + type: 'string', + maxGraphemes: 300, + maxLength: 3000, + }, + descriptionFacets: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.richtext.facet', + }, + }, + avatar: { + type: 'blob', + accept: ['image/png', 'image/jpeg'], + maxSize: 1000000, + }, + labels: { + type: 'union', + refs: ['lex:com.atproto.label.defs#selfLabels'], + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + }, + }, + AppBskyFeedGetActorFeeds: { + lexicon: 1, + id: 'app.bsky.feed.getActorFeeds', + defs: { + main: { + type: 'query', + description: 'Get a list of feeds created by the actor.', + parameters: { + type: 'params', + required: ['actor'], + properties: { + actor: { + type: 'string', + format: 'at-identifier', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['feeds'], + properties: { + cursor: { + type: 'string', + }, + feeds: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#generatorView', + }, + }, + }, + }, + }, + }, + }, + }, + AppBskyFeedGetActorLikes: { + lexicon: 1, + id: 'app.bsky.feed.getActorLikes', + defs: { + main: { + type: 'query', + description: 'Get a list of posts liked by an actor.', + parameters: { + type: 'params', + required: ['actor'], + properties: { + actor: { + type: 'string', + format: 'at-identifier', + }, + 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: 'BlockedActor', + }, + { + name: 'BlockedByActor', + }, + ], + }, + }, + }, + AppBskyFeedGetAuthorFeed: { + lexicon: 1, + id: 'app.bsky.feed.getAuthorFeed', + defs: { + main: { + type: 'query', + description: "Get a view of an actor's feed.", + parameters: { + type: 'params', + required: ['actor'], + properties: { + actor: { + type: 'string', + format: 'at-identifier', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + filter: { + type: 'string', + knownValues: [ + 'posts_with_replies', + 'posts_no_replies', + 'posts_with_media', + 'posts_and_author_threads', + ], + default: 'posts_with_replies', + }, + }, + }, + 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: 'BlockedActor', + }, + { + name: 'BlockedByActor', + }, + ], + }, + }, + }, + AppBskyFeedGetFeed: { + lexicon: 1, + id: 'app.bsky.feed.getFeed', + defs: { + main: { + type: 'query', + description: + "Get a hydrated feed from an actor's selected feed generator.", + parameters: { + type: 'params', + required: ['feed'], + properties: { + feed: { + 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: 'UnknownFeed', + }, + ], + }, + }, + }, + AppBskyFeedGetFeedGenerator: { + lexicon: 1, + id: 'app.bsky.feed.getFeedGenerator', + defs: { + main: { + type: 'query', + description: 'Get information about a feed generator.', + parameters: { + type: 'params', + required: ['feed'], + properties: { + feed: { + type: 'string', + format: 'at-uri', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['view', 'isOnline', 'isValid'], + properties: { + view: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#generatorView', + }, + isOnline: { + type: 'boolean', + }, + isValid: { + type: 'boolean', + }, + }, + }, + }, + }, + }, + }, + AppBskyFeedGetFeedGenerators: { + lexicon: 1, + id: 'app.bsky.feed.getFeedGenerators', + defs: { + main: { + type: 'query', + description: 'Get information about a list of feed generators.', + parameters: { + type: 'params', + required: ['feeds'], + properties: { + feeds: { + type: 'array', + items: { + type: 'string', + format: 'at-uri', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['feeds'], + properties: { + feeds: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#generatorView', + }, + }, + }, + }, + }, + }, + }, + }, + AppBskyFeedGetFeedSkeleton: { + lexicon: 1, + id: 'app.bsky.feed.getFeedSkeleton', + defs: { + main: { + type: 'query', + description: 'Get a skeleton of a feed provided by a feed generator.', + parameters: { + type: 'params', + required: ['feed'], + properties: { + feed: { + 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#skeletonFeedPost', + }, + }, + }, + }, + }, + errors: [ + { + name: 'UnknownFeed', + }, + ], + }, + }, + }, + AppBskyFeedGetLikes: { + lexicon: 1, + id: 'app.bsky.feed.getLikes', + defs: { + main: { + type: 'query', + description: 'Get the list of likes.', + parameters: { + type: 'params', + required: ['uri'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['uri', 'likes'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + cursor: { + type: 'string', + }, + likes: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.feed.getLikes#like', + }, + }, + }, + }, + }, + }, + like: { + type: 'object', + required: ['indexedAt', 'createdAt', 'actor'], + properties: { + indexedAt: { + type: 'string', + format: 'datetime', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + actor: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileView', + }, + }, + }, + }, + }, + AppBskyFeedGetListFeed: { + lexicon: 1, + id: 'app.bsky.feed.getListFeed', + defs: { + main: { + type: 'query', + description: 'Get 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', + defs: { + main: { + type: 'query', + description: 'Get posts in a thread.', + parameters: { + type: 'params', + required: ['uri'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + depth: { + type: 'integer', + default: 6, + minimum: 0, + maximum: 1000, + }, + parentHeight: { + type: 'integer', + default: 80, + minimum: 0, + maximum: 1000, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['thread'], + properties: { + thread: { + type: 'union', + refs: [ + 'lex:app.bsky.feed.defs#threadViewPost', + 'lex:app.bsky.feed.defs#notFoundPost', + 'lex:app.bsky.feed.defs#blockedPost', + ], + }, + }, + }, + }, + errors: [ + { + name: 'NotFound', + }, + ], + }, + }, + }, + AppBskyFeedGetPosts: { + lexicon: 1, + id: 'app.bsky.feed.getPosts', + defs: { + main: { + type: 'query', + description: "Get a view of an actor's feed.", + parameters: { + type: 'params', + required: ['uris'], + properties: { + uris: { + type: 'array', + items: { + type: 'string', + format: 'at-uri', + }, + maxLength: 25, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['posts'], + properties: { + posts: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#postView', + }, + }, + }, + }, + }, + }, + }, + }, + AppBskyFeedGetRepostedBy: { + lexicon: 1, + id: 'app.bsky.feed.getRepostedBy', + defs: { + main: { + type: 'query', + description: 'Get a list of reposts.', + parameters: { + type: 'params', + required: ['uri'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['uri', 'repostedBy'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + cursor: { + type: 'string', + }, + repostedBy: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileView', + }, + }, + }, + }, + }, + }, + }, + }, + AppBskyFeedGetSuggestedFeeds: { + lexicon: 1, + id: 'app.bsky.feed.getSuggestedFeeds', + defs: { + main: { + type: 'query', + description: 'Get a list of suggested feeds for the viewer.', + parameters: { + type: 'params', + properties: { + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['feeds'], + properties: { + cursor: { + type: 'string', + }, + feeds: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#generatorView', + }, + }, + }, + }, + }, + }, + }, + }, + AppBskyFeedGetTimeline: { + lexicon: 1, + id: 'app.bsky.feed.getTimeline', + defs: { + main: { + type: 'query', + description: "Get a view of the actor's home timeline.", + parameters: { + type: 'params', + properties: { + algorithm: { + type: 'string', + }, + 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', + }, + }, + }, + }, + }, + }, + }, + }, + AppBskyFeedLike: { + lexicon: 1, + id: 'app.bsky.feed.like', + defs: { + main: { + type: 'record', + description: 'A declaration of a like.', + key: 'tid', + record: { + type: 'object', + required: ['subject', 'createdAt'], + properties: { + subject: { + type: 'ref', + ref: 'lex:com.atproto.repo.strongRef', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + }, + }, + AppBskyFeedPost: { + lexicon: 1, + id: 'app.bsky.feed.post', + defs: { + main: { + type: 'record', + description: 'A declaration of a post.', + key: 'tid', + record: { + type: 'object', + required: ['text', 'createdAt'], + properties: { + text: { + type: 'string', + maxLength: 3000, + maxGraphemes: 300, + }, + entities: { + type: 'array', + description: 'Deprecated: replaced by app.bsky.richtext.facet.', + items: { + type: 'ref', + ref: 'lex:app.bsky.feed.post#entity', + }, + }, + facets: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.richtext.facet', + }, + }, + reply: { + type: 'ref', + ref: 'lex:app.bsky.feed.post#replyRef', + }, + embed: { + type: 'union', + refs: [ + 'lex:app.bsky.embed.images', + 'lex:app.bsky.embed.external', + 'lex:app.bsky.embed.record', + 'lex:app.bsky.embed.recordWithMedia', + ], + }, + langs: { + type: 'array', + maxLength: 3, + items: { + type: 'string', + format: 'language', + }, + }, + labels: { + 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', + }, + }, + }, + }, + replyRef: { + type: 'object', + required: ['root', 'parent'], + properties: { + root: { + type: 'ref', + ref: 'lex:com.atproto.repo.strongRef', + }, + parent: { + type: 'ref', + ref: 'lex:com.atproto.repo.strongRef', + }, + }, + }, + entity: { + type: 'object', + description: 'Deprecated: use facets instead.', + required: ['index', 'type', 'value'], + properties: { + index: { + type: 'ref', + ref: 'lex:app.bsky.feed.post#textSlice', + }, + type: { + type: 'string', + description: "Expected values are 'mention' and 'link'.", + }, + value: { + type: 'string', + }, + }, + }, + textSlice: { + type: 'object', + description: + 'Deprecated. Use app.bsky.richtext instead -- A text segment. Start is inclusive, end is exclusive. Indices are for utf16-encoded strings.', + required: ['start', 'end'], + properties: { + start: { + type: 'integer', + minimum: 0, + }, + end: { + type: 'integer', + minimum: 0, + }, + }, + }, + }, + }, + AppBskyFeedRepost: { + lexicon: 1, + id: 'app.bsky.feed.repost', + defs: { + main: { + description: 'A declaration of a repost.', + type: 'record', + key: 'tid', + record: { + type: 'object', + required: ['subject', 'createdAt'], + properties: { + subject: { + type: 'ref', + ref: 'lex:com.atproto.repo.strongRef', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + }, + }, + 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', + defs: { + main: { + type: 'record', + description: 'A declaration of a block.', + key: 'tid', + record: { + type: 'object', + required: ['subject', 'createdAt'], + properties: { + subject: { + type: 'string', + format: 'did', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + }, + }, + AppBskyGraphDefs: { + lexicon: 1, + id: 'app.bsky.graph.defs', + defs: { + listViewBasic: { + type: 'object', + required: ['uri', 'cid', 'name', 'purpose'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + name: { + type: 'string', + maxLength: 64, + minLength: 1, + }, + purpose: { + type: 'ref', + ref: 'lex:app.bsky.graph.defs#listPurpose', + }, + avatar: { + type: 'string', + }, + viewer: { + type: 'ref', + ref: 'lex:app.bsky.graph.defs#listViewerState', + }, + indexedAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + listView: { + type: 'object', + required: ['uri', 'cid', 'creator', 'name', 'purpose', 'indexedAt'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + creator: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileView', + }, + name: { + type: 'string', + maxLength: 64, + minLength: 1, + }, + purpose: { + type: 'ref', + ref: 'lex:app.bsky.graph.defs#listPurpose', + }, + description: { + type: 'string', + maxGraphemes: 300, + maxLength: 3000, + }, + descriptionFacets: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.richtext.facet', + }, + }, + avatar: { + type: 'string', + }, + viewer: { + type: 'ref', + ref: 'lex:app.bsky.graph.defs#listViewerState', + }, + indexedAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + listItemView: { + type: 'object', + required: ['uri', 'subject'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + subject: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileView', + }, + }, + }, + listPurpose: { + type: 'string', + 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: { + muted: { + type: 'boolean', + }, + blocked: { + type: 'string', + format: 'at-uri', + }, + }, + }, + }, + }, + AppBskyGraphFollow: { + lexicon: 1, + id: 'app.bsky.graph.follow', + defs: { + main: { + type: 'record', + description: 'A declaration of a social follow.', + key: 'tid', + record: { + type: 'object', + required: ['subject', 'createdAt'], + properties: { + subject: { + type: 'string', + format: 'did', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + }, + }, + AppBskyGraphGetBlocks: { + lexicon: 1, + id: 'app.bsky.graph.getBlocks', + defs: { + main: { + type: 'query', + description: 'Get a list of who the actor is blocking.', + parameters: { + type: 'params', + properties: { + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['blocks'], + properties: { + cursor: { + type: 'string', + }, + blocks: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileView', + }, + }, + }, + }, + }, + }, + }, + }, + AppBskyGraphGetFollowers: { + lexicon: 1, + id: 'app.bsky.graph.getFollowers', + defs: { + main: { + type: 'query', + description: "Get a list of an actor's followers.", + parameters: { + type: 'params', + required: ['actor'], + properties: { + actor: { + type: 'string', + format: 'at-identifier', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['subject', 'followers'], + properties: { + subject: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileView', + }, + cursor: { + type: 'string', + }, + followers: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileView', + }, + }, + }, + }, + }, + }, + }, + }, + AppBskyGraphGetFollows: { + lexicon: 1, + id: 'app.bsky.graph.getFollows', + defs: { + main: { + type: 'query', + description: 'Get a list of who the actor follows.', + parameters: { + type: 'params', + required: ['actor'], + properties: { + actor: { + type: 'string', + format: 'at-identifier', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['subject', 'follows'], + properties: { + subject: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileView', + }, + cursor: { + type: 'string', + }, + follows: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileView', + }, + }, + }, + }, + }, + }, + }, + }, + AppBskyGraphGetList: { + lexicon: 1, + id: 'app.bsky.graph.getList', + defs: { + main: { + type: 'query', + description: 'Get a list of actors.', + 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: ['list', 'items'], + properties: { + cursor: { + type: 'string', + }, + list: { + type: 'ref', + ref: 'lex:app.bsky.graph.defs#listView', + }, + items: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.graph.defs#listItemView', + }, + }, + }, + }, + }, + }, + }, + }, + AppBskyGraphGetListBlocks: { + lexicon: 1, + id: 'app.bsky.graph.getListBlocks', + defs: { + main: { + type: 'query', + description: 'Get lists that the actor is blocking.', + parameters: { + type: 'params', + properties: { + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['lists'], + properties: { + cursor: { + type: 'string', + }, + lists: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.graph.defs#listView', + }, + }, + }, + }, + }, + }, + }, + }, + AppBskyGraphGetListMutes: { + lexicon: 1, + id: 'app.bsky.graph.getListMutes', + defs: { + main: { + type: 'query', + description: 'Get lists that the actor is muting.', + parameters: { + type: 'params', + properties: { + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['lists'], + properties: { + cursor: { + type: 'string', + }, + lists: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.graph.defs#listView', + }, + }, + }, + }, + }, + }, + }, + }, + AppBskyGraphGetLists: { + lexicon: 1, + id: 'app.bsky.graph.getLists', + defs: { + main: { + type: 'query', + description: 'Get a list of lists that belong to an actor.', + parameters: { + type: 'params', + required: ['actor'], + properties: { + actor: { + type: 'string', + format: 'at-identifier', + }, + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['lists'], + properties: { + cursor: { + type: 'string', + }, + lists: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.graph.defs#listView', + }, + }, + }, + }, + }, + }, + }, + }, + AppBskyGraphGetMutes: { + lexicon: 1, + id: 'app.bsky.graph.getMutes', + defs: { + main: { + type: 'query', + description: 'Get a list of who the actor mutes.', + parameters: { + type: 'params', + properties: { + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['mutes'], + properties: { + cursor: { + type: 'string', + }, + mutes: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileView', + }, + }, + }, + }, + }, + }, + }, + }, + AppBskyGraphGetSuggestedFollowsByActor: { + lexicon: 1, + id: 'app.bsky.graph.getSuggestedFollowsByActor', + defs: { + main: { + type: 'query', + description: 'Get suggested follows related to a given actor.', + parameters: { + type: 'params', + required: ['actor'], + properties: { + actor: { + type: 'string', + format: 'at-identifier', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['suggestions'], + properties: { + suggestions: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileView', + }, + }, + }, + }, + }, + }, + }, + }, + AppBskyGraphList: { + lexicon: 1, + id: 'app.bsky.graph.list', + defs: { + main: { + type: 'record', + description: 'A declaration of a list of actors.', + key: 'tid', + record: { + type: 'object', + required: ['name', 'purpose', 'createdAt'], + properties: { + purpose: { + type: 'ref', + ref: 'lex:app.bsky.graph.defs#listPurpose', + }, + name: { + type: 'string', + maxLength: 64, + minLength: 1, + }, + description: { + type: 'string', + maxGraphemes: 300, + maxLength: 3000, + }, + descriptionFacets: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.richtext.facet', + }, + }, + avatar: { + type: 'blob', + accept: ['image/png', 'image/jpeg'], + maxSize: 1000000, + }, + labels: { + type: 'union', + refs: ['lex:com.atproto.label.defs#selfLabels'], + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + }, + }, + AppBskyGraphListblock: { + lexicon: 1, + id: 'app.bsky.graph.listblock', + defs: { + main: { + type: 'record', + description: 'A block of an entire list of actors.', + key: 'tid', + record: { + type: 'object', + required: ['subject', 'createdAt'], + properties: { + subject: { + type: 'string', + format: 'at-uri', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + }, + }, + AppBskyGraphListitem: { + lexicon: 1, + id: 'app.bsky.graph.listitem', + defs: { + main: { + type: 'record', + description: 'An item under a declared list of actors.', + key: 'tid', + record: { + type: 'object', + required: ['subject', 'list', 'createdAt'], + properties: { + subject: { + type: 'string', + format: 'did', + }, + list: { + type: 'string', + format: 'at-uri', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + }, + }, + AppBskyGraphMuteActor: { + lexicon: 1, + id: 'app.bsky.graph.muteActor', + defs: { + main: { + type: 'procedure', + description: 'Mute an actor by DID or handle.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['actor'], + properties: { + actor: { + type: 'string', + format: 'at-identifier', + }, + }, + }, + }, + }, + }, + }, + AppBskyGraphMuteActorList: { + lexicon: 1, + id: 'app.bsky.graph.muteActorList', + defs: { + main: { + type: 'procedure', + description: 'Mute a list of actors.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['list'], + properties: { + list: { + type: 'string', + format: 'at-uri', + }, + }, + }, + }, + }, + }, + }, + AppBskyGraphUnmuteActor: { + lexicon: 1, + id: 'app.bsky.graph.unmuteActor', + defs: { + main: { + type: 'procedure', + description: 'Unmute an actor by DID or handle.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['actor'], + properties: { + actor: { + type: 'string', + format: 'at-identifier', + }, + }, + }, + }, + }, + }, + }, + AppBskyGraphUnmuteActorList: { + lexicon: 1, + id: 'app.bsky.graph.unmuteActorList', + defs: { + main: { + type: 'procedure', + description: 'Unmute a list of actors.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['list'], + properties: { + list: { + type: 'string', + format: 'at-uri', + }, + }, + }, + }, + }, + }, + }, + AppBskyNotificationGetUnreadCount: { + lexicon: 1, + id: 'app.bsky.notification.getUnreadCount', + defs: { + main: { + type: 'query', + description: 'Get the count of unread notifications.', + parameters: { + type: 'params', + properties: { + seenAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['count'], + properties: { + count: { + type: 'integer', + }, + }, + }, + }, + }, + }, + }, + AppBskyNotificationListNotifications: { + lexicon: 1, + id: 'app.bsky.notification.listNotifications', + defs: { + main: { + type: 'query', + description: 'Get a list of notifications.', + parameters: { + type: 'params', + properties: { + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + seenAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['notifications'], + properties: { + cursor: { + type: 'string', + }, + notifications: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.notification.listNotifications#notification', + }, + }, + seenAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + }, + notification: { + type: 'object', + required: [ + 'uri', + 'cid', + 'author', + 'reason', + 'record', + 'isRead', + 'indexedAt', + ], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + cid: { + type: 'string', + format: 'cid', + }, + author: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileView', + }, + reason: { + type: 'string', + description: + "Expected values are 'like', 'repost', 'follow', 'mention', 'reply', and 'quote'.", + knownValues: [ + 'like', + 'repost', + 'follow', + 'mention', + 'reply', + 'quote', + ], + }, + reasonSubject: { + type: 'string', + format: 'at-uri', + }, + record: { + type: 'unknown', + }, + isRead: { + type: 'boolean', + }, + indexedAt: { + type: 'string', + format: 'datetime', + }, + labels: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.label.defs#label', + }, + }, + }, + }, + }, + }, + AppBskyNotificationRegisterPush: { + lexicon: 1, + id: 'app.bsky.notification.registerPush', + defs: { + main: { + type: 'procedure', + description: 'Register for push notifications with a service.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['serviceDid', 'token', 'platform', 'appId'], + properties: { + serviceDid: { + type: 'string', + format: 'did', + }, + token: { + type: 'string', + }, + platform: { + type: 'string', + knownValues: ['ios', 'android', 'web'], + }, + appId: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + AppBskyNotificationUpdateSeen: { + lexicon: 1, + id: 'app.bsky.notification.updateSeen', + defs: { + main: { + type: 'procedure', + description: 'Notify server that the user has seen notifications.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['seenAt'], + properties: { + seenAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + }, + }, + }, + }, + AppBskyRichtextFacet: { + lexicon: 1, + id: 'app.bsky.richtext.facet', + defs: { + main: { + type: 'object', + required: ['index', 'features'], + properties: { + index: { + type: 'ref', + ref: 'lex:app.bsky.richtext.facet#byteSlice', + }, + features: { + type: 'array', + items: { + type: 'union', + refs: [ + 'lex:app.bsky.richtext.facet#mention', + 'lex:app.bsky.richtext.facet#link', + 'lex:app.bsky.richtext.facet#tag', + ], + }, + }, + }, + }, + mention: { + type: 'object', + description: 'A facet feature for actor mentions.', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + }, + }, + }, + link: { + type: 'object', + description: 'A facet feature for links.', + required: ['uri'], + properties: { + 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.', + required: ['byteStart', 'byteEnd'], + properties: { + byteStart: { + type: 'integer', + minimum: 0, + }, + byteEnd: { + type: 'integer', + minimum: 0, + }, + }, + }, + }, + }, + AppBskyUnspeccedDefs: { + lexicon: 1, + id: 'app.bsky.unspecced.defs', + defs: { + skeletonSearchPost: { + type: 'object', + required: ['uri'], + properties: { + uri: { + type: 'string', + format: 'at-uri', + }, + }, + }, + skeletonSearchActor: { + type: 'object', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + }, + }, + }, + }, + }, + AppBskyUnspeccedGetPopular: { + lexicon: 1, + id: 'app.bsky.unspecced.getPopular', + defs: { + main: { + type: 'query', + description: + 'DEPRECATED: will be removed soon. Use a feed generator alternative.', + parameters: { + type: 'params', + properties: { + includeNsfw: { + type: 'boolean', + default: false, + }, + 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', + }, + }, + }, + }, + }, + }, + }, + }, + AppBskyUnspeccedGetPopularFeedGenerators: { + lexicon: 1, + id: 'app.bsky.unspecced.getPopularFeedGenerators', + defs: { + main: { + type: 'query', + description: 'An unspecced view of globally popular feed generators.', + parameters: { + type: 'params', + properties: { + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, + }, + cursor: { + type: 'string', + }, + query: { + type: 'string', + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['feeds'], + properties: { + cursor: { + type: 'string', + }, + feeds: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:app.bsky.feed.defs#generatorView', + }, + }, + }, + }, + }, + }, + }, + }, + AppBskyUnspeccedGetTimelineSkeleton: { + lexicon: 1, + id: 'app.bsky.unspecced.getTimelineSkeleton', + defs: { + main: { + type: 'query', + description: + 'DEPRECATED: a skeleton of a timeline. Unspecced and will be unavailable soon.', + parameters: { + type: 'params', + properties: { + 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#skeletonFeedPost', + }, + }, + }, + }, + }, + errors: [ + { + name: 'UnknownFeed', + }, + ], + }, + }, + }, + AppBskyUnspeccedSearchActorsSkeleton: { + lexicon: 1, + id: 'app.bsky.unspecced.searchActorsSkeleton', + defs: { + main: { + type: 'query', + description: 'Backend Actors (profile) search, returns 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, returns 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) +export const ids = { + ComAtprotoAdminDefs: 'com.atproto.admin.defs', + ComAtprotoAdminDeleteAccount: 'com.atproto.admin.deleteAccount', + ComAtprotoAdminDisableAccountInvites: + 'com.atproto.admin.disableAccountInvites', + ComAtprotoAdminDisableInviteCodes: 'com.atproto.admin.disableInviteCodes', + ComAtprotoAdminEmitModerationEvent: 'com.atproto.admin.emitModerationEvent', + ComAtprotoAdminEnableAccountInvites: 'com.atproto.admin.enableAccountInvites', + ComAtprotoAdminGetAccountInfo: 'com.atproto.admin.getAccountInfo', + ComAtprotoAdminGetAccountInfos: 'com.atproto.admin.getAccountInfos', + ComAtprotoAdminGetInviteCodes: 'com.atproto.admin.getInviteCodes', + ComAtprotoAdminGetModerationEvent: 'com.atproto.admin.getModerationEvent', + ComAtprotoAdminGetRecord: 'com.atproto.admin.getRecord', + ComAtprotoAdminGetRepo: 'com.atproto.admin.getRepo', + ComAtprotoAdminGetSubjectStatus: 'com.atproto.admin.getSubjectStatus', + ComAtprotoAdminQueryModerationEvents: + 'com.atproto.admin.queryModerationEvents', + ComAtprotoAdminQueryModerationStatuses: + 'com.atproto.admin.queryModerationStatuses', + ComAtprotoAdminSearchRepos: 'com.atproto.admin.searchRepos', + ComAtprotoAdminSendEmail: 'com.atproto.admin.sendEmail', + ComAtprotoAdminUpdateAccountEmail: 'com.atproto.admin.updateAccountEmail', + ComAtprotoAdminUpdateAccountHandle: 'com.atproto.admin.updateAccountHandle', + ComAtprotoAdminUpdateSubjectStatus: 'com.atproto.admin.updateSubjectStatus', + ComAtprotoIdentityResolveHandle: 'com.atproto.identity.resolveHandle', + ComAtprotoIdentityUpdateHandle: 'com.atproto.identity.updateHandle', + ComAtprotoLabelDefs: 'com.atproto.label.defs', + ComAtprotoLabelQueryLabels: 'com.atproto.label.queryLabels', + ComAtprotoLabelSubscribeLabels: 'com.atproto.label.subscribeLabels', + ComAtprotoModerationCreateReport: 'com.atproto.moderation.createReport', + ComAtprotoModerationDefs: 'com.atproto.moderation.defs', + ComAtprotoRepoApplyWrites: 'com.atproto.repo.applyWrites', + ComAtprotoRepoCreateRecord: 'com.atproto.repo.createRecord', + ComAtprotoRepoDeleteRecord: 'com.atproto.repo.deleteRecord', + ComAtprotoRepoDescribeRepo: 'com.atproto.repo.describeRepo', + ComAtprotoRepoGetRecord: 'com.atproto.repo.getRecord', + ComAtprotoRepoListRecords: 'com.atproto.repo.listRecords', + ComAtprotoRepoPutRecord: 'com.atproto.repo.putRecord', + ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', + ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', + ComAtprotoServerConfirmEmail: 'com.atproto.server.confirmEmail', + ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount', + ComAtprotoServerCreateAppPassword: 'com.atproto.server.createAppPassword', + ComAtprotoServerCreateInviteCode: 'com.atproto.server.createInviteCode', + ComAtprotoServerCreateInviteCodes: 'com.atproto.server.createInviteCodes', + ComAtprotoServerCreateSession: 'com.atproto.server.createSession', + ComAtprotoServerDefs: 'com.atproto.server.defs', + ComAtprotoServerDeleteAccount: 'com.atproto.server.deleteAccount', + ComAtprotoServerDeleteSession: 'com.atproto.server.deleteSession', + ComAtprotoServerDescribeServer: 'com.atproto.server.describeServer', + ComAtprotoServerGetAccountInviteCodes: + 'com.atproto.server.getAccountInviteCodes', + ComAtprotoServerGetSession: 'com.atproto.server.getSession', + ComAtprotoServerListAppPasswords: 'com.atproto.server.listAppPasswords', + ComAtprotoServerRefreshSession: 'com.atproto.server.refreshSession', + ComAtprotoServerRequestAccountDelete: + 'com.atproto.server.requestAccountDelete', + ComAtprotoServerRequestEmailConfirmation: + 'com.atproto.server.requestEmailConfirmation', + ComAtprotoServerRequestEmailUpdate: 'com.atproto.server.requestEmailUpdate', + ComAtprotoServerRequestPasswordReset: + 'com.atproto.server.requestPasswordReset', + ComAtprotoServerReserveSigningKey: 'com.atproto.server.reserveSigningKey', + ComAtprotoServerResetPassword: 'com.atproto.server.resetPassword', + ComAtprotoServerRevokeAppPassword: 'com.atproto.server.revokeAppPassword', + ComAtprotoServerUpdateEmail: 'com.atproto.server.updateEmail', + ComAtprotoSyncGetBlob: 'com.atproto.sync.getBlob', + ComAtprotoSyncGetBlocks: 'com.atproto.sync.getBlocks', + ComAtprotoSyncGetCheckout: 'com.atproto.sync.getCheckout', + ComAtprotoSyncGetHead: 'com.atproto.sync.getHead', + ComAtprotoSyncGetLatestCommit: 'com.atproto.sync.getLatestCommit', + ComAtprotoSyncGetRecord: 'com.atproto.sync.getRecord', + ComAtprotoSyncGetRepo: 'com.atproto.sync.getRepo', + ComAtprotoSyncListBlobs: 'com.atproto.sync.listBlobs', + ComAtprotoSyncListRepos: 'com.atproto.sync.listRepos', + ComAtprotoSyncNotifyOfUpdate: 'com.atproto.sync.notifyOfUpdate', + ComAtprotoSyncRequestCrawl: 'com.atproto.sync.requestCrawl', + ComAtprotoSyncSubscribeRepos: 'com.atproto.sync.subscribeRepos', + ComAtprotoTempFetchLabels: 'com.atproto.temp.fetchLabels', + ComAtprotoTempImportRepo: 'com.atproto.temp.importRepo', + ComAtprotoTempPushBlob: 'com.atproto.temp.pushBlob', + ComAtprotoTempTransferAccount: 'com.atproto.temp.transferAccount', + AppBskyActorDefs: 'app.bsky.actor.defs', + AppBskyActorGetPreferences: 'app.bsky.actor.getPreferences', + AppBskyActorGetProfile: 'app.bsky.actor.getProfile', + AppBskyActorGetProfiles: 'app.bsky.actor.getProfiles', + AppBskyActorGetSuggestions: 'app.bsky.actor.getSuggestions', + AppBskyActorProfile: 'app.bsky.actor.profile', + AppBskyActorPutPreferences: 'app.bsky.actor.putPreferences', + AppBskyActorSearchActors: 'app.bsky.actor.searchActors', + AppBskyActorSearchActorsTypeahead: 'app.bsky.actor.searchActorsTypeahead', + AppBskyEmbedExternal: 'app.bsky.embed.external', + AppBskyEmbedImages: 'app.bsky.embed.images', + AppBskyEmbedRecord: 'app.bsky.embed.record', + AppBskyEmbedRecordWithMedia: 'app.bsky.embed.recordWithMedia', + AppBskyFeedDefs: 'app.bsky.feed.defs', + AppBskyFeedDescribeFeedGenerator: 'app.bsky.feed.describeFeedGenerator', + AppBskyFeedGenerator: 'app.bsky.feed.generator', + AppBskyFeedGetActorFeeds: 'app.bsky.feed.getActorFeeds', + AppBskyFeedGetActorLikes: 'app.bsky.feed.getActorLikes', + AppBskyFeedGetAuthorFeed: 'app.bsky.feed.getAuthorFeed', + AppBskyFeedGetFeed: 'app.bsky.feed.getFeed', + AppBskyFeedGetFeedGenerator: 'app.bsky.feed.getFeedGenerator', + 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', + AppBskyFeedGetSuggestedFeeds: 'app.bsky.feed.getSuggestedFeeds', + AppBskyFeedGetTimeline: 'app.bsky.feed.getTimeline', + 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', + AppBskyGraphGetBlocks: 'app.bsky.graph.getBlocks', + AppBskyGraphGetFollowers: 'app.bsky.graph.getFollowers', + AppBskyGraphGetFollows: 'app.bsky.graph.getFollows', + AppBskyGraphGetList: 'app.bsky.graph.getList', + AppBskyGraphGetListBlocks: 'app.bsky.graph.getListBlocks', + AppBskyGraphGetListMutes: 'app.bsky.graph.getListMutes', + AppBskyGraphGetLists: 'app.bsky.graph.getLists', + AppBskyGraphGetMutes: 'app.bsky.graph.getMutes', + AppBskyGraphGetSuggestedFollowsByActor: + 'app.bsky.graph.getSuggestedFollowsByActor', + AppBskyGraphList: 'app.bsky.graph.list', + AppBskyGraphListblock: 'app.bsky.graph.listblock', + AppBskyGraphListitem: 'app.bsky.graph.listitem', + AppBskyGraphMuteActor: 'app.bsky.graph.muteActor', + AppBskyGraphMuteActorList: 'app.bsky.graph.muteActorList', + AppBskyGraphUnmuteActor: 'app.bsky.graph.unmuteActor', + AppBskyGraphUnmuteActorList: 'app.bsky.graph.unmuteActorList', + AppBskyNotificationGetUnreadCount: 'app.bsky.notification.getUnreadCount', + AppBskyNotificationListNotifications: + 'app.bsky.notification.listNotifications', + AppBskyNotificationRegisterPush: 'app.bsky.notification.registerPush', + AppBskyNotificationUpdateSeen: 'app.bsky.notification.updateSeen', + AppBskyRichtextFacet: 'app.bsky.richtext.facet', + 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/ozone/src/lexicon/types/app/bsky/actor/defs.ts b/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts new file mode 100644 index 00000000000..c20177ca50e --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/actor/defs.ts @@ -0,0 +1,235 @@ +/** + * 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' +import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' +import * as AppBskyGraphDefs from '../graph/defs' + +export interface ProfileViewBasic { + did: string + handle: string + displayName?: string + avatar?: string + viewer?: ViewerState + labels?: ComAtprotoLabelDefs.Label[] + [k: string]: unknown +} + +export function isProfileViewBasic(v: unknown): v is ProfileViewBasic { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#profileViewBasic' + ) +} + +export function validateProfileViewBasic(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#profileViewBasic', v) +} + +export interface ProfileView { + did: string + handle: string + displayName?: string + description?: string + avatar?: string + indexedAt?: string + viewer?: ViewerState + labels?: ComAtprotoLabelDefs.Label[] + [k: string]: unknown +} + +export function isProfileView(v: unknown): v is ProfileView { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#profileView' + ) +} + +export function validateProfileView(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#profileView', v) +} + +export interface ProfileViewDetailed { + did: string + handle: string + displayName?: string + description?: string + avatar?: string + banner?: string + followersCount?: number + followsCount?: number + postsCount?: number + indexedAt?: string + viewer?: ViewerState + labels?: ComAtprotoLabelDefs.Label[] + [k: string]: unknown +} + +export function isProfileViewDetailed(v: unknown): v is ProfileViewDetailed { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#profileViewDetailed' + ) +} + +export function validateProfileViewDetailed(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#profileViewDetailed', v) +} + +export interface ViewerState { + muted?: boolean + mutedByList?: AppBskyGraphDefs.ListViewBasic + blockedBy?: boolean + blocking?: string + blockingByList?: AppBskyGraphDefs.ListViewBasic + following?: string + followedBy?: string + [k: string]: unknown +} + +export function isViewerState(v: unknown): v is ViewerState { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#viewerState' + ) +} + +export function validateViewerState(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#viewerState', v) +} + +export type Preferences = ( + | AdultContentPref + | ContentLabelPref + | SavedFeedsPref + | PersonalDetailsPref + | FeedViewPref + | ThreadViewPref + | { $type: string; [k: string]: unknown } +)[] + +export interface AdultContentPref { + enabled: boolean + [k: string]: unknown +} + +export function isAdultContentPref(v: unknown): v is AdultContentPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#adultContentPref' + ) +} + +export function validateAdultContentPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#adultContentPref', v) +} + +export interface ContentLabelPref { + label: string + visibility: 'show' | 'warn' | 'hide' | (string & {}) + [k: string]: unknown +} + +export function isContentLabelPref(v: unknown): v is ContentLabelPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#contentLabelPref' + ) +} + +export function validateContentLabelPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#contentLabelPref', v) +} + +export interface SavedFeedsPref { + pinned: string[] + saved: string[] + [k: string]: unknown +} + +export function isSavedFeedsPref(v: unknown): v is SavedFeedsPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#savedFeedsPref' + ) +} + +export function validateSavedFeedsPref(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.defs#savedFeedsPref', v) +} + +export interface PersonalDetailsPref { + /** The birth date of account owner. */ + birthDate?: string + [k: string]: unknown +} + +export function isPersonalDetailsPref(v: unknown): v is PersonalDetailsPref { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.actor.defs#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 for threads. */ + 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/ozone/src/lexicon/types/app/bsky/actor/getPreferences.ts b/packages/ozone/src/lexicon/types/app/bsky/actor/getPreferences.ts new file mode 100644 index 00000000000..88d78a57cba --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/actor/getPreferences.ts @@ -0,0 +1,44 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyActorDefs from './defs' + +export interface QueryParams {} + +export type InputSchema = undefined + +export interface OutputSchema { + preferences: AppBskyActorDefs.Preferences + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +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/ozone/src/lexicon/types/app/bsky/actor/getProfile.ts b/packages/ozone/src/lexicon/types/app/bsky/actor/getProfile.ts new file mode 100644 index 00000000000..802afda5361 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/actor/getProfile.ts @@ -0,0 +1,41 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyActorDefs from './defs' + +export interface QueryParams { + actor: string +} + +export type InputSchema = undefined +export type OutputSchema = AppBskyActorDefs.ProfileViewDetailed +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +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/ozone/src/lexicon/types/app/bsky/actor/getProfiles.ts b/packages/ozone/src/lexicon/types/app/bsky/actor/getProfiles.ts new file mode 100644 index 00000000000..2549b264e33 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/actor/getProfiles.ts @@ -0,0 +1,46 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyActorDefs from './defs' + +export interface QueryParams { + actors: string[] +} + +export type InputSchema = undefined + +export interface OutputSchema { + profiles: AppBskyActorDefs.ProfileViewDetailed[] + [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 +} + +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/ozone/src/lexicon/types/app/bsky/actor/getSuggestions.ts b/packages/ozone/src/lexicon/types/app/bsky/actor/getSuggestions.ts new file mode 100644 index 00000000000..a6d4d6102af --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/actor/getSuggestions.ts @@ -0,0 +1,48 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyActorDefs from './defs' + +export interface QueryParams { + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + actors: AppBskyActorDefs.ProfileView[] + [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 +} + +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/ozone/src/lexicon/types/app/bsky/actor/profile.ts b/packages/ozone/src/lexicon/types/app/bsky/actor/profile.ts new file mode 100644 index 00000000000..7dbc4c1ccec --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/actor/profile.ts @@ -0,0 +1,32 @@ +/** + * 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' +import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' + +export interface Record { + displayName?: string + description?: string + avatar?: BlobRef + banner?: BlobRef + labels?: + | ComAtprotoLabelDefs.SelfLabels + | { $type: string; [k: string]: unknown } + [k: string]: unknown +} + +export function isRecord(v: unknown): v is Record { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'app.bsky.actor.profile#main' || + v.$type === 'app.bsky.actor.profile') + ) +} + +export function validateRecord(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.actor.profile#main', v) +} diff --git a/packages/ozone/src/lexicon/types/app/bsky/actor/putPreferences.ts b/packages/ozone/src/lexicon/types/app/bsky/actor/putPreferences.ts new file mode 100644 index 00000000000..1e5ee2d834e --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/actor/putPreferences.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' +import * as AppBskyActorDefs from './defs' + +export interface QueryParams {} + +export interface InputSchema { + preferences: AppBskyActorDefs.Preferences + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | void +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/ozone/src/lexicon/types/app/bsky/actor/searchActors.ts b/packages/ozone/src/lexicon/types/app/bsky/actor/searchActors.ts new file mode 100644 index 00000000000..f072b8a4d04 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/actor/searchActors.ts @@ -0,0 +1,52 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyActorDefs from './defs' + +export interface QueryParams { + /** 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 +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + actors: AppBskyActorDefs.ProfileView[] + [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 +} + +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/ozone/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts b/packages/ozone/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts new file mode 100644 index 00000000000..0cf56753db2 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts @@ -0,0 +1,50 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyActorDefs from './defs' + +export interface QueryParams { + /** DEPRECATED: use 'q' instead. */ + term?: string + /** Search query prefix; not a full query string. */ + q?: string + limit: number +} + +export type InputSchema = undefined + +export interface OutputSchema { + actors: AppBskyActorDefs.ProfileViewBasic[] + [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 +} + +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/ozone/src/lexicon/types/app/bsky/embed/external.ts b/packages/ozone/src/lexicon/types/app/bsky/embed/external.ts new file mode 100644 index 00000000000..f42a6cfd95c --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/embed/external.ts @@ -0,0 +1,82 @@ +/** + * 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 Main { + external: External + [k: string]: unknown +} + +export function isMain(v: unknown): v is Main { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'app.bsky.embed.external#main' || + v.$type === 'app.bsky.embed.external') + ) +} + +export function validateMain(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.external#main', v) +} + +export interface External { + uri: string + title: string + description: string + thumb?: BlobRef + [k: string]: unknown +} + +export function isExternal(v: unknown): v is External { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.embed.external#external' + ) +} + +export function validateExternal(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.external#external', v) +} + +export interface View { + external: ViewExternal + [k: string]: unknown +} + +export function isView(v: unknown): v is View { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.embed.external#view' + ) +} + +export function validateView(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.external#view', v) +} + +export interface ViewExternal { + uri: string + title: string + description: string + thumb?: string + [k: string]: unknown +} + +export function isViewExternal(v: unknown): v is ViewExternal { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.embed.external#viewExternal' + ) +} + +export function validateViewExternal(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.external#viewExternal', v) +} diff --git a/packages/ozone/src/lexicon/types/app/bsky/embed/images.ts b/packages/ozone/src/lexicon/types/app/bsky/embed/images.ts new file mode 100644 index 00000000000..4864fad3dea --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/embed/images.ts @@ -0,0 +1,96 @@ +/** + * 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 Main { + images: Image[] + [k: string]: unknown +} + +export function isMain(v: unknown): v is Main { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'app.bsky.embed.images#main' || + v.$type === 'app.bsky.embed.images') + ) +} + +export function validateMain(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.images#main', v) +} + +export interface Image { + image: BlobRef + alt: string + aspectRatio?: AspectRatio + [k: string]: unknown +} + +export function isImage(v: unknown): v is Image { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.embed.images#image' + ) +} + +export function validateImage(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.images#image', v) +} + +/** width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit. */ +export interface AspectRatio { + width: number + height: number + [k: string]: unknown +} + +export function isAspectRatio(v: unknown): v is AspectRatio { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.embed.images#aspectRatio' + ) +} + +export function validateAspectRatio(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.images#aspectRatio', v) +} + +export interface View { + images: ViewImage[] + [k: string]: unknown +} + +export function isView(v: unknown): v is View { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.embed.images#view' + ) +} + +export function validateView(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.images#view', v) +} + +export interface ViewImage { + thumb: string + fullsize: string + alt: string + aspectRatio?: AspectRatio + [k: string]: unknown +} + +export function isViewImage(v: unknown): v is ViewImage { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.embed.images#viewImage' + ) +} + +export function validateViewImage(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.images#viewImage', v) +} diff --git a/packages/ozone/src/lexicon/types/app/bsky/embed/record.ts b/packages/ozone/src/lexicon/types/app/bsky/embed/record.ts new file mode 100644 index 00000000000..cea5742a45e --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/embed/record.ts @@ -0,0 +1,120 @@ +/** + * 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' +import * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef' +import * as AppBskyFeedDefs from '../feed/defs' +import * as AppBskyGraphDefs from '../graph/defs' +import * as AppBskyActorDefs from '../actor/defs' +import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' +import * as AppBskyEmbedImages from './images' +import * as AppBskyEmbedExternal from './external' +import * as AppBskyEmbedRecordWithMedia from './recordWithMedia' + +export interface Main { + record: ComAtprotoRepoStrongRef.Main + [k: string]: unknown +} + +export function isMain(v: unknown): v is Main { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'app.bsky.embed.record#main' || + v.$type === 'app.bsky.embed.record') + ) +} + +export function validateMain(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.record#main', v) +} + +export interface View { + record: + | ViewRecord + | ViewNotFound + | ViewBlocked + | AppBskyFeedDefs.GeneratorView + | AppBskyGraphDefs.ListView + | { $type: string; [k: string]: unknown } + [k: string]: unknown +} + +export function isView(v: unknown): v is View { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.embed.record#view' + ) +} + +export function validateView(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.record#view', v) +} + +export interface ViewRecord { + uri: string + cid: string + author: AppBskyActorDefs.ProfileViewBasic + value: {} + labels?: ComAtprotoLabelDefs.Label[] + embeds?: ( + | AppBskyEmbedImages.View + | AppBskyEmbedExternal.View + | View + | AppBskyEmbedRecordWithMedia.View + | { $type: string; [k: string]: unknown } + )[] + indexedAt: string + [k: string]: unknown +} + +export function isViewRecord(v: unknown): v is ViewRecord { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.embed.record#viewRecord' + ) +} + +export function validateViewRecord(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.record#viewRecord', v) +} + +export interface ViewNotFound { + uri: string + notFound: true + [k: string]: unknown +} + +export function isViewNotFound(v: unknown): v is ViewNotFound { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.embed.record#viewNotFound' + ) +} + +export function validateViewNotFound(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.record#viewNotFound', v) +} + +export interface ViewBlocked { + uri: string + blocked: true + author: AppBskyFeedDefs.BlockedAuthor + [k: string]: unknown +} + +export function isViewBlocked(v: unknown): v is ViewBlocked { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.embed.record#viewBlocked' + ) +} + +export function validateViewBlocked(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.record#viewBlocked', v) +} diff --git a/packages/ozone/src/lexicon/types/app/bsky/embed/recordWithMedia.ts b/packages/ozone/src/lexicon/types/app/bsky/embed/recordWithMedia.ts new file mode 100644 index 00000000000..f8f1ae50b9e --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/embed/recordWithMedia.ts @@ -0,0 +1,53 @@ +/** + * 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' +import * as AppBskyEmbedRecord from './record' +import * as AppBskyEmbedImages from './images' +import * as AppBskyEmbedExternal from './external' + +export interface Main { + record: AppBskyEmbedRecord.Main + media: + | AppBskyEmbedImages.Main + | AppBskyEmbedExternal.Main + | { $type: string; [k: string]: unknown } + [k: string]: unknown +} + +export function isMain(v: unknown): v is Main { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'app.bsky.embed.recordWithMedia#main' || + v.$type === 'app.bsky.embed.recordWithMedia') + ) +} + +export function validateMain(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.recordWithMedia#main', v) +} + +export interface View { + record: AppBskyEmbedRecord.View + media: + | AppBskyEmbedImages.View + | AppBskyEmbedExternal.View + | { $type: string; [k: string]: unknown } + [k: string]: unknown +} + +export function isView(v: unknown): v is View { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.embed.recordWithMedia#view' + ) +} + +export function validateView(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.embed.recordWithMedia#view', v) +} diff --git a/packages/ozone/src/lexicon/types/app/bsky/feed/defs.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/defs.ts new file mode 100644 index 00000000000..382d3f58ecf --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/feed/defs.ts @@ -0,0 +1,308 @@ +/** + * 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' +import * as AppBskyActorDefs from '../actor/defs' +import * as AppBskyEmbedImages from '../embed/images' +import * as AppBskyEmbedExternal from '../embed/external' +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 + cid: string + author: AppBskyActorDefs.ProfileViewBasic + record: {} + embed?: + | AppBskyEmbedImages.View + | AppBskyEmbedExternal.View + | AppBskyEmbedRecord.View + | AppBskyEmbedRecordWithMedia.View + | { $type: string; [k: string]: unknown } + replyCount?: number + repostCount?: number + likeCount?: number + indexedAt: string + viewer?: ViewerState + labels?: ComAtprotoLabelDefs.Label[] + threadgate?: ThreadgateView + [k: string]: unknown +} + +export function isPostView(v: unknown): v is PostView { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.feed.defs#postView' + ) +} + +export function validatePostView(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.defs#postView', v) +} + +export interface ViewerState { + repost?: string + like?: string + replyDisabled?: boolean + [k: string]: unknown +} + +export function isViewerState(v: unknown): v is ViewerState { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.defs#viewerState' + ) +} + +export function validateViewerState(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.defs#viewerState', v) +} + +export interface FeedViewPost { + post: PostView + reply?: ReplyRef + reason?: ReasonRepost | { $type: string; [k: string]: unknown } + [k: string]: unknown +} + +export function isFeedViewPost(v: unknown): v is FeedViewPost { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.defs#feedViewPost' + ) +} + +export function validateFeedViewPost(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.defs#feedViewPost', v) +} + +export interface ReplyRef { + root: + | PostView + | NotFoundPost + | BlockedPost + | { $type: string; [k: string]: unknown } + parent: + | PostView + | NotFoundPost + | BlockedPost + | { $type: string; [k: string]: unknown } + [k: string]: unknown +} + +export function isReplyRef(v: unknown): v is ReplyRef { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.feed.defs#replyRef' + ) +} + +export function validateReplyRef(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.defs#replyRef', v) +} + +export interface ReasonRepost { + by: AppBskyActorDefs.ProfileViewBasic + indexedAt: string + [k: string]: unknown +} + +export function isReasonRepost(v: unknown): v is ReasonRepost { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.defs#reasonRepost' + ) +} + +export function validateReasonRepost(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.defs#reasonRepost', v) +} + +export interface ThreadViewPost { + post: PostView + parent?: + | ThreadViewPost + | NotFoundPost + | BlockedPost + | { $type: string; [k: string]: unknown } + replies?: ( + | ThreadViewPost + | NotFoundPost + | BlockedPost + | { $type: string; [k: string]: unknown } + )[] + [k: string]: unknown +} + +export function isThreadViewPost(v: unknown): v is ThreadViewPost { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.defs#threadViewPost' + ) +} + +export function validateThreadViewPost(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.defs#threadViewPost', v) +} + +export interface NotFoundPost { + uri: string + notFound: true + [k: string]: unknown +} + +export function isNotFoundPost(v: unknown): v is NotFoundPost { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.defs#notFoundPost' + ) +} + +export function validateNotFoundPost(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.defs#notFoundPost', v) +} + +export interface BlockedPost { + uri: string + blocked: true + author: BlockedAuthor + [k: string]: unknown +} + +export function isBlockedPost(v: unknown): v is BlockedPost { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.defs#blockedPost' + ) +} + +export function validateBlockedPost(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.defs#blockedPost', v) +} + +export interface BlockedAuthor { + did: string + viewer?: AppBskyActorDefs.ViewerState + [k: string]: unknown +} + +export function isBlockedAuthor(v: unknown): v is BlockedAuthor { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.defs#blockedAuthor' + ) +} + +export function validateBlockedAuthor(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.defs#blockedAuthor', v) +} + +export interface GeneratorView { + uri: string + cid: string + did: string + creator: AppBskyActorDefs.ProfileView + displayName: string + description?: string + descriptionFacets?: AppBskyRichtextFacet.Main[] + avatar?: string + likeCount?: number + viewer?: GeneratorViewerState + indexedAt: string + [k: string]: unknown +} + +export function isGeneratorView(v: unknown): v is GeneratorView { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.defs#generatorView' + ) +} + +export function validateGeneratorView(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.defs#generatorView', v) +} + +export interface GeneratorViewerState { + like?: string + [k: string]: unknown +} + +export function isGeneratorViewerState(v: unknown): v is GeneratorViewerState { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.defs#generatorViewerState' + ) +} + +export function validateGeneratorViewerState(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.defs#generatorViewerState', v) +} + +export interface SkeletonFeedPost { + post: string + reason?: SkeletonReasonRepost | { $type: string; [k: string]: unknown } + [k: string]: unknown +} + +export function isSkeletonFeedPost(v: unknown): v is SkeletonFeedPost { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.defs#skeletonFeedPost' + ) +} + +export function validateSkeletonFeedPost(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.defs#skeletonFeedPost', v) +} + +export interface SkeletonReasonRepost { + repost: string + [k: string]: unknown +} + +export function isSkeletonReasonRepost(v: unknown): v is SkeletonReasonRepost { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.defs#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/ozone/src/lexicon/types/app/bsky/feed/describeFeedGenerator.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/describeFeedGenerator.ts new file mode 100644 index 00000000000..d329bf20a5a --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/feed/describeFeedGenerator.ts @@ -0,0 +1,80 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export type InputSchema = undefined + +export interface OutputSchema { + did: string + feeds: Feed[] + links?: Links + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +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 + +export interface Feed { + uri: string + [k: string]: unknown +} + +export function isFeed(v: unknown): v is Feed { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.describeFeedGenerator#feed' + ) +} + +export function validateFeed(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.describeFeedGenerator#feed', v) +} + +export interface Links { + privacyPolicy?: string + termsOfService?: string + [k: string]: unknown +} + +export function isLinks(v: unknown): v is Links { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.describeFeedGenerator#links' + ) +} + +export function validateLinks(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.describeFeedGenerator#links', v) +} diff --git a/packages/ozone/src/lexicon/types/app/bsky/feed/generator.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/generator.ts new file mode 100644 index 00000000000..757e74db845 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/feed/generator.ts @@ -0,0 +1,35 @@ +/** + * 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' +import * as AppBskyRichtextFacet from '../richtext/facet' +import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' + +export interface Record { + did: string + displayName: string + description?: string + descriptionFacets?: AppBskyRichtextFacet.Main[] + avatar?: BlobRef + labels?: + | ComAtprotoLabelDefs.SelfLabels + | { $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.generator#main' || + v.$type === 'app.bsky.feed.generator') + ) +} + +export function validateRecord(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.generator#main', v) +} diff --git a/packages/ozone/src/lexicon/types/app/bsky/feed/getActorFeeds.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/getActorFeeds.ts new file mode 100644 index 00000000000..3e930cbe201 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/feed/getActorFeeds.ts @@ -0,0 +1,49 @@ +/** + * 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 { + actor: string + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + feeds: AppBskyFeedDefs.GeneratorView[] + [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 +} + +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/ozone/src/lexicon/types/app/bsky/feed/getActorLikes.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/getActorLikes.ts new file mode 100644 index 00000000000..df2f291e1a7 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/feed/getActorLikes.ts @@ -0,0 +1,50 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyFeedDefs from './defs' + +export interface QueryParams { + actor: string + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + feed: AppBskyFeedDefs.FeedViewPost[] + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'BlockedActor' | 'BlockedByActor' +} + +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/ozone/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts new file mode 100644 index 00000000000..25f51f6fe5f --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/feed/getAuthorFeed.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 AppBskyFeedDefs from './defs' + +export interface QueryParams { + actor: string + limit: number + cursor?: string + filter: + | 'posts_with_replies' + | 'posts_no_replies' + | 'posts_with_media' + | 'posts_and_author_threads' + | (string & {}) +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + feed: AppBskyFeedDefs.FeedViewPost[] + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'BlockedActor' | 'BlockedByActor' +} + +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/ozone/src/lexicon/types/app/bsky/feed/getFeed.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/getFeed.ts new file mode 100644 index 00000000000..e72b1010aea --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/feed/getFeed.ts @@ -0,0 +1,50 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyFeedDefs from './defs' + +export interface QueryParams { + feed: string + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + feed: AppBskyFeedDefs.FeedViewPost[] + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'UnknownFeed' +} + +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/ozone/src/lexicon/types/app/bsky/feed/getFeedGenerator.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/getFeedGenerator.ts new file mode 100644 index 00000000000..fab3b30c316 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/feed/getFeedGenerator.ts @@ -0,0 +1,48 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyFeedDefs from './defs' + +export interface QueryParams { + feed: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + view: AppBskyFeedDefs.GeneratorView + isOnline: boolean + isValid: boolean + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +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/ozone/src/lexicon/types/app/bsky/feed/getFeedGenerators.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/getFeedGenerators.ts new file mode 100644 index 00000000000..d7e082f2362 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/feed/getFeedGenerators.ts @@ -0,0 +1,46 @@ +/** + * 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 { + feeds: string[] +} + +export type InputSchema = undefined + +export interface OutputSchema { + feeds: AppBskyFeedDefs.GeneratorView[] + [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 +} + +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/ozone/src/lexicon/types/app/bsky/feed/getFeedSkeleton.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/getFeedSkeleton.ts new file mode 100644 index 00000000000..1c8f349b42b --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/feed/getFeedSkeleton.ts @@ -0,0 +1,50 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyFeedDefs from './defs' + +export interface QueryParams { + feed: string + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + feed: AppBskyFeedDefs.SkeletonFeedPost[] + [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?: 'UnknownFeed' +} + +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/ozone/src/lexicon/types/app/bsky/feed/getLikes.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/getLikes.ts new file mode 100644 index 00000000000..d581f5bfa9c --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/feed/getLikes.ts @@ -0,0 +1,69 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyActorDefs from '../actor/defs' + +export interface QueryParams { + uri: string + cid?: string + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + uri: string + cid?: string + cursor?: string + likes: Like[] + [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 +} + +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 + +export interface Like { + indexedAt: string + createdAt: string + actor: AppBskyActorDefs.ProfileView + [k: string]: unknown +} + +export function isLike(v: unknown): v is Like { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.feed.getLikes#like' + ) +} + +export function validateLike(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.getLikes#like', v) +} diff --git a/packages/ozone/src/lexicon/types/app/bsky/feed/getListFeed.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/getListFeed.ts new file mode 100644 index 00000000000..e24c3f8ed22 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/feed/getListFeed.ts @@ -0,0 +1,50 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyFeedDefs from './defs' + +export interface QueryParams { + list: string + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + feed: AppBskyFeedDefs.FeedViewPost[] + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'UnknownList' +} + +export type HandlerOutput = HandlerError | HandlerSuccess +export type HandlerReqCtx = { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/ozone/src/lexicon/types/app/bsky/feed/getPostThread.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/getPostThread.ts new file mode 100644 index 00000000000..61de94b729d --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/feed/getPostThread.ts @@ -0,0 +1,53 @@ +/** + * 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 { + uri: string + depth: number + parentHeight: number +} + +export type InputSchema = undefined + +export interface OutputSchema { + thread: + | AppBskyFeedDefs.ThreadViewPost + | AppBskyFeedDefs.NotFoundPost + | AppBskyFeedDefs.BlockedPost + | { $type: string; [k: string]: unknown } + [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?: 'NotFound' +} + +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/ozone/src/lexicon/types/app/bsky/feed/getPosts.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/getPosts.ts new file mode 100644 index 00000000000..4282f5d349f --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/feed/getPosts.ts @@ -0,0 +1,46 @@ +/** + * 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 { + uris: string[] +} + +export type InputSchema = undefined + +export interface OutputSchema { + 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 +} + +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/ozone/src/lexicon/types/app/bsky/feed/getRepostedBy.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/getRepostedBy.ts new file mode 100644 index 00000000000..0b9c1a6f68b --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/feed/getRepostedBy.ts @@ -0,0 +1,52 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyActorDefs from '../actor/defs' + +export interface QueryParams { + uri: string + cid?: string + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + uri: string + cid?: string + cursor?: string + repostedBy: AppBskyActorDefs.ProfileView[] + [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 +} + +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/ozone/src/lexicon/types/app/bsky/feed/getSuggestedFeeds.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/getSuggestedFeeds.ts new file mode 100644 index 00000000000..9b271335466 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/feed/getSuggestedFeeds.ts @@ -0,0 +1,48 @@ +/** + * 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 { + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + feeds: AppBskyFeedDefs.GeneratorView[] + [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 +} + +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/ozone/src/lexicon/types/app/bsky/feed/getTimeline.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/getTimeline.ts new file mode 100644 index 00000000000..832caf5c6f7 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/feed/getTimeline.ts @@ -0,0 +1,49 @@ +/** + * 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 { + algorithm?: string + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + feed: AppBskyFeedDefs.FeedViewPost[] + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +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/ozone/src/lexicon/types/app/bsky/feed/like.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/like.ts new file mode 100644 index 00000000000..1da9dd940dc --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/feed/like.ts @@ -0,0 +1,26 @@ +/** + * 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' +import * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef' + +export interface Record { + subject: ComAtprotoRepoStrongRef.Main + createdAt: string + [k: string]: unknown +} + +export function isRecord(v: unknown): v is Record { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'app.bsky.feed.like#main' || v.$type === 'app.bsky.feed.like') + ) +} + +export function validateRecord(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.like#main', v) +} diff --git a/packages/ozone/src/lexicon/types/app/bsky/feed/post.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/post.ts new file mode 100644 index 00000000000..93870b4452d --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/feed/post.ts @@ -0,0 +1,102 @@ +/** + * 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' +import * as AppBskyRichtextFacet from '../richtext/facet' +import * as AppBskyEmbedImages from '../embed/images' +import * as AppBskyEmbedExternal from '../embed/external' +import * as AppBskyEmbedRecord from '../embed/record' +import * as AppBskyEmbedRecordWithMedia from '../embed/recordWithMedia' +import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' +import * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef' + +export interface Record { + text: string + /** Deprecated: replaced by app.bsky.richtext.facet. */ + entities?: Entity[] + facets?: AppBskyRichtextFacet.Main[] + reply?: ReplyRef + embed?: + | AppBskyEmbedImages.Main + | AppBskyEmbedExternal.Main + | AppBskyEmbedRecord.Main + | AppBskyEmbedRecordWithMedia.Main + | { $type: string; [k: string]: unknown } + langs?: string[] + labels?: + | ComAtprotoLabelDefs.SelfLabels + | { $type: string; [k: string]: unknown } + /** Additional non-inline tags describing this post. */ + tags?: string[] + createdAt: string + [k: string]: unknown +} + +export function isRecord(v: unknown): v is Record { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'app.bsky.feed.post#main' || v.$type === 'app.bsky.feed.post') + ) +} + +export function validateRecord(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.post#main', v) +} + +export interface ReplyRef { + root: ComAtprotoRepoStrongRef.Main + parent: ComAtprotoRepoStrongRef.Main + [k: string]: unknown +} + +export function isReplyRef(v: unknown): v is ReplyRef { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.feed.post#replyRef' + ) +} + +export function validateReplyRef(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.post#replyRef', v) +} + +/** Deprecated: use facets instead. */ +export interface Entity { + index: TextSlice + /** Expected values are 'mention' and 'link'. */ + type: string + value: string + [k: string]: unknown +} + +export function isEntity(v: unknown): v is Entity { + return ( + isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.feed.post#entity' + ) +} + +export function validateEntity(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.post#entity', v) +} + +/** Deprecated. Use app.bsky.richtext instead -- A text segment. Start is inclusive, end is exclusive. Indices are for utf16-encoded strings. */ +export interface TextSlice { + start: number + end: number + [k: string]: unknown +} + +export function isTextSlice(v: unknown): v is TextSlice { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.feed.post#textSlice' + ) +} + +export function validateTextSlice(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.post#textSlice', v) +} diff --git a/packages/ozone/src/lexicon/types/app/bsky/feed/repost.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/repost.ts new file mode 100644 index 00000000000..4c836d76e2a --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/feed/repost.ts @@ -0,0 +1,27 @@ +/** + * 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' +import * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef' + +export interface Record { + subject: ComAtprotoRepoStrongRef.Main + createdAt: string + [k: string]: unknown +} + +export function isRecord(v: unknown): v is Record { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'app.bsky.feed.repost#main' || + v.$type === 'app.bsky.feed.repost') + ) +} + +export function validateRecord(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.feed.repost#main', v) +} diff --git a/packages/ozone/src/lexicon/types/app/bsky/feed/searchPosts.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/searchPosts.ts new file mode 100644 index 00000000000..36ac7cbb67d --- /dev/null +++ b/packages/ozone/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/ozone/src/lexicon/types/app/bsky/feed/threadgate.ts b/packages/ozone/src/lexicon/types/app/bsky/feed/threadgate.ts new file mode 100644 index 00000000000..6a190d6e98a --- /dev/null +++ b/packages/ozone/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/ozone/src/lexicon/types/app/bsky/graph/block.ts b/packages/ozone/src/lexicon/types/app/bsky/graph/block.ts new file mode 100644 index 00000000000..947463af422 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/graph/block.ts @@ -0,0 +1,26 @@ +/** + * 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 { + subject: string + createdAt: string + [k: string]: unknown +} + +export function isRecord(v: unknown): v is Record { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'app.bsky.graph.block#main' || + v.$type === 'app.bsky.graph.block') + ) +} + +export function validateRecord(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.graph.block#main', v) +} diff --git a/packages/ozone/src/lexicon/types/app/bsky/graph/defs.ts b/packages/ozone/src/lexicon/types/app/bsky/graph/defs.ts new file mode 100644 index 00000000000..be2d8c385d9 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/graph/defs.ts @@ -0,0 +1,104 @@ +/** + * 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' +import * as AppBskyActorDefs from '../actor/defs' +import * as AppBskyRichtextFacet from '../richtext/facet' + +export interface ListViewBasic { + uri: string + cid: string + name: string + purpose: ListPurpose + avatar?: string + viewer?: ListViewerState + indexedAt?: string + [k: string]: unknown +} + +export function isListViewBasic(v: unknown): v is ListViewBasic { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.graph.defs#listViewBasic' + ) +} + +export function validateListViewBasic(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.graph.defs#listViewBasic', v) +} + +export interface ListView { + uri: string + cid: string + creator: AppBskyActorDefs.ProfileView + name: string + purpose: ListPurpose + description?: string + descriptionFacets?: AppBskyRichtextFacet.Main[] + avatar?: string + viewer?: ListViewerState + indexedAt: string + [k: string]: unknown +} + +export function isListView(v: unknown): v is ListView { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.graph.defs#listView' + ) +} + +export function validateListView(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.graph.defs#listView', v) +} + +export interface ListItemView { + uri: string + subject: AppBskyActorDefs.ProfileView + [k: string]: unknown +} + +export function isListItemView(v: unknown): v is ListItemView { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.graph.defs#listItemView' + ) +} + +export function validateListItemView(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.graph.defs#listItemView', v) +} + +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 + blocked?: string + [k: string]: unknown +} + +export function isListViewerState(v: unknown): v is ListViewerState { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.graph.defs#listViewerState' + ) +} + +export function validateListViewerState(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.graph.defs#listViewerState', v) +} diff --git a/packages/ozone/src/lexicon/types/app/bsky/graph/follow.ts b/packages/ozone/src/lexicon/types/app/bsky/graph/follow.ts new file mode 100644 index 00000000000..4b74a58a5df --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/graph/follow.ts @@ -0,0 +1,26 @@ +/** + * 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 { + subject: string + createdAt: string + [k: string]: unknown +} + +export function isRecord(v: unknown): v is Record { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'app.bsky.graph.follow#main' || + v.$type === 'app.bsky.graph.follow') + ) +} + +export function validateRecord(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.graph.follow#main', v) +} diff --git a/packages/ozone/src/lexicon/types/app/bsky/graph/getBlocks.ts b/packages/ozone/src/lexicon/types/app/bsky/graph/getBlocks.ts new file mode 100644 index 00000000000..d380a14880a --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/graph/getBlocks.ts @@ -0,0 +1,48 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyActorDefs from '../actor/defs' + +export interface QueryParams { + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + blocks: AppBskyActorDefs.ProfileView[] + [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 +} + +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/ozone/src/lexicon/types/app/bsky/graph/getFollowers.ts b/packages/ozone/src/lexicon/types/app/bsky/graph/getFollowers.ts new file mode 100644 index 00000000000..b337be52c1b --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/graph/getFollowers.ts @@ -0,0 +1,50 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyActorDefs from '../actor/defs' + +export interface QueryParams { + actor: string + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + subject: AppBskyActorDefs.ProfileView + cursor?: string + followers: AppBskyActorDefs.ProfileView[] + [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 +} + +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/ozone/src/lexicon/types/app/bsky/graph/getFollows.ts b/packages/ozone/src/lexicon/types/app/bsky/graph/getFollows.ts new file mode 100644 index 00000000000..71e9ca0270c --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/graph/getFollows.ts @@ -0,0 +1,50 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyActorDefs from '../actor/defs' + +export interface QueryParams { + actor: string + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + subject: AppBskyActorDefs.ProfileView + cursor?: string + follows: AppBskyActorDefs.ProfileView[] + [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 +} + +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/ozone/src/lexicon/types/app/bsky/graph/getList.ts b/packages/ozone/src/lexicon/types/app/bsky/graph/getList.ts new file mode 100644 index 00000000000..fc45dd20985 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/graph/getList.ts @@ -0,0 +1,50 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyGraphDefs from './defs' + +export interface QueryParams { + list: string + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + list: AppBskyGraphDefs.ListView + items: AppBskyGraphDefs.ListItemView[] + [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 +} + +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/ozone/src/lexicon/types/app/bsky/graph/getListBlocks.ts b/packages/ozone/src/lexicon/types/app/bsky/graph/getListBlocks.ts new file mode 100644 index 00000000000..04cca70b44d --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/graph/getListBlocks.ts @@ -0,0 +1,48 @@ +/** + * 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 AppBskyGraphDefs from './defs' + +export interface QueryParams { + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + lists: AppBskyGraphDefs.ListView[] + [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 +} + +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/ozone/src/lexicon/types/app/bsky/graph/getListMutes.ts b/packages/ozone/src/lexicon/types/app/bsky/graph/getListMutes.ts new file mode 100644 index 00000000000..04cca70b44d --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/graph/getListMutes.ts @@ -0,0 +1,48 @@ +/** + * 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 AppBskyGraphDefs from './defs' + +export interface QueryParams { + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + lists: AppBskyGraphDefs.ListView[] + [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 +} + +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/ozone/src/lexicon/types/app/bsky/graph/getLists.ts b/packages/ozone/src/lexicon/types/app/bsky/graph/getLists.ts new file mode 100644 index 00000000000..8acf9362c00 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/graph/getLists.ts @@ -0,0 +1,49 @@ +/** + * 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 AppBskyGraphDefs from './defs' + +export interface QueryParams { + actor: string + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + lists: AppBskyGraphDefs.ListView[] + [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 +} + +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/ozone/src/lexicon/types/app/bsky/graph/getMutes.ts b/packages/ozone/src/lexicon/types/app/bsky/graph/getMutes.ts new file mode 100644 index 00000000000..0034095b975 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/graph/getMutes.ts @@ -0,0 +1,48 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyActorDefs from '../actor/defs' + +export interface QueryParams { + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + mutes: AppBskyActorDefs.ProfileView[] + [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 +} + +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/ozone/src/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.ts b/packages/ozone/src/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.ts new file mode 100644 index 00000000000..a2245846fd2 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.ts @@ -0,0 +1,46 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyActorDefs from '../actor/defs' + +export interface QueryParams { + actor: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + suggestions: AppBskyActorDefs.ProfileView[] + [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 +} + +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/ozone/src/lexicon/types/app/bsky/graph/list.ts b/packages/ozone/src/lexicon/types/app/bsky/graph/list.ts new file mode 100644 index 00000000000..36a7fb17a3f --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/graph/list.ts @@ -0,0 +1,36 @@ +/** + * 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' +import * as AppBskyGraphDefs from './defs' +import * as AppBskyRichtextFacet from '../richtext/facet' +import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' + +export interface Record { + purpose: AppBskyGraphDefs.ListPurpose + name: string + description?: string + descriptionFacets?: AppBskyRichtextFacet.Main[] + avatar?: BlobRef + labels?: + | ComAtprotoLabelDefs.SelfLabels + | { $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.graph.list#main' || + v.$type === 'app.bsky.graph.list') + ) +} + +export function validateRecord(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.graph.list#main', v) +} diff --git a/packages/ozone/src/lexicon/types/app/bsky/graph/listblock.ts b/packages/ozone/src/lexicon/types/app/bsky/graph/listblock.ts new file mode 100644 index 00000000000..59f2e057eb5 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/graph/listblock.ts @@ -0,0 +1,26 @@ +/** + * 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 { + subject: string + createdAt: string + [k: string]: unknown +} + +export function isRecord(v: unknown): v is Record { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'app.bsky.graph.listblock#main' || + v.$type === 'app.bsky.graph.listblock') + ) +} + +export function validateRecord(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.graph.listblock#main', v) +} diff --git a/packages/ozone/src/lexicon/types/app/bsky/graph/listitem.ts b/packages/ozone/src/lexicon/types/app/bsky/graph/listitem.ts new file mode 100644 index 00000000000..69eff329ed4 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/graph/listitem.ts @@ -0,0 +1,27 @@ +/** + * 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 { + subject: string + list: string + createdAt: string + [k: string]: unknown +} + +export function isRecord(v: unknown): v is Record { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'app.bsky.graph.listitem#main' || + v.$type === 'app.bsky.graph.listitem') + ) +} + +export function validateRecord(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.graph.listitem#main', v) +} diff --git a/packages/ozone/src/lexicon/types/app/bsky/graph/muteActor.ts b/packages/ozone/src/lexicon/types/app/bsky/graph/muteActor.ts new file mode 100644 index 00000000000..52d1b864989 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/graph/muteActor.ts @@ -0,0 +1,38 @@ +/** + * 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 { + actor: string + [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/ozone/src/lexicon/types/app/bsky/graph/muteActorList.ts b/packages/ozone/src/lexicon/types/app/bsky/graph/muteActorList.ts new file mode 100644 index 00000000000..bf803f388af --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/graph/muteActorList.ts @@ -0,0 +1,38 @@ +/** + * 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 { + list: string + [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/ozone/src/lexicon/types/app/bsky/graph/unmuteActor.ts b/packages/ozone/src/lexicon/types/app/bsky/graph/unmuteActor.ts new file mode 100644 index 00000000000..52d1b864989 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/graph/unmuteActor.ts @@ -0,0 +1,38 @@ +/** + * 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 { + actor: string + [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/ozone/src/lexicon/types/app/bsky/graph/unmuteActorList.ts b/packages/ozone/src/lexicon/types/app/bsky/graph/unmuteActorList.ts new file mode 100644 index 00000000000..bf803f388af --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/graph/unmuteActorList.ts @@ -0,0 +1,38 @@ +/** + * 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 { + list: string + [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/ozone/src/lexicon/types/app/bsky/notification/getUnreadCount.ts b/packages/ozone/src/lexicon/types/app/bsky/notification/getUnreadCount.ts new file mode 100644 index 00000000000..6cf3c84beb5 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/notification/getUnreadCount.ts @@ -0,0 +1,45 @@ +/** + * 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 { + seenAt?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + count: number + [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 +} + +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/ozone/src/lexicon/types/app/bsky/notification/listNotifications.ts b/packages/ozone/src/lexicon/types/app/bsky/notification/listNotifications.ts new file mode 100644 index 00000000000..b50d6e8282e --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/notification/listNotifications.ts @@ -0,0 +1,87 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as AppBskyActorDefs from '../actor/defs' +import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' + +export interface QueryParams { + limit: number + cursor?: string + seenAt?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + notifications: Notification[] + seenAt?: string + [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 +} + +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 + +export interface Notification { + uri: string + cid: string + author: AppBskyActorDefs.ProfileView + /** Expected values are 'like', 'repost', 'follow', 'mention', 'reply', and 'quote'. */ + reason: + | 'like' + | 'repost' + | 'follow' + | 'mention' + | 'reply' + | 'quote' + | (string & {}) + reasonSubject?: string + record: {} + isRead: boolean + indexedAt: string + labels?: ComAtprotoLabelDefs.Label[] + [k: string]: unknown +} + +export function isNotification(v: unknown): v is Notification { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.notification.listNotifications#notification' + ) +} + +export function validateNotification(v: unknown): ValidationResult { + return lexicons.validate( + 'app.bsky.notification.listNotifications#notification', + v, + ) +} diff --git a/packages/ozone/src/lexicon/types/app/bsky/notification/registerPush.ts b/packages/ozone/src/lexicon/types/app/bsky/notification/registerPush.ts new file mode 100644 index 00000000000..9923aeb058e --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/notification/registerPush.ts @@ -0,0 +1,41 @@ +/** + * 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 { + serviceDid: string + token: string + platform: 'ios' | 'android' | 'web' | (string & {}) + appId: string + [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/ozone/src/lexicon/types/app/bsky/notification/updateSeen.ts b/packages/ozone/src/lexicon/types/app/bsky/notification/updateSeen.ts new file mode 100644 index 00000000000..136191edc40 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/notification/updateSeen.ts @@ -0,0 +1,38 @@ +/** + * 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 { + seenAt: string + [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/ozone/src/lexicon/types/app/bsky/richtext/facet.ts b/packages/ozone/src/lexicon/types/app/bsky/richtext/facet.ts new file mode 100644 index 00000000000..2c5b2d723a9 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/richtext/facet.ts @@ -0,0 +1,97 @@ +/** + * 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 Main { + index: ByteSlice + features: (Mention | Link | Tag | { $type: string; [k: string]: unknown })[] + [k: string]: unknown +} + +export function isMain(v: unknown): v is Main { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'app.bsky.richtext.facet#main' || + v.$type === 'app.bsky.richtext.facet') + ) +} + +export function validateMain(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.richtext.facet#main', v) +} + +/** A facet feature for actor mentions. */ +export interface Mention { + did: string + [k: string]: unknown +} + +export function isMention(v: unknown): v is Mention { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.richtext.facet#mention' + ) +} + +export function validateMention(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.richtext.facet#mention', v) +} + +/** A facet feature for links. */ +export interface Link { + uri: string + [k: string]: unknown +} + +export function isLink(v: unknown): v is Link { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.richtext.facet#link' + ) +} + +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 + byteEnd: number + [k: string]: unknown +} + +export function isByteSlice(v: unknown): v is ByteSlice { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'app.bsky.richtext.facet#byteSlice' + ) +} + +export function validateByteSlice(v: unknown): ValidationResult { + return lexicons.validate('app.bsky.richtext.facet#byteSlice', v) +} diff --git a/packages/ozone/src/lexicon/types/app/bsky/unspecced/defs.ts b/packages/ozone/src/lexicon/types/app/bsky/unspecced/defs.ts new file mode 100644 index 00000000000..59a6b38064c --- /dev/null +++ b/packages/ozone/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/ozone/src/lexicon/types/app/bsky/unspecced/getPopular.ts b/packages/ozone/src/lexicon/types/app/bsky/unspecced/getPopular.ts new file mode 100644 index 00000000000..8471ed77a6c --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/unspecced/getPopular.ts @@ -0,0 +1,49 @@ +/** + * 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 '../feed/defs' + +export interface QueryParams { + includeNsfw: boolean + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + feed: AppBskyFeedDefs.FeedViewPost[] + [k: string]: unknown +} + +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +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/ozone/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/ozone/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts new file mode 100644 index 00000000000..97937e926c2 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -0,0 +1,49 @@ +/** + * 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 '../feed/defs' + +export interface QueryParams { + limit: number + cursor?: string + query?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + feeds: AppBskyFeedDefs.GeneratorView[] + [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 +} + +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/ozone/src/lexicon/types/app/bsky/unspecced/getTimelineSkeleton.ts b/packages/ozone/src/lexicon/types/app/bsky/unspecced/getTimelineSkeleton.ts new file mode 100644 index 00000000000..4ccad20c902 --- /dev/null +++ b/packages/ozone/src/lexicon/types/app/bsky/unspecced/getTimelineSkeleton.ts @@ -0,0 +1,49 @@ +/** + * 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 '../feed/defs' + +export interface QueryParams { + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + feed: AppBskyFeedDefs.SkeletonFeedPost[] + [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?: 'UnknownFeed' +} + +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/ozone/src/lexicon/types/app/bsky/unspecced/searchActorsSkeleton.ts b/packages/ozone/src/lexicon/types/app/bsky/unspecced/searchActorsSkeleton.ts new file mode 100644 index 00000000000..5c45b9fb622 --- /dev/null +++ b/packages/ozone/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/ozone/src/lexicon/types/app/bsky/unspecced/searchPostsSkeleton.ts b/packages/ozone/src/lexicon/types/app/bsky/unspecced/searchPostsSkeleton.ts new file mode 100644 index 00000000000..15532087b82 --- /dev/null +++ b/packages/ozone/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/ozone/src/lexicon/types/com/atproto/admin/defs.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/defs.ts new file mode 100644 index 00000000000..8236f848fa0 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/defs.ts @@ -0,0 +1,719 @@ +/** + * 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' +import * as ComAtprotoRepoStrongRef from '../repo/strongRef' +import * as ComAtprotoModerationDefs from '../moderation/defs' +import * as ComAtprotoServerDefs from '../server/defs' +import * as ComAtprotoLabelDefs from '../label/defs' + +export interface StatusAttr { + applied: boolean + ref?: string + [k: string]: unknown +} + +export function isStatusAttr(v: unknown): v is StatusAttr { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#statusAttr' + ) +} + +export function validateStatusAttr(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#statusAttr', v) +} + +export interface ModEventView { + id: number + event: + | ModEventTakedown + | ModEventReverseTakedown + | ModEventComment + | ModEventReport + | ModEventLabel + | ModEventAcknowledge + | ModEventEscalate + | ModEventMute + | ModEventEmail + | { $type: string; [k: string]: unknown } + subject: + | RepoRef + | ComAtprotoRepoStrongRef.Main + | { $type: string; [k: string]: unknown } + subjectBlobCids: string[] + createdBy: string + createdAt: string + creatorHandle?: string + subjectHandle?: string + [k: string]: unknown +} + +export function isModEventView(v: unknown): v is ModEventView { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#modEventView' + ) +} + +export function validateModEventView(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#modEventView', v) +} + +export interface ModEventViewDetail { + id: number + event: + | ModEventTakedown + | ModEventReverseTakedown + | ModEventComment + | ModEventReport + | ModEventLabel + | ModEventAcknowledge + | ModEventEscalate + | ModEventMute + | ModEventResolveAppeal + | { $type: string; [k: string]: unknown } + subject: + | RepoView + | RepoViewNotFound + | RecordView + | RecordViewNotFound + | { $type: string; [k: string]: unknown } + subjectBlobs: BlobView[] + createdBy: string + createdAt: string + [k: string]: unknown +} + +export function isModEventViewDetail(v: unknown): v is ModEventViewDetail { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#modEventViewDetail' + ) +} + +export function validateModEventViewDetail(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#modEventViewDetail', v) +} + +export interface ReportView { + id: number + reasonType: ComAtprotoModerationDefs.ReasonType + comment?: string + subjectRepoHandle?: string + subject: + | RepoRef + | ComAtprotoRepoStrongRef.Main + | { $type: string; [k: string]: unknown } + reportedBy: string + createdAt: string + resolvedByActionIds: number[] + [k: string]: unknown +} + +export function isReportView(v: unknown): v is ReportView { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#reportView' + ) +} + +export function validateReportView(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#reportView', v) +} + +export interface SubjectStatusView { + id: number + subject: + | RepoRef + | ComAtprotoRepoStrongRef.Main + | { $type: string; [k: string]: unknown } + subjectBlobCids?: string[] + subjectRepoHandle?: string + /** Timestamp referencing when the last update was made to the moderation status of the subject */ + updatedAt: string + /** Timestamp referencing the first moderation status impacting event was emitted on the subject */ + createdAt: string + reviewState: SubjectReviewState + /** Sticky comment on the subject. */ + comment?: string + muteUntil?: string + lastReviewedBy?: string + lastReviewedAt?: string + lastReportedAt?: string + /** Timestamp referencing when the author of the subject appealed a moderation action */ + lastAppealedAt?: string + takendown?: boolean + /** True indicates that the a previously taken moderator action was appealed against, by the author of the content. False indicates last appeal was resolved by moderators. */ + appealed?: boolean + suspendUntil?: string + [k: string]: unknown +} + +export function isSubjectStatusView(v: unknown): v is SubjectStatusView { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#subjectStatusView' + ) +} + +export function validateSubjectStatusView(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#subjectStatusView', v) +} + +export interface ReportViewDetail { + id: number + reasonType: ComAtprotoModerationDefs.ReasonType + comment?: string + subject: + | RepoView + | RepoViewNotFound + | RecordView + | RecordViewNotFound + | { $type: string; [k: string]: unknown } + subjectStatus?: SubjectStatusView + reportedBy: string + createdAt: string + resolvedByActions: ModEventView[] + [k: string]: unknown +} + +export function isReportViewDetail(v: unknown): v is ReportViewDetail { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#reportViewDetail' + ) +} + +export function validateReportViewDetail(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#reportViewDetail', v) +} + +export interface RepoView { + did: string + handle: string + email?: string + relatedRecords: {}[] + indexedAt: string + moderation: Moderation + invitedBy?: ComAtprotoServerDefs.InviteCode + invitesDisabled?: boolean + inviteNote?: string + [k: string]: unknown +} + +export function isRepoView(v: unknown): v is RepoView { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#repoView' + ) +} + +export function validateRepoView(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#repoView', v) +} + +export interface RepoViewDetail { + did: string + handle: string + email?: string + relatedRecords: {}[] + indexedAt: string + moderation: ModerationDetail + labels?: ComAtprotoLabelDefs.Label[] + invitedBy?: ComAtprotoServerDefs.InviteCode + invites?: ComAtprotoServerDefs.InviteCode[] + invitesDisabled?: boolean + inviteNote?: string + emailConfirmedAt?: string + [k: string]: unknown +} + +export function isRepoViewDetail(v: unknown): v is RepoViewDetail { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#repoViewDetail' + ) +} + +export function validateRepoViewDetail(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#repoViewDetail', v) +} + +export interface AccountView { + did: string + handle: string + email?: string + relatedRecords?: {}[] + indexedAt: string + invitedBy?: ComAtprotoServerDefs.InviteCode + invites?: ComAtprotoServerDefs.InviteCode[] + invitesDisabled?: boolean + emailConfirmedAt?: string + inviteNote?: string + [k: string]: unknown +} + +export function isAccountView(v: unknown): v is AccountView { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#accountView' + ) +} + +export function validateAccountView(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#accountView', v) +} + +export interface RepoViewNotFound { + did: string + [k: string]: unknown +} + +export function isRepoViewNotFound(v: unknown): v is RepoViewNotFound { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#repoViewNotFound' + ) +} + +export function validateRepoViewNotFound(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#repoViewNotFound', v) +} + +export interface RepoRef { + did: string + [k: string]: unknown +} + +export function isRepoRef(v: unknown): v is RepoRef { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#repoRef' + ) +} + +export function validateRepoRef(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#repoRef', v) +} + +export interface RepoBlobRef { + did: string + cid: string + recordUri?: string + [k: string]: unknown +} + +export function isRepoBlobRef(v: unknown): v is RepoBlobRef { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#repoBlobRef' + ) +} + +export function validateRepoBlobRef(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#repoBlobRef', v) +} + +export interface RecordView { + uri: string + cid: string + value: {} + blobCids: string[] + indexedAt: string + moderation: Moderation + repo: RepoView + [k: string]: unknown +} + +export function isRecordView(v: unknown): v is RecordView { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#recordView' + ) +} + +export function validateRecordView(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#recordView', v) +} + +export interface RecordViewDetail { + uri: string + cid: string + value: {} + blobs: BlobView[] + labels?: ComAtprotoLabelDefs.Label[] + indexedAt: string + moderation: ModerationDetail + repo: RepoView + [k: string]: unknown +} + +export function isRecordViewDetail(v: unknown): v is RecordViewDetail { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#recordViewDetail' + ) +} + +export function validateRecordViewDetail(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#recordViewDetail', v) +} + +export interface RecordViewNotFound { + uri: string + [k: string]: unknown +} + +export function isRecordViewNotFound(v: unknown): v is RecordViewNotFound { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#recordViewNotFound' + ) +} + +export function validateRecordViewNotFound(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#recordViewNotFound', v) +} + +export interface Moderation { + subjectStatus?: SubjectStatusView + [k: string]: unknown +} + +export function isModeration(v: unknown): v is Moderation { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#moderation' + ) +} + +export function validateModeration(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#moderation', v) +} + +export interface ModerationDetail { + subjectStatus?: SubjectStatusView + [k: string]: unknown +} + +export function isModerationDetail(v: unknown): v is ModerationDetail { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#moderationDetail' + ) +} + +export function validateModerationDetail(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#moderationDetail', v) +} + +export interface BlobView { + cid: string + mimeType: string + size: number + createdAt: string + details?: + | ImageDetails + | VideoDetails + | { $type: string; [k: string]: unknown } + moderation?: Moderation + [k: string]: unknown +} + +export function isBlobView(v: unknown): v is BlobView { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#blobView' + ) +} + +export function validateBlobView(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#blobView', v) +} + +export interface ImageDetails { + width: number + height: number + [k: string]: unknown +} + +export function isImageDetails(v: unknown): v is ImageDetails { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#imageDetails' + ) +} + +export function validateImageDetails(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#imageDetails', v) +} + +export interface VideoDetails { + width: number + height: number + length: number + [k: string]: unknown +} + +export function isVideoDetails(v: unknown): v is VideoDetails { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#videoDetails' + ) +} + +export function validateVideoDetails(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#videoDetails', v) +} + +export type SubjectReviewState = + | 'lex:com.atproto.admin.defs#reviewOpen' + | 'lex:com.atproto.admin.defs#reviewEscalated' + | 'lex:com.atproto.admin.defs#reviewClosed' + | (string & {}) + +/** Moderator review status of a subject: Open. Indicates that the subject needs to be reviewed by a moderator */ +export const REVIEWOPEN = 'com.atproto.admin.defs#reviewOpen' +/** Moderator review status of a subject: Escalated. Indicates that the subject was escalated for review by a moderator */ +export const REVIEWESCALATED = 'com.atproto.admin.defs#reviewEscalated' +/** Moderator review status of a subject: Closed. Indicates that the subject was already reviewed and resolved by a moderator */ +export const REVIEWCLOSED = 'com.atproto.admin.defs#reviewClosed' + +/** Take down a subject permanently or temporarily */ +export interface ModEventTakedown { + comment?: string + /** Indicates how long the takedown should be in effect before automatically expiring. */ + durationInHours?: number + [k: string]: unknown +} + +export function isModEventTakedown(v: unknown): v is ModEventTakedown { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#modEventTakedown' + ) +} + +export function validateModEventTakedown(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#modEventTakedown', v) +} + +/** Revert take down action on a subject */ +export interface ModEventReverseTakedown { + /** Describe reasoning behind the reversal. */ + comment?: string + [k: string]: unknown +} + +export function isModEventReverseTakedown( + v: unknown, +): v is ModEventReverseTakedown { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#modEventReverseTakedown' + ) +} + +export function validateModEventReverseTakedown(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#modEventReverseTakedown', v) +} + +/** Resolve appeal on a subject */ +export interface ModEventResolveAppeal { + /** Describe resolution. */ + comment?: string + [k: string]: unknown +} + +export function isModEventResolveAppeal( + v: unknown, +): v is ModEventResolveAppeal { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#modEventResolveAppeal' + ) +} + +export function validateModEventResolveAppeal(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#modEventResolveAppeal', v) +} + +/** Add a comment to a subject */ +export interface ModEventComment { + comment: string + /** Make the comment persistent on the subject */ + sticky?: boolean + [k: string]: unknown +} + +export function isModEventComment(v: unknown): v is ModEventComment { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#modEventComment' + ) +} + +export function validateModEventComment(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#modEventComment', v) +} + +/** Report a subject */ +export interface ModEventReport { + comment?: string + reportType: ComAtprotoModerationDefs.ReasonType + [k: string]: unknown +} + +export function isModEventReport(v: unknown): v is ModEventReport { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#modEventReport' + ) +} + +export function validateModEventReport(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#modEventReport', v) +} + +/** Apply/Negate labels on a subject */ +export interface ModEventLabel { + comment?: string + createLabelVals: string[] + negateLabelVals: string[] + [k: string]: unknown +} + +export function isModEventLabel(v: unknown): v is ModEventLabel { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#modEventLabel' + ) +} + +export function validateModEventLabel(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#modEventLabel', v) +} + +export interface ModEventAcknowledge { + comment?: string + [k: string]: unknown +} + +export function isModEventAcknowledge(v: unknown): v is ModEventAcknowledge { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#modEventAcknowledge' + ) +} + +export function validateModEventAcknowledge(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#modEventAcknowledge', v) +} + +export interface ModEventEscalate { + comment?: string + [k: string]: unknown +} + +export function isModEventEscalate(v: unknown): v is ModEventEscalate { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#modEventEscalate' + ) +} + +export function validateModEventEscalate(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#modEventEscalate', v) +} + +/** Mute incoming reports on a subject */ +export interface ModEventMute { + comment?: string + /** Indicates how long the subject should remain muted. */ + durationInHours: number + [k: string]: unknown +} + +export function isModEventMute(v: unknown): v is ModEventMute { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#modEventMute' + ) +} + +export function validateModEventMute(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#modEventMute', v) +} + +/** Unmute action on a subject */ +export interface ModEventUnmute { + /** Describe reasoning behind the reversal. */ + comment?: string + [k: string]: unknown +} + +export function isModEventUnmute(v: unknown): v is ModEventUnmute { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#modEventUnmute' + ) +} + +export function validateModEventUnmute(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#modEventUnmute', v) +} + +/** Keep a log of outgoing email to a user */ +export interface ModEventEmail { + /** The subject line of the email sent to the user. */ + subjectLine: string + /** Additional comment about the outgoing comm. */ + comment?: string + [k: string]: unknown +} + +export function isModEventEmail(v: unknown): v is ModEventEmail { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.admin.defs#modEventEmail' + ) +} + +export function validateModEventEmail(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.admin.defs#modEventEmail', v) +} diff --git a/packages/ozone/src/lexicon/types/com/atproto/admin/deleteAccount.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/deleteAccount.ts new file mode 100644 index 00000000000..13e68eb5c7d --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/deleteAccount.ts @@ -0,0 +1,38 @@ +/** + * 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 + [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/ozone/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts new file mode 100644 index 00000000000..62864923dfd --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts @@ -0,0 +1,40 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + account: string + /** Optional reason for disabled invites. */ + note?: string + [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/ozone/src/lexicon/types/com/atproto/admin/disableInviteCodes.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/disableInviteCodes.ts new file mode 100644 index 00000000000..2b64371f1ed --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/disableInviteCodes.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 { + codes?: string[] + accounts?: string[] + [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/ozone/src/lexicon/types/com/atproto/admin/emitModerationEvent.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/emitModerationEvent.ts new file mode 100644 index 00000000000..df44702b51c --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/emitModerationEvent.ts @@ -0,0 +1,66 @@ +/** + * 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 ComAtprotoAdminDefs from './defs' +import * as ComAtprotoRepoStrongRef from '../repo/strongRef' + +export interface QueryParams {} + +export interface InputSchema { + event: + | ComAtprotoAdminDefs.ModEventTakedown + | ComAtprotoAdminDefs.ModEventAcknowledge + | ComAtprotoAdminDefs.ModEventEscalate + | ComAtprotoAdminDefs.ModEventComment + | ComAtprotoAdminDefs.ModEventLabel + | ComAtprotoAdminDefs.ModEventReport + | ComAtprotoAdminDefs.ModEventMute + | ComAtprotoAdminDefs.ModEventReverseTakedown + | ComAtprotoAdminDefs.ModEventUnmute + | ComAtprotoAdminDefs.ModEventEmail + | { $type: string; [k: string]: unknown } + subject: + | ComAtprotoAdminDefs.RepoRef + | ComAtprotoRepoStrongRef.Main + | { $type: string; [k: string]: unknown } + subjectBlobCids?: string[] + createdBy: string + [k: string]: unknown +} + +export type OutputSchema = ComAtprotoAdminDefs.ModEventView + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'SubjectHasAction' +} + +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/ozone/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts new file mode 100644 index 00000000000..fb3aa8b8375 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts @@ -0,0 +1,40 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + account: string + /** Optional reason for enabled invites. */ + note?: string + [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/ozone/src/lexicon/types/com/atproto/admin/getAccountInfo.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/getAccountInfo.ts new file mode 100644 index 00000000000..88a2b17a4b8 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/getAccountInfo.ts @@ -0,0 +1,41 @@ +/** + * 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 ComAtprotoAdminDefs from './defs' + +export interface QueryParams { + did: string +} + +export type InputSchema = undefined +export type OutputSchema = ComAtprotoAdminDefs.AccountView +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +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/ozone/src/lexicon/types/com/atproto/admin/getAccountInfos.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/getAccountInfos.ts new file mode 100644 index 00000000000..46d917293a8 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/getAccountInfos.ts @@ -0,0 +1,46 @@ +/** + * 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 ComAtprotoAdminDefs from './defs' + +export interface QueryParams { + dids: string[] +} + +export type InputSchema = undefined + +export interface OutputSchema { + infos: ComAtprotoAdminDefs.AccountView[] + [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 +} + +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/ozone/src/lexicon/types/com/atproto/admin/getInviteCodes.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/getInviteCodes.ts new file mode 100644 index 00000000000..1eb099aae66 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/getInviteCodes.ts @@ -0,0 +1,49 @@ +/** + * 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 ComAtprotoServerDefs from '../server/defs' + +export interface QueryParams { + sort: 'recent' | 'usage' | (string & {}) + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + codes: ComAtprotoServerDefs.InviteCode[] + [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 +} + +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/ozone/src/lexicon/types/com/atproto/admin/getModerationEvent.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/getModerationEvent.ts new file mode 100644 index 00000000000..7de567a73db --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/getModerationEvent.ts @@ -0,0 +1,41 @@ +/** + * 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 ComAtprotoAdminDefs from './defs' + +export interface QueryParams { + id: number +} + +export type InputSchema = undefined +export type OutputSchema = ComAtprotoAdminDefs.ModEventViewDetail +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +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/ozone/src/lexicon/types/com/atproto/admin/getRecord.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/getRecord.ts new file mode 100644 index 00000000000..48222d9d819 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/getRecord.ts @@ -0,0 +1,43 @@ +/** + * 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 ComAtprotoAdminDefs from './defs' + +export interface QueryParams { + uri: string + cid?: string +} + +export type InputSchema = undefined +export type OutputSchema = ComAtprotoAdminDefs.RecordViewDetail +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'RecordNotFound' +} + +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/ozone/src/lexicon/types/com/atproto/admin/getRepo.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/getRepo.ts new file mode 100644 index 00000000000..19911baa90a --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/getRepo.ts @@ -0,0 +1,42 @@ +/** + * 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 ComAtprotoAdminDefs from './defs' + +export interface QueryParams { + did: string +} + +export type InputSchema = undefined +export type OutputSchema = ComAtprotoAdminDefs.RepoViewDetail +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'RepoNotFound' +} + +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/ozone/src/lexicon/types/com/atproto/admin/getSubjectStatus.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/getSubjectStatus.ts new file mode 100644 index 00000000000..7315e51e8c2 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/getSubjectStatus.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 ComAtprotoAdminDefs from './defs' +import * as ComAtprotoRepoStrongRef from '../repo/strongRef' + +export interface QueryParams { + did?: string + uri?: string + blob?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + subject: + | ComAtprotoAdminDefs.RepoRef + | ComAtprotoRepoStrongRef.Main + | ComAtprotoAdminDefs.RepoBlobRef + | { $type: string; [k: string]: unknown } + takedown?: ComAtprotoAdminDefs.StatusAttr + [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 +} + +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/ozone/src/lexicon/types/com/atproto/admin/queryModerationEvents.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/queryModerationEvents.ts new file mode 100644 index 00000000000..f3c4f1fbb95 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/queryModerationEvents.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 ComAtprotoAdminDefs from './defs' + +export interface QueryParams { + /** The types of events (fully qualified string in the format of com.atproto.admin#modEvent) to filter by. If not specified, all events are returned. */ + types?: string[] + createdBy?: string + /** Sort direction for the events. Defaults to descending order of created at timestamp. */ + sortDirection: 'asc' | 'desc' + subject?: string + /** If true, events on all record types (posts, lists, profile etc.) owned by the did are returned */ + includeAllUserRecords: boolean + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + events: ComAtprotoAdminDefs.ModEventView[] + [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 +} + +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/ozone/src/lexicon/types/com/atproto/admin/queryModerationStatuses.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/queryModerationStatuses.ts new file mode 100644 index 00000000000..6e1aea1f679 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/queryModerationStatuses.ts @@ -0,0 +1,72 @@ +/** + * 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 ComAtprotoAdminDefs from './defs' + +export interface QueryParams { + subject?: string + /** Search subjects by keyword from comments */ + comment?: string + /** Search subjects reported after a given timestamp */ + reportedAfter?: string + /** Search subjects reported before a given timestamp */ + reportedBefore?: string + /** Search subjects reviewed after a given timestamp */ + reviewedAfter?: string + /** Search subjects reviewed before a given timestamp */ + reviewedBefore?: string + /** By default, we don't include muted subjects in the results. Set this to true to include them. */ + includeMuted?: boolean + /** Specify when fetching subjects in a certain state */ + reviewState?: string + ignoreSubjects?: string[] + /** Get all subject statuses that were reviewed by a specific moderator */ + lastReviewedBy?: string + sortField: 'lastReviewedAt' | 'lastReportedAt' + sortDirection: 'asc' | 'desc' + /** Get subjects that were taken down */ + takendown?: boolean + /** Get subjects in unresolved appealed status */ + appealed?: boolean + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + subjectStatuses: ComAtprotoAdminDefs.SubjectStatusView[] + [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 +} + +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/ozone/src/lexicon/types/com/atproto/admin/searchRepos.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/searchRepos.ts new file mode 100644 index 00000000000..1e7e1a36bb6 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/searchRepos.ts @@ -0,0 +1,51 @@ +/** + * 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 ComAtprotoAdminDefs from './defs' + +export interface QueryParams { + /** DEPRECATED: use 'q' instead */ + term?: string + q?: string + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + repos: ComAtprotoAdminDefs.RepoView[] + [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 +} + +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/ozone/src/lexicon/types/com/atproto/admin/sendEmail.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/sendEmail.ts new file mode 100644 index 00000000000..f94cfb3a083 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/sendEmail.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' + +export interface QueryParams {} + +export interface InputSchema { + recipientDid: string + content: string + subject?: string + senderDid: string + /** Additional comment by the sender that won't be used in the email itself but helpful to provide more context for moderators/reviewers */ + comment?: string + [k: string]: unknown +} + +export interface OutputSchema { + sent: boolean + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +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/ozone/src/lexicon/types/com/atproto/admin/updateAccountEmail.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/updateAccountEmail.ts new file mode 100644 index 00000000000..9e6140256ef --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/updateAccountEmail.ts @@ -0,0 +1,40 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + /** The handle or DID of the repo. */ + account: string + email: string + [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/ozone/src/lexicon/types/com/atproto/admin/updateAccountHandle.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/updateAccountHandle.ts new file mode 100644 index 00000000000..c378f421926 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/updateAccountHandle.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 + handle: string + [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/ozone/src/lexicon/types/com/atproto/admin/updateSubjectStatus.ts b/packages/ozone/src/lexicon/types/com/atproto/admin/updateSubjectStatus.ts new file mode 100644 index 00000000000..559ee948380 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/admin/updateSubjectStatus.ts @@ -0,0 +1,61 @@ +/** + * 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 ComAtprotoAdminDefs from './defs' +import * as ComAtprotoRepoStrongRef from '../repo/strongRef' + +export interface QueryParams {} + +export interface InputSchema { + subject: + | ComAtprotoAdminDefs.RepoRef + | ComAtprotoRepoStrongRef.Main + | ComAtprotoAdminDefs.RepoBlobRef + | { $type: string; [k: string]: unknown } + takedown?: ComAtprotoAdminDefs.StatusAttr + [k: string]: unknown +} + +export interface OutputSchema { + subject: + | ComAtprotoAdminDefs.RepoRef + | ComAtprotoRepoStrongRef.Main + | ComAtprotoAdminDefs.RepoBlobRef + | { $type: string; [k: string]: unknown } + takedown?: ComAtprotoAdminDefs.StatusAttr + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +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/ozone/src/lexicon/types/com/atproto/identity/resolveHandle.ts b/packages/ozone/src/lexicon/types/com/atproto/identity/resolveHandle.ts new file mode 100644 index 00000000000..ef90e99bb30 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/identity/resolveHandle.ts @@ -0,0 +1,46 @@ +/** + * 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 { + /** The handle to resolve. */ + handle: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + did: string + [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 +} + +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/ozone/src/lexicon/types/com/atproto/identity/updateHandle.ts b/packages/ozone/src/lexicon/types/com/atproto/identity/updateHandle.ts new file mode 100644 index 00000000000..1f639c344e9 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/identity/updateHandle.ts @@ -0,0 +1,38 @@ +/** + * 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 { + handle: string + [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/ozone/src/lexicon/types/com/atproto/label/defs.ts b/packages/ozone/src/lexicon/types/com/atproto/label/defs.ts new file mode 100644 index 00000000000..7268650129a --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/label/defs.ts @@ -0,0 +1,73 @@ +/** + * 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' + +/** Metadata tag on an atproto resource (eg, repo or record). */ +export interface Label { + /** DID of the actor who created this label. */ + src: string + /** AT URI of the record, repository (account), or other resource that this label applies to. */ + uri: string + /** Optionally, CID specifying the specific version of 'uri' resource this label applies to. */ + cid?: string + /** The short string name of the value or type of this label. */ + val: string + /** If true, this is a negation label, overwriting a previous label. */ + neg?: boolean + /** Timestamp when this label was created. */ + cts: string + [k: string]: unknown +} + +export function isLabel(v: unknown): v is Label { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.label.defs#label' + ) +} + +export function validateLabel(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.label.defs#label', v) +} + +/** Metadata tags on an atproto record, published by the author within the record. */ +export interface SelfLabels { + values: SelfLabel[] + [k: string]: unknown +} + +export function isSelfLabels(v: unknown): v is SelfLabels { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.label.defs#selfLabels' + ) +} + +export function validateSelfLabels(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.label.defs#selfLabels', v) +} + +/** Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel. */ +export interface SelfLabel { + /** The short string name of the value or type of this label. */ + val: string + [k: string]: unknown +} + +export function isSelfLabel(v: unknown): v is SelfLabel { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.label.defs#selfLabel' + ) +} + +export function validateSelfLabel(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.label.defs#selfLabel', v) +} diff --git a/packages/ozone/src/lexicon/types/com/atproto/label/queryLabels.ts b/packages/ozone/src/lexicon/types/com/atproto/label/queryLabels.ts new file mode 100644 index 00000000000..1d7f8a4def5 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/label/queryLabels.ts @@ -0,0 +1,52 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as ComAtprotoLabelDefs from './defs' + +export interface QueryParams { + /** List of AT URI patterns to match (boolean 'OR'). Each may be a prefix (ending with '*'; will match inclusive of the string leading to '*'), or a full URI. */ + uriPatterns: string[] + /** Optional list of label sources (DIDs) to filter on. */ + sources?: string[] + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + labels: ComAtprotoLabelDefs.Label[] + [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 +} + +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/ozone/src/lexicon/types/com/atproto/label/subscribeLabels.ts b/packages/ozone/src/lexicon/types/com/atproto/label/subscribeLabels.ts new file mode 100644 index 00000000000..9d4b4441ae0 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/label/subscribeLabels.ts @@ -0,0 +1,67 @@ +/** + * 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' +import { HandlerAuth, ErrorFrame } from '@atproto/xrpc-server' +import { IncomingMessage } from 'http' +import * as ComAtprotoLabelDefs from './defs' + +export interface QueryParams { + /** The last known event to backfill from. */ + cursor?: number +} + +export type OutputSchema = + | Labels + | Info + | { $type: string; [k: string]: unknown } +export type HandlerError = ErrorFrame<'FutureCursor'> +export type HandlerOutput = HandlerError | OutputSchema +export type HandlerReqCtx = { + auth: HA + params: QueryParams + req: IncomingMessage + signal: AbortSignal +} +export type Handler = ( + ctx: HandlerReqCtx, +) => AsyncIterable + +export interface Labels { + seq: number + labels: ComAtprotoLabelDefs.Label[] + [k: string]: unknown +} + +export function isLabels(v: unknown): v is Labels { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.label.subscribeLabels#labels' + ) +} + +export function validateLabels(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.label.subscribeLabels#labels', v) +} + +export interface Info { + name: 'OutdatedCursor' | (string & {}) + message?: string + [k: string]: unknown +} + +export function isInfo(v: unknown): v is Info { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.label.subscribeLabels#info' + ) +} + +export function validateInfo(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.label.subscribeLabels#info', v) +} diff --git a/packages/ozone/src/lexicon/types/com/atproto/moderation/createReport.ts b/packages/ozone/src/lexicon/types/com/atproto/moderation/createReport.ts new file mode 100644 index 00000000000..96aaf4a9c29 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/moderation/createReport.ts @@ -0,0 +1,65 @@ +/** + * 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 ComAtprotoModerationDefs from './defs' +import * as ComAtprotoAdminDefs from '../admin/defs' +import * as ComAtprotoRepoStrongRef from '../repo/strongRef' + +export interface QueryParams {} + +export interface InputSchema { + reasonType: ComAtprotoModerationDefs.ReasonType + reason?: string + subject: + | ComAtprotoAdminDefs.RepoRef + | ComAtprotoRepoStrongRef.Main + | { $type: string; [k: string]: unknown } + [k: string]: unknown +} + +export interface OutputSchema { + id: number + reasonType: ComAtprotoModerationDefs.ReasonType + reason?: string + subject: + | ComAtprotoAdminDefs.RepoRef + | ComAtprotoRepoStrongRef.Main + | { $type: string; [k: string]: unknown } + reportedBy: string + createdAt: string + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +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/ozone/src/lexicon/types/com/atproto/moderation/defs.ts b/packages/ozone/src/lexicon/types/com/atproto/moderation/defs.ts new file mode 100644 index 00000000000..08e555c2422 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/moderation/defs.ts @@ -0,0 +1,32 @@ +/** + * 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 type ReasonType = + | 'com.atproto.moderation.defs#reasonSpam' + | 'com.atproto.moderation.defs#reasonViolation' + | 'com.atproto.moderation.defs#reasonMisleading' + | 'com.atproto.moderation.defs#reasonSexual' + | 'com.atproto.moderation.defs#reasonRude' + | 'com.atproto.moderation.defs#reasonOther' + | 'com.atproto.moderation.defs#reasonAppeal' + | (string & {}) + +/** Spam: frequent unwanted promotion, replies, mentions */ +export const REASONSPAM = 'com.atproto.moderation.defs#reasonSpam' +/** Direct violation of server rules, laws, terms of service */ +export const REASONVIOLATION = 'com.atproto.moderation.defs#reasonViolation' +/** Misleading identity, affiliation, or content */ +export const REASONMISLEADING = 'com.atproto.moderation.defs#reasonMisleading' +/** Unwanted or mislabeled sexual content */ +export const REASONSEXUAL = 'com.atproto.moderation.defs#reasonSexual' +/** Rude, harassing, explicit, or otherwise unwelcoming behavior */ +export const REASONRUDE = 'com.atproto.moderation.defs#reasonRude' +/** Other: reports not falling under another report category */ +export const REASONOTHER = 'com.atproto.moderation.defs#reasonOther' +/** Appeal: appeal a previously taken moderation action */ +export const REASONAPPEAL = 'com.atproto.moderation.defs#reasonAppeal' diff --git a/packages/ozone/src/lexicon/types/com/atproto/repo/applyWrites.ts b/packages/ozone/src/lexicon/types/com/atproto/repo/applyWrites.ts new file mode 100644 index 00000000000..61d1e7c28e4 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/repo/applyWrites.ts @@ -0,0 +1,103 @@ +/** + * 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 { + /** The handle or DID of the repo. */ + repo: string + /** Flag for validating the records. */ + validate: boolean + writes: (Create | Update | Delete)[] + swapCommit?: string + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string + error?: 'InvalidSwap' +} + +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 + +/** Create a new record. */ +export interface Create { + collection: string + rkey?: string + value: {} + [k: string]: unknown +} + +export function isCreate(v: unknown): v is Create { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.repo.applyWrites#create' + ) +} + +export function validateCreate(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.repo.applyWrites#create', v) +} + +/** Update an existing record. */ +export interface Update { + collection: string + rkey: string + value: {} + [k: string]: unknown +} + +export function isUpdate(v: unknown): v is Update { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.repo.applyWrites#update' + ) +} + +export function validateUpdate(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.repo.applyWrites#update', v) +} + +/** Delete an existing record. */ +export interface Delete { + collection: string + rkey: string + [k: string]: unknown +} + +export function isDelete(v: unknown): v is Delete { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.repo.applyWrites#delete' + ) +} + +export function validateDelete(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.repo.applyWrites#delete', v) +} diff --git a/packages/ozone/src/lexicon/types/com/atproto/repo/createRecord.ts b/packages/ozone/src/lexicon/types/com/atproto/repo/createRecord.ts new file mode 100644 index 00000000000..df8c5d9e600 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/repo/createRecord.ts @@ -0,0 +1,62 @@ +/** + * 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 { + /** The handle or DID of the repo. */ + repo: string + /** The NSID of the record collection. */ + collection: string + /** The key of the record. */ + rkey?: string + /** Flag for validating the record. */ + validate: boolean + /** The record to create. */ + record: {} + /** Compare and swap with the previous commit by CID. */ + swapCommit?: string + [k: string]: unknown +} + +export interface OutputSchema { + uri: string + cid: string + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'InvalidSwap' +} + +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/ozone/src/lexicon/types/com/atproto/repo/deleteRecord.ts b/packages/ozone/src/lexicon/types/com/atproto/repo/deleteRecord.ts new file mode 100644 index 00000000000..f45118a3769 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/repo/deleteRecord.ts @@ -0,0 +1,48 @@ +/** + * 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 { + /** The handle or DID of the repo. */ + repo: string + /** The NSID of the record collection. */ + collection: string + /** The key of the record. */ + rkey: string + /** Compare and swap with the previous record by CID. */ + swapRecord?: string + /** Compare and swap with the previous commit by CID. */ + swapCommit?: string + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string + error?: 'InvalidSwap' +} + +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/ozone/src/lexicon/types/com/atproto/repo/describeRepo.ts b/packages/ozone/src/lexicon/types/com/atproto/repo/describeRepo.ts new file mode 100644 index 00000000000..7b8a2b995eb --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/repo/describeRepo.ts @@ -0,0 +1,50 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams { + /** The handle or DID of the repo. */ + repo: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + handle: string + did: string + didDoc: {} + collections: string[] + handleIsCorrect: boolean + [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 +} + +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/ozone/src/lexicon/types/com/atproto/repo/getRecord.ts b/packages/ozone/src/lexicon/types/com/atproto/repo/getRecord.ts new file mode 100644 index 00000000000..35c9b4b7166 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/repo/getRecord.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' + +export interface QueryParams { + /** The handle or DID of the repo. */ + repo: string + /** The NSID of the record collection. */ + collection: string + /** The key of the record. */ + rkey: string + /** The CID of the version of the record. If not specified, then return the most recent version. */ + cid?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + uri: string + cid?: string + value: {} + [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 +} + +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/ozone/src/lexicon/types/com/atproto/repo/listRecords.ts b/packages/ozone/src/lexicon/types/com/atproto/repo/listRecords.ts new file mode 100644 index 00000000000..a6cf6abd1f3 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/repo/listRecords.ts @@ -0,0 +1,77 @@ +/** + * 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 { + /** The handle or DID of the repo. */ + repo: string + /** The NSID of the record type. */ + collection: string + /** The number of records to return. */ + limit: number + cursor?: string + /** DEPRECATED: The lowest sort-ordered rkey to start from (exclusive) */ + rkeyStart?: string + /** DEPRECATED: The highest sort-ordered rkey to stop at (exclusive) */ + rkeyEnd?: string + /** Flag to reverse the order of the returned records. */ + reverse?: boolean +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + records: Record[] + [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 +} + +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 + +export interface Record { + uri: string + cid: string + value: {} + [k: string]: unknown +} + +export function isRecord(v: unknown): v is Record { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.repo.listRecords#record' + ) +} + +export function validateRecord(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.repo.listRecords#record', v) +} diff --git a/packages/ozone/src/lexicon/types/com/atproto/repo/putRecord.ts b/packages/ozone/src/lexicon/types/com/atproto/repo/putRecord.ts new file mode 100644 index 00000000000..f10f773c1c4 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/repo/putRecord.ts @@ -0,0 +1,64 @@ +/** + * 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 { + /** The handle or DID of the repo. */ + repo: string + /** The NSID of the record collection. */ + collection: string + /** The key of the record. */ + rkey: string + /** Flag for validating the record. */ + validate: boolean + /** The record to write. */ + record: {} + /** Compare and swap with the previous record by CID. */ + swapRecord?: string | null + /** Compare and swap with the previous commit by CID. */ + swapCommit?: string + [k: string]: unknown +} + +export interface OutputSchema { + uri: string + cid: string + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'InvalidSwap' +} + +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/ozone/src/lexicon/types/com/atproto/repo/strongRef.ts b/packages/ozone/src/lexicon/types/com/atproto/repo/strongRef.ts new file mode 100644 index 00000000000..8b21a6ddf81 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/repo/strongRef.ts @@ -0,0 +1,26 @@ +/** + * 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 Main { + uri: string + cid: string + [k: string]: unknown +} + +export function isMain(v: unknown): v is Main { + return ( + isObj(v) && + hasProp(v, '$type') && + (v.$type === 'com.atproto.repo.strongRef#main' || + v.$type === 'com.atproto.repo.strongRef') + ) +} + +export function validateMain(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.repo.strongRef#main', v) +} diff --git a/packages/ozone/src/lexicon/types/com/atproto/repo/uploadBlob.ts b/packages/ozone/src/lexicon/types/com/atproto/repo/uploadBlob.ts new file mode 100644 index 00000000000..ad6002df925 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/repo/uploadBlob.ts @@ -0,0 +1,47 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import stream from 'stream' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export type InputSchema = string | Uint8Array + +export interface OutputSchema { + blob: BlobRef + [k: string]: unknown +} + +export interface HandlerInput { + encoding: '*/*' + body: stream.Readable +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +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/ozone/src/lexicon/types/com/atproto/server/confirmEmail.ts b/packages/ozone/src/lexicon/types/com/atproto/server/confirmEmail.ts new file mode 100644 index 00000000000..ffaeeb8fe75 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/confirmEmail.ts @@ -0,0 +1,40 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + email: string + token: string + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string + error?: 'AccountNotFound' | 'ExpiredToken' | 'InvalidToken' | 'InvalidEmail' +} + +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/ozone/src/lexicon/types/com/atproto/server/createAccount.ts b/packages/ozone/src/lexicon/types/com/atproto/server/createAccount.ts new file mode 100644 index 00000000000..109d34cf202 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/createAccount.ts @@ -0,0 +1,67 @@ +/** + * 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 { + email?: string + handle: string + did?: string + inviteCode?: string + password?: string + recoveryKey?: string + plcOp?: {} + [k: string]: unknown +} + +export interface OutputSchema { + accessJwt: string + refreshJwt: string + handle: string + did: string + didDoc?: {} + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: + | 'InvalidHandle' + | 'InvalidPassword' + | 'InvalidInviteCode' + | 'HandleNotAvailable' + | 'UnsupportedDomain' + | 'UnresolvableDid' + | 'IncompatibleDidDoc' +} + +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/ozone/src/lexicon/types/com/atproto/server/createAppPassword.ts b/packages/ozone/src/lexicon/types/com/atproto/server/createAppPassword.ts new file mode 100644 index 00000000000..8e4a0a519e0 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/createAppPassword.ts @@ -0,0 +1,69 @@ +/** + * 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 { + name: string + [k: string]: unknown +} + +export type OutputSchema = AppPassword + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'AccountTakedown' +} + +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 + +export interface AppPassword { + name: string + password: string + createdAt: string + [k: string]: unknown +} + +export function isAppPassword(v: unknown): v is AppPassword { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.server.createAppPassword#appPassword' + ) +} + +export function validateAppPassword(v: unknown): ValidationResult { + return lexicons.validate( + 'com.atproto.server.createAppPassword#appPassword', + v, + ) +} diff --git a/packages/ozone/src/lexicon/types/com/atproto/server/createInviteCode.ts b/packages/ozone/src/lexicon/types/com/atproto/server/createInviteCode.ts new file mode 100644 index 00000000000..acfac56ba76 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/createInviteCode.ts @@ -0,0 +1,50 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + useCount: number + forAccount?: string + [k: string]: unknown +} + +export interface OutputSchema { + code: string + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +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/ozone/src/lexicon/types/com/atproto/server/createInviteCodes.ts b/packages/ozone/src/lexicon/types/com/atproto/server/createInviteCodes.ts new file mode 100644 index 00000000000..5887d77fada --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/createInviteCodes.ts @@ -0,0 +1,72 @@ +/** + * 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 { + codeCount: number + useCount: number + forAccounts?: string[] + [k: string]: unknown +} + +export interface OutputSchema { + codes: AccountCodes[] + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +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 + +export interface AccountCodes { + account: string + codes: string[] + [k: string]: unknown +} + +export function isAccountCodes(v: unknown): v is AccountCodes { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.server.createInviteCodes#accountCodes' + ) +} + +export function validateAccountCodes(v: unknown): ValidationResult { + return lexicons.validate( + 'com.atproto.server.createInviteCodes#accountCodes', + v, + ) +} diff --git a/packages/ozone/src/lexicon/types/com/atproto/server/createSession.ts b/packages/ozone/src/lexicon/types/com/atproto/server/createSession.ts new file mode 100644 index 00000000000..2cd448703a6 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/createSession.ts @@ -0,0 +1,58 @@ +/** + * 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 { + /** Handle or other identifier supported by the server for the authenticating user. */ + identifier: string + password: string + [k: string]: unknown +} + +export interface OutputSchema { + accessJwt: string + refreshJwt: string + handle: string + did: string + didDoc?: {} + email?: string + emailConfirmed?: boolean + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'AccountTakedown' +} + +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/ozone/src/lexicon/types/com/atproto/server/defs.ts b/packages/ozone/src/lexicon/types/com/atproto/server/defs.ts new file mode 100644 index 00000000000..9bd67c9d7a5 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/defs.ts @@ -0,0 +1,48 @@ +/** + * 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 InviteCode { + code: string + available: number + disabled: boolean + forAccount: string + createdBy: string + createdAt: string + uses: InviteCodeUse[] + [k: string]: unknown +} + +export function isInviteCode(v: unknown): v is InviteCode { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.server.defs#inviteCode' + ) +} + +export function validateInviteCode(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.server.defs#inviteCode', v) +} + +export interface InviteCodeUse { + usedBy: string + usedAt: string + [k: string]: unknown +} + +export function isInviteCodeUse(v: unknown): v is InviteCodeUse { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.server.defs#inviteCodeUse' + ) +} + +export function validateInviteCodeUse(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.server.defs#inviteCodeUse', v) +} diff --git a/packages/ozone/src/lexicon/types/com/atproto/server/deleteAccount.ts b/packages/ozone/src/lexicon/types/com/atproto/server/deleteAccount.ts new file mode 100644 index 00000000000..37ddbba13e0 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/deleteAccount.ts @@ -0,0 +1,41 @@ +/** + * 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 + password: string + token: string + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string + error?: 'ExpiredToken' | 'InvalidToken' +} + +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/ozone/src/lexicon/types/com/atproto/server/deleteSession.ts b/packages/ozone/src/lexicon/types/com/atproto/server/deleteSession.ts new file mode 100644 index 00000000000..e4244870425 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/deleteSession.ts @@ -0,0 +1,31 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export type InputSchema = undefined +export type HandlerInput = undefined + +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/ozone/src/lexicon/types/com/atproto/server/describeServer.ts b/packages/ozone/src/lexicon/types/com/atproto/server/describeServer.ts new file mode 100644 index 00000000000..bc73d541a04 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/describeServer.ts @@ -0,0 +1,63 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export type InputSchema = undefined + +export interface OutputSchema { + inviteCodeRequired?: boolean + availableUserDomains: string[] + links?: Links + [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 +} + +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 + +export interface Links { + privacyPolicy?: string + termsOfService?: string + [k: string]: unknown +} + +export function isLinks(v: unknown): v is Links { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.server.describeServer#links' + ) +} + +export function validateLinks(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.server.describeServer#links', v) +} diff --git a/packages/ozone/src/lexicon/types/com/atproto/server/getAccountInviteCodes.ts b/packages/ozone/src/lexicon/types/com/atproto/server/getAccountInviteCodes.ts new file mode 100644 index 00000000000..e387a5e38e4 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/getAccountInviteCodes.ts @@ -0,0 +1,48 @@ +/** + * 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 ComAtprotoServerDefs from './defs' + +export interface QueryParams { + includeUsed: boolean + createAvailable: boolean +} + +export type InputSchema = undefined + +export interface OutputSchema { + codes: ComAtprotoServerDefs.InviteCode[] + [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?: 'DuplicateCreate' +} + +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/ozone/src/lexicon/types/com/atproto/server/getSession.ts b/packages/ozone/src/lexicon/types/com/atproto/server/getSession.ts new file mode 100644 index 00000000000..4f95acf523d --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/getSession.ts @@ -0,0 +1,47 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export type InputSchema = undefined + +export interface OutputSchema { + handle: string + did: string + email?: string + emailConfirmed?: boolean + didDoc?: {} + [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 +} + +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/ozone/src/lexicon/types/com/atproto/server/listAppPasswords.ts b/packages/ozone/src/lexicon/types/com/atproto/server/listAppPasswords.ts new file mode 100644 index 00000000000..ebd74da9d39 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/listAppPasswords.ts @@ -0,0 +1,62 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export type InputSchema = undefined + +export interface OutputSchema { + passwords: AppPassword[] + [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?: 'AccountTakedown' +} + +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 + +export interface AppPassword { + name: string + createdAt: string + [k: string]: unknown +} + +export function isAppPassword(v: unknown): v is AppPassword { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.server.listAppPasswords#appPassword' + ) +} + +export function validateAppPassword(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.server.listAppPasswords#appPassword', v) +} diff --git a/packages/ozone/src/lexicon/types/com/atproto/server/refreshSession.ts b/packages/ozone/src/lexicon/types/com/atproto/server/refreshSession.ts new file mode 100644 index 00000000000..35874f78a69 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/refreshSession.ts @@ -0,0 +1,48 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export type InputSchema = undefined + +export interface OutputSchema { + accessJwt: string + refreshJwt: string + handle: string + did: string + didDoc?: {} + [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?: 'AccountTakedown' +} + +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/ozone/src/lexicon/types/com/atproto/server/requestAccountDelete.ts b/packages/ozone/src/lexicon/types/com/atproto/server/requestAccountDelete.ts new file mode 100644 index 00000000000..e4244870425 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/requestAccountDelete.ts @@ -0,0 +1,31 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export type InputSchema = undefined +export type HandlerInput = undefined + +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/ozone/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts b/packages/ozone/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts new file mode 100644 index 00000000000..e4244870425 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/requestEmailConfirmation.ts @@ -0,0 +1,31 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export type InputSchema = undefined +export type HandlerInput = undefined + +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/ozone/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts b/packages/ozone/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts new file mode 100644 index 00000000000..6876d44ca46 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/requestEmailUpdate.ts @@ -0,0 +1,43 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export type InputSchema = undefined + +export interface OutputSchema { + tokenRequired: boolean + [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 +} + +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/ozone/src/lexicon/types/com/atproto/server/requestPasswordReset.ts b/packages/ozone/src/lexicon/types/com/atproto/server/requestPasswordReset.ts new file mode 100644 index 00000000000..47fb4bb62f3 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/requestPasswordReset.ts @@ -0,0 +1,38 @@ +/** + * 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 { + email: string + [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/ozone/src/lexicon/types/com/atproto/server/reserveSigningKey.ts b/packages/ozone/src/lexicon/types/com/atproto/server/reserveSigningKey.ts new file mode 100644 index 00000000000..ad5a5a8758c --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/reserveSigningKey.ts @@ -0,0 +1,51 @@ +/** + * 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 { + /** The did to reserve a new did:key for */ + did?: string + [k: string]: unknown +} + +export interface OutputSchema { + /** Public signing key in the form of a did:key. */ + signingKey: string + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +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/ozone/src/lexicon/types/com/atproto/server/resetPassword.ts b/packages/ozone/src/lexicon/types/com/atproto/server/resetPassword.ts new file mode 100644 index 00000000000..9e6ece3e4c4 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/resetPassword.ts @@ -0,0 +1,40 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' + +export interface QueryParams {} + +export interface InputSchema { + token: string + password: string + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string + error?: 'ExpiredToken' | 'InvalidToken' +} + +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/ozone/src/lexicon/types/com/atproto/server/revokeAppPassword.ts b/packages/ozone/src/lexicon/types/com/atproto/server/revokeAppPassword.ts new file mode 100644 index 00000000000..4627f68eaa2 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/revokeAppPassword.ts @@ -0,0 +1,38 @@ +/** + * 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 { + name: string + [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/ozone/src/lexicon/types/com/atproto/server/updateEmail.ts b/packages/ozone/src/lexicon/types/com/atproto/server/updateEmail.ts new file mode 100644 index 00000000000..c88bd3021b2 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/server/updateEmail.ts @@ -0,0 +1,41 @@ +/** + * 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 { + email: string + /** Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed. */ + token?: string + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string + error?: 'ExpiredToken' | 'InvalidToken' | 'TokenRequired' +} + +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/ozone/src/lexicon/types/com/atproto/sync/getBlob.ts b/packages/ozone/src/lexicon/types/com/atproto/sync/getBlob.ts new file mode 100644 index 00000000000..60750902472 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/sync/getBlob.ts @@ -0,0 +1,43 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import stream from 'stream' +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 { + /** The DID of the repo. */ + did: string + /** The CID of the blob to fetch */ + cid: string +} + +export type InputSchema = undefined +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: '*/*' + body: Uint8Array | stream.Readable + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +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/ozone/src/lexicon/types/com/atproto/sync/getBlocks.ts b/packages/ozone/src/lexicon/types/com/atproto/sync/getBlocks.ts new file mode 100644 index 00000000000..e73410efb41 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/sync/getBlocks.ts @@ -0,0 +1,42 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import stream from 'stream' +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 { + /** The DID of the repo. */ + did: string + cids: string[] +} + +export type InputSchema = undefined +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/vnd.ipld.car' + body: Uint8Array | stream.Readable + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +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/ozone/src/lexicon/types/com/atproto/sync/getCheckout.ts b/packages/ozone/src/lexicon/types/com/atproto/sync/getCheckout.ts new file mode 100644 index 00000000000..63a657e56b9 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/sync/getCheckout.ts @@ -0,0 +1,41 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import stream from 'stream' +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 { + /** The DID of the repo. */ + did: string +} + +export type InputSchema = undefined +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/vnd.ipld.car' + body: Uint8Array | stream.Readable + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +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/ozone/src/lexicon/types/com/atproto/sync/getHead.ts b/packages/ozone/src/lexicon/types/com/atproto/sync/getHead.ts new file mode 100644 index 00000000000..586ae1a4189 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/sync/getHead.ts @@ -0,0 +1,47 @@ +/** + * 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 { + /** The DID of the repo. */ + did: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + root: string + [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?: 'HeadNotFound' +} + +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/ozone/src/lexicon/types/com/atproto/sync/getLatestCommit.ts b/packages/ozone/src/lexicon/types/com/atproto/sync/getLatestCommit.ts new file mode 100644 index 00000000000..9b91e878724 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/sync/getLatestCommit.ts @@ -0,0 +1,48 @@ +/** + * 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 { + /** The DID of the repo. */ + did: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cid: string + rev: string + [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?: 'RepoNotFound' +} + +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/ozone/src/lexicon/types/com/atproto/sync/getRecord.ts b/packages/ozone/src/lexicon/types/com/atproto/sync/getRecord.ts new file mode 100644 index 00000000000..297f0ac7794 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/sync/getRecord.ts @@ -0,0 +1,45 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import stream from 'stream' +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 { + /** The DID of the repo. */ + did: string + collection: string + rkey: string + /** An optional past commit CID. */ + commit?: string +} + +export type InputSchema = undefined +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/vnd.ipld.car' + body: Uint8Array | stream.Readable + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +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/ozone/src/lexicon/types/com/atproto/sync/getRepo.ts b/packages/ozone/src/lexicon/types/com/atproto/sync/getRepo.ts new file mode 100644 index 00000000000..495d31a1a22 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/sync/getRepo.ts @@ -0,0 +1,43 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import stream from 'stream' +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 { + /** The DID of the repo. */ + did: string + /** The revision of the repo to catch up from. */ + since?: string +} + +export type InputSchema = undefined +export type HandlerInput = undefined + +export interface HandlerSuccess { + encoding: 'application/vnd.ipld.car' + body: Uint8Array | stream.Readable + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +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/ozone/src/lexicon/types/com/atproto/sync/listBlobs.ts b/packages/ozone/src/lexicon/types/com/atproto/sync/listBlobs.ts new file mode 100644 index 00000000000..b397bb3b3df --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/sync/listBlobs.ts @@ -0,0 +1,51 @@ +/** + * 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 { + /** The DID of the repo. */ + did: string + /** Optional revision of the repo to list blobs since. */ + since?: string + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + cids: string[] + [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 +} + +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/ozone/src/lexicon/types/com/atproto/sync/listRepos.ts b/packages/ozone/src/lexicon/types/com/atproto/sync/listRepos.ts new file mode 100644 index 00000000000..783a8e314c2 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/sync/listRepos.ts @@ -0,0 +1,66 @@ +/** + * 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 { + limit: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + repos: Repo[] + [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 +} + +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 + +export interface Repo { + did: string + head: string + rev: string + [k: string]: unknown +} + +export function isRepo(v: unknown): v is Repo { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.sync.listRepos#repo' + ) +} + +export function validateRepo(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.sync.listRepos#repo', v) +} diff --git a/packages/ozone/src/lexicon/types/com/atproto/sync/notifyOfUpdate.ts b/packages/ozone/src/lexicon/types/com/atproto/sync/notifyOfUpdate.ts new file mode 100644 index 00000000000..3d310c1139a --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/sync/notifyOfUpdate.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 { + /** Hostname of the service that is notifying of update. */ + hostname: string + [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/ozone/src/lexicon/types/com/atproto/sync/requestCrawl.ts b/packages/ozone/src/lexicon/types/com/atproto/sync/requestCrawl.ts new file mode 100644 index 00000000000..87ef20d7297 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/sync/requestCrawl.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 { + /** Hostname of the service that is requesting to be crawled. */ + hostname: string + [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/ozone/src/lexicon/types/com/atproto/sync/subscribeRepos.ts b/packages/ozone/src/lexicon/types/com/atproto/sync/subscribeRepos.ts new file mode 100644 index 00000000000..fb334778bf6 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/sync/subscribeRepos.ts @@ -0,0 +1,161 @@ +/** + * 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' +import { HandlerAuth, ErrorFrame } from '@atproto/xrpc-server' +import { IncomingMessage } from 'http' + +export interface QueryParams { + /** The last known event to backfill from. */ + cursor?: number +} + +export type OutputSchema = + | Commit + | Handle + | Migrate + | Tombstone + | Info + | { $type: string; [k: string]: unknown } +export type HandlerError = ErrorFrame<'FutureCursor' | 'ConsumerTooSlow'> +export type HandlerOutput = HandlerError | OutputSchema +export type HandlerReqCtx = { + auth: HA + params: QueryParams + req: IncomingMessage + signal: AbortSignal +} +export type Handler = ( + ctx: HandlerReqCtx, +) => AsyncIterable + +export interface Commit { + seq: number + rebase: boolean + tooBig: boolean + repo: string + commit: CID + prev?: CID | null + /** The rev of the emitted commit. */ + rev: string + /** The rev of the last emitted commit from this repo. */ + since: string | null + /** CAR file containing relevant blocks. */ + blocks: Uint8Array + ops: RepoOp[] + blobs: CID[] + time: string + [k: string]: unknown +} + +export function isCommit(v: unknown): v is Commit { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.sync.subscribeRepos#commit' + ) +} + +export function validateCommit(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.sync.subscribeRepos#commit', v) +} + +export interface Handle { + seq: number + did: string + handle: string + time: string + [k: string]: unknown +} + +export function isHandle(v: unknown): v is Handle { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.sync.subscribeRepos#handle' + ) +} + +export function validateHandle(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.sync.subscribeRepos#handle', v) +} + +export interface Migrate { + seq: number + did: string + migrateTo: string | null + time: string + [k: string]: unknown +} + +export function isMigrate(v: unknown): v is Migrate { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.sync.subscribeRepos#migrate' + ) +} + +export function validateMigrate(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.sync.subscribeRepos#migrate', v) +} + +export interface Tombstone { + seq: number + did: string + time: string + [k: string]: unknown +} + +export function isTombstone(v: unknown): v is Tombstone { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.sync.subscribeRepos#tombstone' + ) +} + +export function validateTombstone(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.sync.subscribeRepos#tombstone', v) +} + +export interface Info { + name: 'OutdatedCursor' | (string & {}) + message?: string + [k: string]: unknown +} + +export function isInfo(v: unknown): v is Info { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.sync.subscribeRepos#info' + ) +} + +export function validateInfo(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.sync.subscribeRepos#info', v) +} + +/** A repo operation, ie a write of a single record. For creates and updates, CID is the record's CID as of this operation. For deletes, it's null. */ +export interface RepoOp { + action: 'create' | 'update' | 'delete' | (string & {}) + path: string + cid: CID | null + [k: string]: unknown +} + +export function isRepoOp(v: unknown): v is RepoOp { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'com.atproto.sync.subscribeRepos#repoOp' + ) +} + +export function validateRepoOp(v: unknown): ValidationResult { + return lexicons.validate('com.atproto.sync.subscribeRepos#repoOp', v) +} diff --git a/packages/ozone/src/lexicon/types/com/atproto/temp/fetchLabels.ts b/packages/ozone/src/lexicon/types/com/atproto/temp/fetchLabels.ts new file mode 100644 index 00000000000..39341fd3a0e --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/temp/fetchLabels.ts @@ -0,0 +1,47 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as ComAtprotoLabelDefs from '../label/defs' + +export interface QueryParams { + since?: number + limit: number +} + +export type InputSchema = undefined + +export interface OutputSchema { + labels: ComAtprotoLabelDefs.Label[] + [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 +} + +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/ozone/src/lexicon/types/com/atproto/temp/importRepo.ts b/packages/ozone/src/lexicon/types/com/atproto/temp/importRepo.ts new file mode 100644 index 00000000000..d88361d9856 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/temp/importRepo.ts @@ -0,0 +1,45 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import stream from 'stream' +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 { + /** The DID of the repo. */ + did: string +} + +export type InputSchema = string | Uint8Array + +export interface HandlerInput { + encoding: 'application/vnd.ipld.car' + body: stream.Readable +} + +export interface HandlerSuccess { + encoding: 'text/plain' + body: Uint8Array | stream.Readable + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +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/ozone/src/lexicon/types/com/atproto/temp/pushBlob.ts b/packages/ozone/src/lexicon/types/com/atproto/temp/pushBlob.ts new file mode 100644 index 00000000000..97e890dbb14 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/temp/pushBlob.ts @@ -0,0 +1,39 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import stream from 'stream' +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 { + /** The DID of the repo. */ + did: string +} + +export type InputSchema = string | Uint8Array + +export interface HandlerInput { + encoding: '*/*' + body: stream.Readable +} + +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/ozone/src/lexicon/types/com/atproto/temp/transferAccount.ts b/packages/ozone/src/lexicon/types/com/atproto/temp/transferAccount.ts new file mode 100644 index 00000000000..86c1d750e07 --- /dev/null +++ b/packages/ozone/src/lexicon/types/com/atproto/temp/transferAccount.ts @@ -0,0 +1,62 @@ +/** + * 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 { + handle: string + did: string + plcOp: {} + [k: string]: unknown +} + +export interface OutputSchema { + accessJwt: string + refreshJwt: string + handle: string + did: string + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: + | 'InvalidHandle' + | 'InvalidPassword' + | 'InvalidInviteCode' + | 'HandleNotAvailable' + | 'UnsupportedDomain' + | 'UnresolvableDid' + | 'IncompatibleDidDoc' +} + +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/ozone/src/lexicon/util.ts b/packages/ozone/src/lexicon/util.ts new file mode 100644 index 00000000000..d7e70440d6e --- /dev/null +++ b/packages/ozone/src/lexicon/util.ts @@ -0,0 +1,13 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +export function isObj(v: unknown): v is Record { + return typeof v === 'object' && v !== null +} + +export function hasProp( + data: object, + prop: K, +): data is Record { + return prop in data +} diff --git a/packages/ozone/src/logger.ts b/packages/ozone/src/logger.ts new file mode 100644 index 00000000000..42b0a78b2a4 --- /dev/null +++ b/packages/ozone/src/logger.ts @@ -0,0 +1,19 @@ +import pinoHttp from 'pino-http' +import { subsystemLogger } from '@atproto/common' + +export const dbLogger: ReturnType = + subsystemLogger('ozone:db') +export const httpLogger: ReturnType = + subsystemLogger('ozone') + +export const loggerMiddleware = pinoHttp({ + logger: httpLogger, + serializers: { + err: (err) => { + return { + code: err?.code, + message: err?.message, + } + }, + }, +}) diff --git a/packages/ozone/src/mod-service/index.ts b/packages/ozone/src/mod-service/index.ts new file mode 100644 index 00000000000..4c1c84e55dc --- /dev/null +++ b/packages/ozone/src/mod-service/index.ts @@ -0,0 +1,758 @@ +import { CID } from 'multiformats/cid' +import { AtUri, INVALID_HANDLE } from '@atproto/syntax' +import { InvalidRequestError } from '@atproto/xrpc-server' +import { addHoursToDate } from '@atproto/common' +import { Database } from '../db' +import { AppviewAuth, ModerationViews } from './views' +import { Main as StrongRef } from '../lexicon/types/com/atproto/repo/strongRef' +import { + isModEventComment, + isModEventLabel, + isModEventMute, + isModEventReport, + isModEventTakedown, + isModEventEmail, + RepoRef, + RepoBlobRef, +} from '../lexicon/types/com/atproto/admin/defs' +import { + adjustModerationSubjectStatus, + getStatusIdentifierFromSubject, +} from './status' +import { + ModEventType, + ModerationEventRow, + ModerationSubjectStatusRow, + ReversibleModerationEvent, +} from './types' +import { ModerationEvent } from '../db/schema/moderation_event' +import { StatusKeyset, TimeIdKeyset, paginate } from '../db/pagination' +import AtpAgent from '@atproto/api' +import { Label } from '../lexicon/types/com/atproto/label/defs' +import { Insertable, sql } from 'kysely' +import { + ModSubject, + RecordSubject, + RepoSubject, + subjectFromStatusRow, +} from './subject' +import { BlobPushEvent } from '../db/schema/blob_push_event' +import { BackgroundQueue } from '../background' +import { EventPusher } from '../daemon' + +export type ModerationServiceCreator = (db: Database) => ModerationService + +export class ModerationService { + constructor( + public db: Database, + public backgroundQueue: BackgroundQueue, + public eventPusher: EventPusher, + public appviewAgent: AtpAgent, + private appviewAuth: AppviewAuth, + ) {} + + static creator( + backgroundQueue: BackgroundQueue, + eventPusher: EventPusher, + appviewAgent: AtpAgent, + appviewAuth: AppviewAuth, + ) { + return (db: Database) => + new ModerationService( + db, + backgroundQueue, + eventPusher, + appviewAgent, + appviewAuth, + ) + } + + views = new ModerationViews(this.db, this.appviewAgent, this.appviewAuth) + + async getEvent(id: number): Promise { + return await this.db.db + .selectFrom('moderation_event') + .selectAll() + .where('id', '=', id) + .executeTakeFirst() + } + + async getEventOrThrow(id: number): Promise { + const event = await this.getEvent(id) + if (!event) throw new InvalidRequestError('Moderation event not found') + return event + } + + async getEvents(opts: { + subject?: string + createdBy?: string + limit: number + cursor?: string + includeAllUserRecords: boolean + types: ModerationEvent['action'][] + sortDirection?: 'asc' | 'desc' + }): Promise<{ cursor?: string; events: ModerationEventRow[] }> { + const { + subject, + createdBy, + limit, + cursor, + includeAllUserRecords, + sortDirection = 'desc', + types, + } = opts + let builder = this.db.db.selectFrom('moderation_event').selectAll() + if (subject) { + builder = builder.where((qb) => { + if (includeAllUserRecords) { + // If subject is an at-uri, we need to extract the DID from the at-uri + // otherwise, subject is probably a DID already + if (subject.startsWith('at://')) { + const uri = new AtUri(subject) + return qb.where('subjectDid', '=', uri.hostname) + } + return qb.where('subjectDid', '=', subject) + } + return qb + .where((subQb) => + subQb + .where('subjectDid', '=', subject) + .where('subjectUri', 'is', null), + ) + .orWhere('subjectUri', '=', subject) + }) + } + if (types.length) { + builder = builder.where((qb) => { + if (types.length === 1) { + return qb.where('action', '=', types[0]) + } + + return qb.where('action', 'in', types) + }) + } + if (createdBy) { + builder = builder.where('createdBy', '=', createdBy) + } + + const { ref } = this.db.db.dynamic + const keyset = new TimeIdKeyset( + ref(`moderation_event.createdAt`), + ref('moderation_event.id'), + ) + const paginatedBuilder = paginate(builder, { + limit, + cursor, + keyset, + direction: sortDirection, + tryIndex: true, + }) + + const result = await paginatedBuilder.execute() + + const infos = await this.views.getAccoutInfosByDid([ + ...result.map((row) => row.subjectDid), + ...result.map((row) => row.createdBy), + ]) + + const resultWithHandles = result.map((r) => ({ + ...r, + creatorHandle: infos.get(r.createdBy)?.handle, + subjectHandle: infos.get(r.subjectDid)?.handle, + })) + + return { cursor: keyset.packFromResult(result), events: resultWithHandles } + } + + async getReport(id: number): Promise { + return await this.db.db + .selectFrom('moderation_event') + .where('action', '=', 'com.atproto.admin.defs#modEventReport') + .selectAll() + .where('id', '=', id) + .executeTakeFirst() + } + + async getCurrentStatus( + subject: { did: string } | { uri: AtUri } | { cids: CID[] }, + ) { + let builder = this.db.db.selectFrom('moderation_subject_status').selectAll() + if ('did' in subject) { + builder = builder.where('did', '=', subject.did) + } else if ('uri' in subject) { + builder = builder.where('recordPath', '=', subject.uri.toString()) + } + // TODO: Handle the cid status + return await builder.execute() + } + + async logEvent(info: { + event: ModEventType + subject: ModSubject + createdBy: string + createdAt?: Date + }): Promise { + this.db.assertTransaction() + const { event, subject, createdBy, createdAt = new Date() } = info + + const createLabelVals = + isModEventLabel(event) && event.createLabelVals.length > 0 + ? event.createLabelVals.join(' ') + : undefined + const negateLabelVals = + isModEventLabel(event) && event.negateLabelVals.length > 0 + ? event.negateLabelVals.join(' ') + : undefined + + const meta: Record = {} + + if (isModEventReport(event)) { + meta.reportType = event.reportType + } + + if (isModEventComment(event) && event.sticky) { + meta.sticky = event.sticky + } + + if (isModEventEmail(event)) { + meta.subjectLine = event.subjectLine + } + + const modEvent = await this.db.db + .insertInto('moderation_event') + .values({ + comment: event.comment ? `${event.comment}` : null, + action: event.$type as ModerationEvent['action'], + createdAt: createdAt.toISOString(), + createdBy, + createLabelVals, + negateLabelVals, + durationInHours: event.durationInHours + ? Number(event.durationInHours) + : null, + meta, + expiresAt: + (isModEventTakedown(event) || isModEventMute(event)) && + event.durationInHours + ? addHoursToDate(event.durationInHours, createdAt).toISOString() + : undefined, + ...subject.info(), + }) + .returningAll() + .executeTakeFirstOrThrow() + + await adjustModerationSubjectStatus(this.db, modEvent, subject.blobCids) + + return modEvent + } + + async getLastReversibleEventForSubject(subject: ReversalSubject) { + // If the subject is neither suspended nor muted don't bother finding the last reversible event + // Ideally, this should never happen because the caller of this method should only call this + // after ensuring that the suspended or muted subjects are being reversed + if (!subject.reverseMute && !subject.reverseSuspend) { + return null + } + + let builder = this.db.db + .selectFrom('moderation_event') + .where('subjectDid', '=', subject.subject.did) + + if (subject.subject.recordPath) { + builder = builder.where( + 'subjectUri', + 'like', + `%${subject.subject.recordPath}%`, + ) + } + + // Means the subject was suspended and needs to be unsuspended + if (subject.reverseSuspend) { + builder = builder + .where('action', '=', 'com.atproto.admin.defs#modEventTakedown') + .where('durationInHours', 'is not', null) + } + if (subject.reverseMute) { + builder = builder + .where('action', '=', 'com.atproto.admin.defs#modEventMute') + .where('durationInHours', 'is not', null) + } + + return await builder + .orderBy('id', 'desc') + .selectAll() + .limit(1) + .executeTakeFirst() + } + + async getSubjectsDueForReversal(): Promise { + const now = new Date().toISOString() + const subjects = await this.db.db + .selectFrom('moderation_subject_status') + .where('suspendUntil', '<', now) + .orWhere('muteUntil', '<', now) + .selectAll() + .execute() + + return subjects.map((row) => ({ + subject: subjectFromStatusRow(row), + reverseSuspend: !!row.suspendUntil && row.suspendUntil < now, + reverseMute: !!row.muteUntil && row.muteUntil < now, + })) + } + + async isSubjectSuspended(did: string): Promise { + const res = await this.db.db + .selectFrom('moderation_subject_status') + .where('did', '=', did) + .where('recordPath', '=', '') + .where('suspendUntil', '>', new Date().toISOString()) + .select('did') + .limit(1) + .executeTakeFirst() + return !!res + } + + async revertState({ + createdBy, + createdAt, + comment, + action, + subject, + }: ReversibleModerationEvent): Promise { + const isRevertingTakedown = + action === 'com.atproto.admin.defs#modEventTakedown' + this.db.assertTransaction() + const result = await this.logEvent({ + event: { + $type: isRevertingTakedown + ? 'com.atproto.admin.defs#modEventReverseTakedown' + : 'com.atproto.admin.defs#modEventUnmute', + comment: comment ?? undefined, + }, + createdAt, + createdBy, + subject, + }) + + if (isRevertingTakedown) { + if (subject.isRepo()) { + await this.reverseTakedownRepo(subject) + } else if (subject.isRecord()) { + await this.reverseTakedownRecord(subject) + } + } + + return result + } + + async takedownRepo( + subject: RepoSubject, + takedownId: number, + isSuspend = false, + ) { + const takedownRef = `BSKY-${ + isSuspend ? 'SUSPEND' : 'TAKEDOWN' + }-${takedownId}` + const values = TAKEDOWNS.map((eventType) => ({ + eventType, + subjectDid: subject.did, + takedownRef, + })) + const repoEvts = await this.db.db + .insertInto('repo_push_event') + .values(values) + .onConflict((oc) => + oc.columns(['subjectDid', 'eventType']).doUpdateSet({ + takedownRef, + confirmedAt: null, + attempts: 0, + lastAttempted: null, + }), + ) + .returning('id') + .execute() + + this.db.onCommit(() => { + this.backgroundQueue.add(async () => { + await Promise.all( + repoEvts.map((evt) => this.eventPusher.attemptRepoEvent(evt.id)), + ) + }) + }) + } + + async reverseTakedownRepo(subject: RepoSubject) { + const repoEvts = await this.db.db + .updateTable('repo_push_event') + .where('eventType', 'in', TAKEDOWNS) + .where('subjectDid', '=', subject.did) + .set({ + takedownRef: null, + confirmedAt: null, + attempts: 0, + lastAttempted: null, + }) + .returning('id') + .execute() + + this.db.onCommit(() => { + this.backgroundQueue.add(async () => { + await Promise.all( + repoEvts.map((evt) => this.eventPusher.attemptRepoEvent(evt.id)), + ) + }) + }) + } + + async takedownRecord(subject: RecordSubject, takedownId: number) { + this.db.assertTransaction() + const takedownRef = `BSKY-TAKEDOWN-${takedownId}` + const values = TAKEDOWNS.map((eventType) => ({ + eventType, + subjectDid: subject.did, + subjectUri: subject.uri, + subjectCid: subject.cid, + takedownRef, + })) + const recordEvts = await this.db.db + .insertInto('record_push_event') + .values(values) + .onConflict((oc) => + oc.columns(['subjectUri', 'eventType']).doUpdateSet({ + takedownRef, + confirmedAt: null, + attempts: 0, + lastAttempted: null, + }), + ) + .returning('id') + .execute() + + this.db.onCommit(() => { + this.backgroundQueue.add(async () => { + await Promise.all( + recordEvts.map((evt) => this.eventPusher.attemptRecordEvent(evt.id)), + ) + }) + }) + + const blobCids = subject.blobCids + if (blobCids && blobCids.length > 0) { + const blobValues: Insertable[] = [] + for (const eventType of TAKEDOWNS) { + for (const cid of blobCids) { + blobValues.push({ + eventType, + subjectDid: subject.did, + subjectBlobCid: cid.toString(), + takedownRef, + }) + } + } + const blobEvts = await this.db.db + .insertInto('blob_push_event') + .values(blobValues) + .onConflict((oc) => + oc + .columns(['subjectDid', 'subjectBlobCid', 'eventType']) + .doUpdateSet({ + takedownRef, + confirmedAt: null, + attempts: 0, + lastAttempted: null, + }), + ) + .returning('id') + .execute() + + this.db.onCommit(() => { + this.backgroundQueue.add(async () => { + await Promise.all( + blobEvts.map((evt) => this.eventPusher.attemptBlobEvent(evt.id)), + ) + }) + }) + } + } + + async reverseTakedownRecord(subject: RecordSubject) { + this.db.assertTransaction() + const recordEvts = await this.db.db + .updateTable('record_push_event') + .where('eventType', 'in', TAKEDOWNS) + .where('subjectDid', '=', subject.did) + .where('subjectUri', '=', subject.uri) + .set({ + takedownRef: null, + confirmedAt: null, + attempts: 0, + lastAttempted: null, + }) + .returning('id') + .execute() + this.db.onCommit(() => { + this.backgroundQueue.add(async () => { + await Promise.all( + recordEvts.map((evt) => this.eventPusher.attemptRecordEvent(evt.id)), + ) + }) + }) + + const blobCids = subject.blobCids + if (blobCids && blobCids.length > 0) { + const blobEvts = await this.db.db + .updateTable('blob_push_event') + .where('eventType', 'in', TAKEDOWNS) + .where('subjectDid', '=', subject.did) + .where( + 'subjectBlobCid', + 'in', + blobCids.map((c) => c.toString()), + ) + .set({ + takedownRef: null, + confirmedAt: null, + attempts: 0, + lastAttempted: null, + }) + .returning('id') + .execute() + + this.db.onCommit(() => { + this.backgroundQueue.add(async () => { + await Promise.all( + blobEvts.map((evt) => this.eventPusher.attemptBlobEvent(evt.id)), + ) + }) + }) + } + } + + async report(info: { + reasonType: NonNullable['reportType'] + reason?: string + subject: ModSubject + reportedBy: string + createdAt?: Date + }): Promise { + const { + reasonType, + reason, + reportedBy, + createdAt = new Date(), + subject, + } = info + + const event = await this.logEvent({ + event: { + $type: 'com.atproto.admin.defs#modEventReport', + reportType: reasonType, + comment: reason, + }, + createdBy: reportedBy, + subject, + createdAt, + }) + + return event + } + + async getSubjectStatuses({ + cursor, + limit = 50, + takendown, + appealed, + reviewState, + reviewedAfter, + reviewedBefore, + reportedAfter, + reportedBefore, + includeMuted, + ignoreSubjects, + sortDirection, + lastReviewedBy, + sortField, + subject, + }: { + cursor?: string + limit?: number + takendown?: boolean + appealed?: boolean | null + reviewedBefore?: string + reviewState?: ModerationSubjectStatusRow['reviewState'] + reviewedAfter?: string + reportedAfter?: string + reportedBefore?: string + includeMuted?: boolean + subject?: string + ignoreSubjects?: string[] + sortDirection: 'asc' | 'desc' + lastReviewedBy?: string + sortField: 'lastReviewedAt' | 'lastReportedAt' + }) { + let builder = this.db.db.selectFrom('moderation_subject_status').selectAll() + + if (subject) { + const subjectInfo = getStatusIdentifierFromSubject(subject) + builder = builder + .where('moderation_subject_status.did', '=', subjectInfo.did) + .where((qb) => + subjectInfo.recordPath + ? qb.where('recordPath', '=', subjectInfo.recordPath) + : qb.where('recordPath', '=', ''), + ) + } + + if (ignoreSubjects?.length) { + builder = builder + .where('moderation_subject_status.did', 'not in', ignoreSubjects) + .where('recordPath', 'not in', ignoreSubjects) + } + + if (reviewState) { + builder = builder.where('reviewState', '=', reviewState) + } + + if (lastReviewedBy) { + builder = builder.where('lastReviewedBy', '=', lastReviewedBy) + } + + if (reviewedAfter) { + builder = builder.where('lastReviewedAt', '>', reviewedAfter) + } + + if (reviewedBefore) { + builder = builder.where('lastReviewedAt', '<', reviewedBefore) + } + + if (reportedAfter) { + builder = builder.where('lastReviewedAt', '>', reportedAfter) + } + + if (reportedBefore) { + builder = builder.where('lastReportedAt', '<', reportedBefore) + } + + if (takendown) { + builder = builder.where('takendown', '=', true) + } + + if (appealed !== undefined) { + builder = + appealed === null + ? builder.where('appealed', 'is', null) + : builder.where('appealed', '=', appealed) + } + + if (!includeMuted) { + builder = builder.where((qb) => + qb + .where('muteUntil', '<', new Date().toISOString()) + .orWhere('muteUntil', 'is', null), + ) + } + + const { ref } = this.db.db.dynamic + const keyset = new StatusKeyset( + ref(`moderation_subject_status.${sortField}`), + ref('moderation_subject_status.id'), + ) + const paginatedBuilder = paginate(builder, { + limit, + cursor, + keyset, + direction: sortDirection, + tryIndex: true, + nullsLast: true, + }) + + const results = await paginatedBuilder.execute() + + const infos = await this.views.getAccoutInfosByDid( + results.map((r) => r.did), + ) + const resultsWithHandles = results.map((r) => ({ + ...r, + handle: infos.get(r.did)?.handle ?? INVALID_HANDLE, + })) + + return { + statuses: resultsWithHandles, + cursor: keyset.packFromResult(results), + } + } + + async isSubjectTakendown(subject: ModSubject): Promise { + const builder = this.db.db + .selectFrom('moderation_subject_status') + .where('did', '=', subject.did) + .where('recordPath', '=', subject.recordPath || '') + + const result = await builder.select('takendown').executeTakeFirst() + + return !!result?.takendown + } + + async formatAndCreateLabels( + src: string, + uri: string, + cid: string | null, + labels: { create?: string[]; negate?: string[] }, + ): Promise { + 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(), + })) + const formatted = [...toCreate, ...toNegate] + await this.createLabels(formatted) + return formatted + } + + async createLabels(labels: Label[]) { + if (labels.length < 1) return + const dbVals = labels.map((l) => ({ + ...l, + cid: l.cid ?? '', + neg: !!l.neg, + })) + 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() + } +} + +const TAKEDOWNS = ['pds_takedown' as const, 'appview_takedown' as const] + +export type TakedownSubjects = { + did: string + subjects: (RepoRef | RepoBlobRef | StrongRef)[] +} + +export type ReversalSubject = { + subject: ModSubject + reverseSuspend: boolean + reverseMute: boolean +} diff --git a/packages/bsky/src/services/moderation/status.ts b/packages/ozone/src/mod-service/status.ts similarity index 94% rename from packages/bsky/src/services/moderation/status.ts rename to packages/ozone/src/mod-service/status.ts index 151f6137a05..598ebe20712 100644 --- a/packages/bsky/src/services/moderation/status.ts +++ b/packages/ozone/src/mod-service/status.ts @@ -1,18 +1,17 @@ // This may require better organization but for now, just dumping functions here containing DB queries for moderation status import { AtUri } from '@atproto/syntax' -import { PrimaryDatabase } from '../../db' -import { ModerationSubjectStatus } from '../../db/tables/moderation' +import { Database } from '../db' +import { ModerationSubjectStatus } from '../db/schema/moderation_subject_status' import { REVIEWOPEN, REVIEWCLOSED, REVIEWESCALATED, -} from '../../lexicon/types/com/atproto/admin/defs' +} from '../lexicon/types/com/atproto/admin/defs' import { ModerationEventRow, ModerationSubjectStatusRow } from './types' import { HOUR } from '@atproto/common' -import { CID } from 'multiformats/cid' import { sql } from 'kysely' -import { REASONAPPEAL } from '../../lexicon/types/com/atproto/moderation/defs' +import { REASONAPPEAL } from '../lexicon/types/com/atproto/moderation/defs' const getSubjectStatusForModerationEvent = ({ action, @@ -96,9 +95,9 @@ const getSubjectStatusForModerationEvent = ({ // If there's no existing status, it will create one // If the action event does not affect the status, it will do nothing export const adjustModerationSubjectStatus = async ( - db: PrimaryDatabase, + db: Database, moderationEvent: ModerationEventRow, - blobCids?: CID[], + blobCids?: string[], ) => { const { action, @@ -193,7 +192,7 @@ export const adjustModerationSubjectStatus = async ( if (blobCids?.length) { const newBlobCids = sql`${JSON.stringify( - blobCids.map((c) => c.toString()), + blobCids, )}` as unknown as ModerationSubjectStatusRow['blobCids'] newStatus.blobCids = newBlobCids subjectStatus.blobCids = newBlobCids @@ -224,7 +223,7 @@ type ModerationSubjectStatusFilter = | Pick | Pick export const getModerationSubjectStatus = async ( - db: PrimaryDatabase, + db: Database, filters: ModerationSubjectStatusFilter, ) => { let builder = db.db diff --git a/packages/ozone/src/mod-service/subject.ts b/packages/ozone/src/mod-service/subject.ts new file mode 100644 index 00000000000..2ea41eed42e --- /dev/null +++ b/packages/ozone/src/mod-service/subject.ts @@ -0,0 +1,136 @@ +import { AtUri } from '@atproto/syntax' +import { InputSchema as ReportInput } from '../lexicon/types/com/atproto/moderation/createReport' +import { InputSchema as ActionInput } from '../lexicon/types/com/atproto/admin/emitModerationEvent' +import { InvalidRequestError } from '@atproto/xrpc-server' +import { ModerationEventRow, ModerationSubjectStatusRow } from './types' +import { RepoRef } from '../lexicon/types/com/atproto/admin/defs' +import { Main as StrongRef } from '../lexicon/types/com/atproto/repo/strongRef' + +type SubjectInput = ReportInput['subject'] | ActionInput['subject'] + +export const subjectFromInput = ( + subject: SubjectInput, + blobs?: string[], +): ModSubject => { + if ( + subject.$type === 'com.atproto.admin.defs#repoRef' && + typeof subject.did === 'string' + ) { + if (blobs && blobs.length > 0) { + throw new InvalidRequestError('Blobs do not apply to repo subjects') + } + return new RepoSubject(subject.did) + } + if ( + subject.$type === 'com.atproto.repo.strongRef' && + typeof subject.uri === 'string' && + typeof subject.cid === 'string' + ) { + return new RecordSubject(subject.uri, subject.cid, blobs) + } + throw new InvalidRequestError('Invalid subject') +} + +export const subjectFromEventRow = (row: ModerationEventRow): ModSubject => { + if ( + row.subjectType === 'com.atproto.repo.strongRef' && + row.subjectUri && + row.subjectCid + ) { + return new RecordSubject(row.subjectUri, row.subjectCid) + } else { + return new RepoSubject(row.subjectDid) + } +} + +export const subjectFromStatusRow = ( + row: ModerationSubjectStatusRow, +): ModSubject => { + if (row.recordPath && row.recordCid) { + // Not too intuitive but the recordpath is basically / + // which is what the last 2 params of .make() arguments are + const uri = AtUri.make(row.did, ...row.recordPath.split('/')).toString() + return new RecordSubject(uri.toString(), row.recordCid) + } else { + return new RepoSubject(row.did) + } +} + +type SubjectInfo = { + subjectType: 'com.atproto.admin.defs#repoRef' | 'com.atproto.repo.strongRef' + subjectDid: string + subjectUri: string | null + subjectCid: string | null +} + +export interface ModSubject { + did: string + recordPath: string | undefined + blobCids?: string[] + isRepo(): this is RepoSubject + isRecord(): this is RecordSubject + info(): SubjectInfo + lex(): RepoRef | StrongRef +} + +export class RepoSubject implements ModSubject { + blobCids = undefined + recordPath = undefined + constructor(public did: string) {} + isRepo() { + return true + } + isRecord() { + return false + } + info() { + return { + subjectType: 'com.atproto.admin.defs#repoRef' as const, + subjectDid: this.did, + subjectUri: null, + subjectCid: null, + } + } + lex(): RepoRef { + return { + $type: 'com.atproto.admin.defs#repoRef', + did: this.did, + } + } +} + +export class RecordSubject implements ModSubject { + parsedUri: AtUri + did: string + recordPath: string + constructor( + public uri: string, + public cid: string, + public blobCids?: string[], + ) { + this.parsedUri = new AtUri(uri) + this.did = this.parsedUri.hostname + this.recordPath = `${this.parsedUri.collection}/${this.parsedUri.rkey}` + } + isRepo() { + return false + } + isRecord() { + return true + } + info() { + return { + subjectType: 'com.atproto.repo.strongRef' as const, + subjectDid: this.did, + subjectUri: this.uri, + subjectCid: this.cid, + } + } + lex(): StrongRef { + return { + $type: 'com.atproto.repo.strongRef', + uri: this.uri, + cid: this.cid, + } + } +} diff --git a/packages/bsky/src/services/moderation/types.ts b/packages/ozone/src/mod-service/types.ts similarity index 64% rename from packages/bsky/src/services/moderation/types.ts rename to packages/ozone/src/mod-service/types.ts index 77a8baf71ff..94fc58a8d33 100644 --- a/packages/bsky/src/services/moderation/types.ts +++ b/packages/ozone/src/mod-service/types.ts @@ -1,25 +1,8 @@ import { Selectable } from 'kysely' -import { - ModerationEvent, - ModerationSubjectStatus, -} from '../../db/tables/moderation' -import { AtUri } from '@atproto/syntax' -import { CID } from 'multiformats/cid' +import { ModerationEvent } from '../db/schema/moderation_event' +import { ModerationSubjectStatus } from '../db/schema/moderation_subject_status' import { ComAtprotoAdminDefs } from '@atproto/api' - -export type SubjectInfo = - | { - subjectType: 'com.atproto.admin.defs#repoRef' - subjectDid: string - subjectUri: null - subjectCid: null - } - | { - subjectType: 'com.atproto.repo.strongRef' - subjectDid: string - subjectUri: string - subjectCid: string - } +import { ModSubject } from './subject' export type ModerationEventRow = Selectable export type ReversibleModerationEvent = Pick< @@ -27,7 +10,7 @@ export type ReversibleModerationEvent = Pick< 'createdBy' | 'comment' | 'action' > & { createdAt?: Date - subject: { did: string } | { uri: AtUri; cid: CID } + subject: ModSubject } export type ModerationEventRowWithHandle = ModerationEventRow & { diff --git a/packages/ozone/src/mod-service/views.ts b/packages/ozone/src/mod-service/views.ts new file mode 100644 index 00000000000..1ae32126b8f --- /dev/null +++ b/packages/ozone/src/mod-service/views.ts @@ -0,0 +1,531 @@ +import { sql } from 'kysely' +import { AtUri, INVALID_HANDLE, normalizeDatetimeAlways } from '@atproto/syntax' +import AtpAgent from '@atproto/api' +import { dedupeStrs } from '@atproto/common' +import { BlobRef } from '@atproto/lexicon' +import { Database } from '../db' +import { + ModEventView, + RepoView, + RepoViewDetail, + RecordView, + RecordViewDetail, + ReportViewDetail, + BlobView, + SubjectStatusView, + ModEventViewDetail, + AccountView, +} from '../lexicon/types/com/atproto/admin/defs' +import { OutputSchema as ReportOutput } from '../lexicon/types/com/atproto/moderation/createReport' +import { Label, isSelfLabels } from '../lexicon/types/com/atproto/label/defs' +import { + ModerationEventRowWithHandle, + ModerationSubjectStatusRowWithHandle, +} from './types' +import { REASONOTHER } from '../lexicon/types/com/atproto/moderation/defs' +import { subjectFromEventRow, subjectFromStatusRow } from './subject' + +export type AppviewAuth = () => Promise< + | { + headers: { + authorization: string + } + } + | undefined +> + +export class ModerationViews { + constructor( + private db: Database, + private appviewAgent: AtpAgent, + private appviewAuth: AppviewAuth, + ) {} + + async getAccoutInfosByDid(dids: string[]): Promise> { + if (dids.length === 0) return new Map() + const auth = await this.appviewAuth() + if (!auth) return new Map() + const res = await this.appviewAgent.api.com.atproto.admin.getAccountInfos( + { + dids: dedupeStrs(dids), + }, + auth, + ) + return res.data.infos.reduce((acc, cur) => { + return acc.set(cur.did, cur) + }, new Map()) + } + + async repos(dids: string[]): Promise> { + if (dids.length === 0) return new Map() + const [infos, subjectStatuses] = await Promise.all([ + this.getAccoutInfosByDid(dids), + this.getSubjectStatus(dids), + ]) + + return dids.reduce((acc, did) => { + const info = infos.get(did) + if (!info) return acc + const status = subjectStatuses.get(did) + return acc.set(did, { + // No email or invite info on appview + did, + handle: info.handle, + relatedRecords: info.relatedRecords ?? [], + indexedAt: info.indexedAt, + moderation: { + subjectStatus: status ? this.formatSubjectStatus(status) : undefined, + }, + }) + }, new Map()) + } + + formatEvent(event: ModerationEventRowWithHandle): ModEventView { + const eventView: ModEventView = { + id: event.id, + event: { + $type: event.action, + comment: event.comment ?? undefined, + }, + subject: subjectFromEventRow(event).lex(), + subjectBlobCids: [], + createdBy: event.createdBy, + createdAt: event.createdAt, + subjectHandle: event.subjectHandle ?? undefined, + creatorHandle: event.creatorHandle ?? undefined, + } + + if ( + [ + 'com.atproto.admin.defs#modEventTakedown', + 'com.atproto.admin.defs#modEventMute', + ].includes(event.action) + ) { + eventView.event = { + ...eventView.event, + durationInHours: event.durationInHours ?? undefined, + } + } + + if (event.action === 'com.atproto.admin.defs#modEventLabel') { + eventView.event = { + ...eventView.event, + createLabelVals: event.createLabelVals?.length + ? event.createLabelVals.split(' ') + : [], + negateLabelVals: event.negateLabelVals?.length + ? event.negateLabelVals.split(' ') + : [], + } + } + + // This is for legacy data only, for new events, these types of events won't have labels attached + if ( + [ + 'com.atproto.admin.defs#modEventAcknowledge', + 'com.atproto.admin.defs#modEventTakedown', + 'com.atproto.admin.defs#modEventEscalate', + ].includes(event.action) + ) { + if (event.createLabelVals?.length) { + eventView.event = { + ...eventView.event, + createLabelVals: event.createLabelVals.split(' '), + } + } + + if (event.negateLabelVals?.length) { + eventView.event = { + ...eventView.event, + negateLabelVals: event.negateLabelVals.split(' '), + } + } + } + + if (event.action === 'com.atproto.admin.defs#modEventReport') { + eventView.event = { + ...eventView.event, + reportType: event.meta?.reportType ?? undefined, + } + } + + if (event.action === 'com.atproto.admin.defs#modEventEmail') { + eventView.event = { + ...eventView.event, + subjectLine: event.meta?.subjectLine ?? '', + } + } + + if ( + event.action === 'com.atproto.admin.defs#modEventComment' && + event.meta?.sticky + ) { + eventView.event.sticky = true + } + + return eventView + } + + async eventDetail( + result: ModerationEventRowWithHandle, + ): Promise { + const subjectId = + result.subjectType === 'com.atproto.admin.defs#repoRef' + ? result.subjectDid + : result.subjectUri + if (!subjectId) { + throw new Error(`Bad subject: ${result.id}`) + } + const subject = await this.subject(subjectId) + const eventView = this.formatEvent(result) + const allBlobs = findBlobRefs(subject.value) + const subjectBlobs = await this.blob( + allBlobs.filter((blob) => + eventView.subjectBlobCids.includes(blob.ref.toString()), + ), + ) + return { + ...eventView, + subject, + subjectBlobs, + } + } + + async repoDetail(did: string): Promise { + const [repos, labels] = await Promise.all([ + this.repos([did]), + this.labels(did), + ]) + const repo = repos.get(did) + if (!repo) return + + return { + ...repo, + moderation: { + ...repo.moderation, + }, + labels, + } + } + + async fetchRecords( + subjects: RecordSubject[], + ): Promise> { + const auth = await this.appviewAuth() + if (!auth) return new Map() + const fetched = await Promise.all( + subjects.map(async (subject) => { + const uri = new AtUri(subject.uri) + try { + return await this.appviewAgent.api.com.atproto.repo.getRecord( + { + repo: uri.hostname, + collection: uri.collection, + rkey: uri.rkey, + cid: subject.cid, + }, + auth, + ) + } catch { + return null + } + }), + ) + return fetched.reduce((acc, cur) => { + if (!cur) return acc + const data = cur.data + const indexedAt = new Date().toISOString() + return acc.set(data.uri, { ...data, cid: data.cid ?? '', indexedAt }) + }, new Map()) + } + + async records(subjects: RecordSubject[]): Promise> { + const uris = subjects.map((record) => new AtUri(record.uri)) + const dids = uris.map((u) => u.hostname) + + const [repos, subjectStatuses, records] = await Promise.all([ + this.repos(dids), + this.getSubjectStatus(subjects.map((s) => s.uri)), + this.fetchRecords(subjects), + ]) + + return uris.reduce((acc, uri) => { + const repo = repos.get(uri.hostname) + if (!repo) return acc + const record = records.get(uri.toString()) + if (!record) return acc + const subjectStatus = subjectStatuses.get(uri.toString()) + return acc.set(uri.toString(), { + uri: uri.toString(), + cid: record.cid, + value: record.value, + blobCids: findBlobRefs(record.value).map((blob) => blob.ref.toString()), + indexedAt: record.indexedAt, + repo, + moderation: { + subjectStatus: subjectStatus + ? this.formatSubjectStatus(subjectStatus) + : undefined, + }, + }) + }, new Map()) + } + + async recordDetail( + subject: RecordSubject, + ): Promise { + const [records, subjectStatusesResult] = await Promise.all([ + this.records([subject]), + this.getSubjectStatus([subject.uri]), + ]) + const record = records.get(subject.uri) + if (!record) return undefined + + const status = subjectStatusesResult.get(subject.uri) + + const [blobs, labels, subjectStatus] = await Promise.all([ + this.blob(findBlobRefs(record.value)), + this.labels(record.uri), + status ? this.formatSubjectStatus(status) : Promise.resolve(undefined), + ]) + const selfLabels = getSelfLabels({ + uri: record.uri, + cid: record.cid, + record: record.value, + }) + return { + ...record, + blobs, + moderation: { + ...record.moderation, + subjectStatus, + }, + labels: [...labels, ...selfLabels], + } + } + + formatReport(report: ModerationEventRowWithHandle): ReportOutput { + return { + id: report.id, + createdAt: report.createdAt, + // Ideally, we would never have a report entry that does not have a reasonType but at the schema level + // we are not guarantying that so in whatever case, if we end up with such entries, default to 'other' + reasonType: report.meta?.reportType + ? (report.meta?.reportType as string) + : REASONOTHER, + reason: report.comment ?? undefined, + reportedBy: report.createdBy, + subject: subjectFromEventRow(report).lex(), + } + } + // Partial view for subjects + + async subject(subject: string): Promise { + if (subject.startsWith('did:')) { + const repos = await this.repos([subject]) + const repo = repos.get(subject) + if (repo) { + return { + $type: 'com.atproto.admin.defs#repoView', + ...repo, + } + } else { + return { + $type: 'com.atproto.admin.defs#repoViewNotFound', + did: subject, + } + } + } else { + const records = await this.records([{ uri: subject }]) + const record = records.get(subject) + if (record) { + return { + $type: 'com.atproto.admin.defs#recordView', + ...record, + } + } else { + return { + $type: 'com.atproto.admin.defs#recordViewNotFound', + uri: subject, + } + } + } + } + + // Partial view for blobs + + async blob(blobs: BlobRef[]): Promise { + if (!blobs.length) return [] + const { ref } = this.db.db.dynamic + const modStatusResults = await this.db.db + .selectFrom('moderation_subject_status') + .where( + sql`${ref( + 'moderation_subject_status.blobCids', + )} @> ${JSON.stringify(blobs.map((blob) => blob.ref.toString()))}`, + ) + .selectAll() + .executeTakeFirst() + const statusByCid = (modStatusResults?.blobCids || [])?.reduce( + (acc, cur) => Object.assign(acc, { [cur]: modStatusResults }), + {}, + ) + // Intentionally missing details field, since we don't have any on appview. + // We also don't know when the blob was created, so we use a canned creation time. + const unknownTime = new Date(0).toISOString() + return blobs.map((blob) => { + const cid = blob.ref.toString() + const subjectStatus = statusByCid[cid] + ? this.formatSubjectStatus(statusByCid[cid]) + : undefined + return { + cid, + mimeType: blob.mimeType, + size: blob.size, + createdAt: unknownTime, + moderation: { + subjectStatus, + }, + } + }) + } + + 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', '=', false)) + .selectAll() + .execute() + return res.map((l) => ({ + ...l, + cid: l.cid === '' ? undefined : l.cid, + neg: l.neg, + })) + } + + async getSubjectStatus( + subjects: string[], + ): Promise> { + const parsedSubjects = subjects.map((subject) => parseSubjectId(subject)) + const filterForSubject = (did: string, recordPath?: string) => { + return (clause: any) => { + clause = clause + .where('moderation_subject_status.did', '=', did) + .where('moderation_subject_status.recordPath', '=', recordPath || '') + return clause + } + // TODO: Fix the typing here? + } + + const builder = this.db.db + .selectFrom('moderation_subject_status') + .where((clause) => { + parsedSubjects.forEach((subject, i) => { + const applySubjectFilter = filterForSubject( + subject.did, + subject.recordPath, + ) + if (i === 0) { + clause = clause.where(applySubjectFilter) + } else { + clause = clause.orWhere(applySubjectFilter) + } + }) + + return clause + }) + .selectAll() + + const [statusRes, accountsByDid] = await Promise.all([ + builder.execute(), + this.getAccoutInfosByDid(parsedSubjects.map((s) => s.did)), + ]) + + return statusRes.reduce((acc, cur) => { + const subject = cur.recordPath + ? formatSubjectId(cur.did, cur.recordPath) + : cur.did + const handle = accountsByDid.get(cur.did)?.handle + return acc.set(subject, { + ...cur, + handle: handle ?? INVALID_HANDLE, + }) + }, new Map()) + } + + formatSubjectStatus( + status: ModerationSubjectStatusRowWithHandle, + ): SubjectStatusView { + return { + id: status.id, + reviewState: status.reviewState, + createdAt: status.createdAt, + updatedAt: status.updatedAt, + comment: status.comment ?? undefined, + lastReviewedBy: status.lastReviewedBy ?? undefined, + lastReviewedAt: status.lastReviewedAt ?? undefined, + lastReportedAt: status.lastReportedAt ?? undefined, + lastAppealedAt: status.lastAppealedAt ?? undefined, + muteUntil: status.muteUntil ?? undefined, + suspendUntil: status.suspendUntil ?? undefined, + takendown: status.takendown ?? undefined, + appealed: status.appealed ?? undefined, + subjectRepoHandle: status.handle ?? undefined, + subjectBlobCids: status.blobCids || [], + subject: subjectFromStatusRow(status).lex(), + } + } +} + +type RecordSubject = { uri: string; cid?: string } + +type SubjectView = ModEventViewDetail['subject'] & ReportViewDetail['subject'] + +type RecordInfo = { + uri: string + cid: string + value: Record + indexedAt: string +} + +function parseSubjectId(subject: string) { + if (subject.startsWith('did:')) { + return { did: subject } + } + const uri = new AtUri(subject) + return { did: uri.hostname, recordPath: `${uri.collection}/${uri.rkey}` } +} + +function formatSubjectId(did: string, recordPath?: string) { + return recordPath ? `at://${did}/${recordPath}` : did +} + +function findBlobRefs(value: unknown, refs: BlobRef[] = []) { + if (value instanceof BlobRef) { + refs.push(value) + } else if (Array.isArray(value)) { + value.forEach((val) => findBlobRefs(val, refs)) + } else if (value && typeof value === 'object') { + Object.values(value).forEach((val) => findBlobRefs(val, refs)) + } + return refs +} + +export function getSelfLabels(details: { + uri: string | null + cid: string | null + record: Record | null +}): Label[] { + const { uri, cid, record } = details + if (!uri || !cid || !record) return [] + if (!isSelfLabels(record.labels)) return [] + const src = new AtUri(uri).host // record creator + const cts = + typeof record.createdAt === 'string' + ? normalizeDatetimeAlways(record.createdAt) + : new Date(0).toISOString() + return record.labels.values.map(({ val }) => { + return { src, uri, cid, val, cts, neg: false } + }) +} diff --git a/packages/ozone/src/util.ts b/packages/ozone/src/util.ts new file mode 100644 index 00000000000..ab96998642a --- /dev/null +++ b/packages/ozone/src/util.ts @@ -0,0 +1,26 @@ +import { AxiosError } from 'axios' +import { XRPCError, ResponseType } from '@atproto/xrpc' +import { RetryOptions, retry } from '@atproto/common' + +export async function retryHttp( + fn: () => Promise, + opts: RetryOptions = {}, +): Promise { + return retry(fn, { retryable: retryableHttp, ...opts }) +} + +export function retryableHttp(err: unknown) { + if (err instanceof XRPCError) { + if (err.status === ResponseType.Unknown) return true + return retryableHttpStatusCodes.has(err.status) + } + if (err instanceof AxiosError) { + if (!err.response) return true + return retryableHttpStatusCodes.has(err.response.status) + } + return false +} + +const retryableHttpStatusCodes = new Set([ + 408, 425, 429, 500, 502, 503, 504, 522, 524, +]) diff --git a/packages/ozone/test.env b/packages/ozone/test.env new file mode 100644 index 00000000000..b854b5ae3c7 --- /dev/null +++ b/packages/ozone/test.env @@ -0,0 +1,2 @@ +LOG_ENABLED=true +LOG_DESTINATION=test.log diff --git a/packages/bsky/tests/admin/__snapshots__/get-record.test.ts.snap b/packages/ozone/tests/__snapshots__/get-record.test.ts.snap similarity index 100% rename from packages/bsky/tests/admin/__snapshots__/get-record.test.ts.snap rename to packages/ozone/tests/__snapshots__/get-record.test.ts.snap diff --git a/packages/bsky/tests/admin/__snapshots__/get-repo.test.ts.snap b/packages/ozone/tests/__snapshots__/get-repo.test.ts.snap similarity index 100% rename from packages/bsky/tests/admin/__snapshots__/get-repo.test.ts.snap rename to packages/ozone/tests/__snapshots__/get-repo.test.ts.snap diff --git a/packages/bsky/tests/admin/__snapshots__/moderation-events.test.ts.snap b/packages/ozone/tests/__snapshots__/moderation-events.test.ts.snap similarity index 54% rename from packages/bsky/tests/admin/__snapshots__/moderation-events.test.ts.snap rename to packages/ozone/tests/__snapshots__/moderation-events.test.ts.snap index 8fa16b311f2..0ebda08b84e 100644 --- a/packages/bsky/tests/admin/__snapshots__/moderation-events.test.ts.snap +++ b/packages/ozone/tests/__snapshots__/moderation-events.test.ts.snap @@ -5,60 +5,82 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "createdBy": "user(2)", "event": Object { - "$type": "com.atproto.admin.defs#modEventReport", - "comment": "X", - "reportType": "com.atproto.moderation.defs#reasonMisleading", + "$type": "com.atproto.admin.defs#modEventLabel", + "comment": "[AutoModerator]: Applying labels", + "createLabelVals": Array [ + "test-label", + ], + "negateLabelVals": Array [], }, "id": 1, "subject": Object { - "$type": "com.atproto.admin.defs#repoView", - "did": "user(0)", - "handle": "alice.test", + "$type": "com.atproto.admin.defs#recordView", + "blobCids": Array [], + "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", - "moderation": Object { - "subjectStatus": Object { - "createdAt": "1970-01-01T00:00:00.000Z", - "id": 1, - "lastReportedAt": "1970-01-01T00:00:00.000Z", - "lastReviewedAt": "1970-01-01T00:00:00.000Z", - "lastReviewedBy": "user(1)", - "reviewState": "com.atproto.admin.defs#reviewEscalated", - "subject": Object { - "$type": "com.atproto.admin.defs#repoRef", - "did": "user(0)", + "moderation": Object {}, + "repo": Object { + "did": "user(0)", + "handle": "alice.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "moderation": Object { + "subjectStatus": Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "id": 1, + "lastReportedAt": "1970-01-01T00:00:00.000Z", + "lastReviewedAt": "1970-01-01T00:00:00.000Z", + "lastReviewedBy": "user(1)", + "reviewState": "com.atproto.admin.defs#reviewEscalated", + "subject": Object { + "$type": "com.atproto.admin.defs#repoRef", + "did": "user(0)", + }, + "subjectBlobCids": Array [], + "subjectRepoHandle": "alice.test", + "takendown": false, + "updatedAt": "1970-01-01T00:00:00.000Z", }, - "subjectBlobCids": Array [], - "subjectRepoHandle": "alice.test", - "takendown": false, - "updatedAt": "1970-01-01T00:00:00.000Z", }, - }, - "relatedRecords": Array [ - Object { - "$type": "app.bsky.actor.profile", - "avatar": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(0)", + "relatedRecords": Array [ + Object { + "$type": "app.bsky.actor.profile", + "avatar": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(2)", + }, + "size": 3976, + }, + "description": "its me!", + "displayName": "ali", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label-a", + }, + Object { + "val": "self-label-b", + }, + ], }, - "size": 3976, }, - "description": "its me!", - "displayName": "ali", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ - Object { - "val": "self-label-a", - }, - Object { - "val": "self-label-b", - }, - ], + ], + }, + "uri": "record(0)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(1)", + "uri": "record(1)", }, }, - ], + "text": "yoohoo label_me", + }, }, "subjectBlobCids": Array [], "subjectBlobs": Array [], @@ -76,7 +98,7 @@ Array [ "comment": "X", "reportType": "com.atproto.moderation.defs#reasonSpam", }, - "id": 7, + "id": 9, "subject": Object { "$type": "com.atproto.admin.defs#repoRef", "did": "user(0)", @@ -93,7 +115,7 @@ Array [ "comment": "X", "reportType": "com.atproto.moderation.defs#reasonSpam", }, - "id": 3, + "id": 5, "subject": Object { "$type": "com.atproto.admin.defs#repoRef", "did": "user(0)", @@ -115,7 +137,7 @@ Array [ "comment": "X", "reportType": "com.atproto.moderation.defs#reasonSpam", }, - "id": 6, + "id": 8, "subject": Object { "$type": "com.atproto.repo.strongRef", "cid": "cids(0)", @@ -133,7 +155,7 @@ Array [ "comment": "X", "reportType": "com.atproto.moderation.defs#reasonSpam", }, - "id": 2, + "id": 4, "subject": Object { "$type": "com.atproto.repo.strongRef", "cid": "cids(0)", diff --git a/packages/bsky/tests/admin/__snapshots__/moderation-statuses.test.ts.snap b/packages/ozone/tests/__snapshots__/moderation-statuses.test.ts.snap similarity index 100% rename from packages/bsky/tests/admin/__snapshots__/moderation-statuses.test.ts.snap rename to packages/ozone/tests/__snapshots__/moderation-statuses.test.ts.snap diff --git a/packages/bsky/tests/admin/__snapshots__/moderation.test.ts.snap b/packages/ozone/tests/__snapshots__/moderation.test.ts.snap similarity index 97% rename from packages/bsky/tests/admin/__snapshots__/moderation.test.ts.snap rename to packages/ozone/tests/__snapshots__/moderation.test.ts.snap index 33a973e714f..d2f5731d42d 100644 --- a/packages/bsky/tests/admin/__snapshots__/moderation.test.ts.snap +++ b/packages/ozone/tests/__snapshots__/moderation.test.ts.snap @@ -4,7 +4,7 @@ exports[`moderation reporting creates reports of a record. 1`] = ` Array [ Object { "createdAt": "1970-01-01T00:00:00.000Z", - "id": 4, + "id": 6, "reasonType": "com.atproto.moderation.defs#reasonSpam", "reportedBy": "user(0)", "subject": Object { @@ -15,7 +15,7 @@ Array [ }, Object { "createdAt": "1970-01-01T00:00:00.000Z", - "id": 5, + "id": 7, "reason": "defamation", "reasonType": "com.atproto.moderation.defs#reasonOther", "reportedBy": "user(1)", @@ -32,7 +32,7 @@ exports[`moderation reporting creates reports of a repo. 1`] = ` Array [ Object { "createdAt": "1970-01-01T00:00:00.000Z", - "id": 1, + "id": 3, "reasonType": "com.atproto.moderation.defs#reasonSpam", "reportedBy": "user(0)", "subject": Object { @@ -42,7 +42,7 @@ Array [ }, Object { "createdAt": "1970-01-01T00:00:00.000Z", - "id": 2, + "id": 4, "reason": "impersonation", "reasonType": "com.atproto.moderation.defs#reasonOther", "reportedBy": "user(2)", diff --git a/packages/ozone/tests/_util.ts b/packages/ozone/tests/_util.ts new file mode 100644 index 00000000000..8d39a0f9c2c --- /dev/null +++ b/packages/ozone/tests/_util.ts @@ -0,0 +1,192 @@ +import { AtUri } from '@atproto/syntax' +import { lexToJson } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' +import { + FeedViewPost, + PostView, + isPostView, + isThreadViewPost, +} from '../src/lexicon/types/app/bsky/feed/defs' +import { isViewRecord } from '../src/lexicon/types/app/bsky/embed/record' + +// Swap out identifiers and dates with stable +// values for the purpose of snapshot testing +export const forSnapshot = (obj: unknown) => { + const records = { [kTake]: 'record' } + const collections = { [kTake]: 'collection' } + const users = { [kTake]: 'user' } + const cids = { [kTake]: 'cids' } + const unknown = { [kTake]: 'unknown' } + const toWalk = lexToJson(obj as any) // remove any blobrefs/cids + return mapLeafValues(toWalk, (item) => { + const asCid = CID.asCID(item) + if (asCid !== null) { + return take(cids, asCid.toString()) + } + if (typeof item !== 'string') { + return item + } + const str = item.startsWith('did:plc:') ? `at://${item}` : item + if (str.startsWith('at://')) { + const uri = new AtUri(str) + if (uri.rkey) { + return take(records, str) + } + if (uri.collection) { + return take(collections, str) + } + if (uri.hostname) { + return take(users, str) + } + return take(unknown, str) + } + if (str.match(/^\d{4}-\d{2}-\d{2}T/)) { + if (str.match(/\d{6}Z$/)) { + return constantDate.replace('Z', '000Z') // e.g. microseconds in record createdAt + } else if (str.endsWith('+00:00')) { + return constantDate.replace('Z', '+00:00') // e.g. timezone in record createdAt + } else { + return constantDate + } + } + if (str.match(/^\d+::bafy/)) { + return constantKeysetCursor + } + if (str.match(/\/img\/[^/]+\/.+\/did:plc:[^/]+\/[^/]+@[\w]+$/)) { + // Match image urls + const match = str.match( + /\/img\/[^/]+\/.+\/(did:plc:[^/]+)\/([^/]+)@[\w]+$/, + ) + if (!match) return str + const [, did, cid] = match + return str.replace(did, take(users, did)).replace(cid, take(cids, cid)) + } + let isCid: boolean + try { + CID.parse(str) + isCid = true + } catch (_err) { + isCid = false + } + if (isCid) { + return take(cids, str) + } + return item + }) +} + +// Feed testing utils + +export const getOriginator = (item: FeedViewPost) => { + if (!item.reason) { + return item.post.author.did + } else { + return (item.reason.by as { [did: string]: string }).did + } +} + +// Useful for remapping ids in snapshot testing, to make snapshots deterministic. +// E.g. you may use this to map this: +// [{ uri: 'did://rad'}, { uri: 'did://bad' }, { uri: 'did://rad'}] +// to this: +// [{ uri: '0'}, { uri: '1' }, { uri: '0'}] +const kTake = Symbol('take') +export function take(obj, value: string): string +export function take(obj, value: string | undefined): string | undefined +export function take( + obj: { [s: string]: number; [kTake]?: string }, + value: string | undefined, +): string | undefined { + if (value === undefined) { + return + } + if (!(value in obj)) { + obj[value] = Object.keys(obj).length + } + const kind = obj[kTake] + return typeof kind === 'string' + ? `${kind}(${obj[value]})` + : String(obj[value]) +} + +export const constantDate = new Date(0).toISOString() +export const constantKeysetCursor = '0000000000000::bafycid' + +const mapLeafValues = (obj: unknown, fn: (val: unknown) => unknown) => { + if (Array.isArray(obj)) { + return obj.map((item) => mapLeafValues(item, fn)) + } + if (obj && typeof obj === 'object') { + return Object.entries(obj).reduce( + (collect, [name, value]) => + Object.assign(collect, { [name]: mapLeafValues(value, fn) }), + {}, + ) + } + return fn(obj) +} + +export const paginateAll = async ( + fn: (cursor?: string) => Promise, + limit = Infinity, +): Promise => { + const results: T[] = [] + let cursor + do { + const res = await fn(cursor) + results.push(res) + cursor = res.cursor + } while (cursor && results.length < limit) + return results +} + +// @NOTE mutates +export const stripViewer = }>( + val: T, +): T => { + delete val.viewer + return val +} + +// @NOTE mutates +export const stripViewerFromPost = (postUnknown: unknown): PostView => { + if (postUnknown?.['$type'] && !isPostView(postUnknown)) { + throw new Error('Expected post view') + } + const post = postUnknown as PostView + post.author = stripViewer(post.author) + const recordEmbed = + post.embed && isViewRecord(post.embed.record) + ? post.embed.record // Record from record embed + : post.embed?.['record'] && isViewRecord(post.embed['record']['record']) + ? post.embed['record']['record'] // Record from record-with-media embed + : undefined + if (recordEmbed) { + recordEmbed.author = stripViewer(recordEmbed.author) + recordEmbed.embeds?.forEach((deepEmbed) => { + const deepRecordEmbed = isViewRecord(deepEmbed.record) + ? deepEmbed.record // Record from record embed + : deepEmbed['record'] && isViewRecord(deepEmbed['record']['record']) + ? deepEmbed['record']['record'] // Record from record-with-media embed + : undefined + if (deepRecordEmbed) { + deepRecordEmbed.author = stripViewer(deepRecordEmbed.author) + } + }) + } + return stripViewer(post) +} + +// @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) + } + if (thread.replies) { + thread.replies = thread.replies.map(stripViewerFromThread) + } + return thread +} diff --git a/packages/ozone/tests/db.test.ts b/packages/ozone/tests/db.test.ts new file mode 100644 index 00000000000..30ca5fde5ec --- /dev/null +++ b/packages/ozone/tests/db.test.ts @@ -0,0 +1,184 @@ +import { sql } from 'kysely' +import { wait } from '@atproto/common' +import { TestNetwork } from '@atproto/dev-env' +import { Database } from '../src' + +describe('db', () => { + let network: TestNetwork + let db: Database + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'ozone_db', + }) + db = network.ozone.ctx.db + }) + + afterAll(async () => { + await network.close() + }) + + it('handles client errors without crashing.', async () => { + const tryKillConnection = db.transaction(async (dbTxn) => { + const result = await sql`select pg_backend_pid() as pid;`.execute( + dbTxn.db, + ) + const pid = result.rows[0]?.['pid'] as number + await sql`select pg_terminate_backend(${pid});`.execute(db.db) + await sql`select 1;`.execute(dbTxn.db) + }) + // This should throw, but no unhandled error + await expect(tryKillConnection).rejects.toThrow() + }) + + it('handles pool errors without crashing.', async () => { + const conn1 = await db.pool.connect() + const conn2 = await db.pool.connect() + const result = await conn1.query('select pg_backend_pid() as pid;') + const conn1pid: number = result.rows[0].pid + conn1.release() + await wait(100) // let release apply, conn is now idle on pool. + await conn2.query(`select pg_terminate_backend(${conn1pid});`) + conn2.release() + }) + + describe('transaction()', () => { + it('commits changes', async () => { + const result = await db.transaction(async (dbTxn) => { + return await dbTxn.db + .insertInto('repo_push_event') + .values({ + eventType: 'takedown', + subjectDid: 'x', + }) + .returning('subjectDid') + .executeTakeFirst() + }) + + if (!result) { + return expect(result).toBeTruthy() + } + + expect(result.subjectDid).toEqual('x') + + const row = await db.db + .selectFrom('repo_push_event') + .selectAll() + .where('subjectDid', '=', 'x') + .executeTakeFirst() + + expect(row).toMatchObject({ + eventType: 'takedown', + subjectDid: 'x', + }) + }) + + it('rolls-back changes on failure', async () => { + const promise = db.transaction(async (dbTxn) => { + await dbTxn.db + .insertInto('repo_push_event') + .values({ + eventType: 'takedown', + subjectDid: 'y', + }) + .returning('subjectDid') + .executeTakeFirst() + + throw new Error('Oops!') + }) + + await expect(promise).rejects.toThrow('Oops!') + + const row = await db.db + .selectFrom('repo_push_event') + .selectAll() + .where('subjectDid', '=', 'y') + .executeTakeFirst() + + expect(row).toBeUndefined() + }) + + it('indicates isTransaction', async () => { + expect(db.isTransaction).toEqual(false) + + await db.transaction(async (dbTxn) => { + expect(db.isTransaction).toEqual(false) + expect(dbTxn.isTransaction).toEqual(true) + }) + + expect(db.isTransaction).toEqual(false) + }) + + it('asserts transaction', async () => { + expect(() => db.assertTransaction()).toThrow('Transaction required') + + await db.transaction(async (dbTxn) => { + expect(() => dbTxn.assertTransaction()).not.toThrow() + }) + }) + + it('does not allow leaky transactions', async () => { + let leakedTx: Database | undefined + + const tx = db.transaction(async (dbTxn) => { + leakedTx = dbTxn + await dbTxn.db + .insertInto('repo_push_event') + .values({ eventType: 'takedown', subjectDid: 'a' }) + .execute() + throw new Error('test tx failed') + }) + await expect(tx).rejects.toThrow('test tx failed') + + const attempt = leakedTx?.db + .insertInto('repo_push_event') + .values({ eventType: 'takedown', subjectDid: 'b' }) + .execute() + await expect(attempt).rejects.toThrow('tx already failed') + + const res = await db.db + .selectFrom('repo_push_event') + .selectAll() + .where('subjectDid', 'in', ['a', 'b']) + .execute() + + expect(res.length).toBe(0) + }) + + it('ensures all inflight queries are rolled back', async () => { + let promise: Promise | undefined = undefined + const names: string[] = [] + try { + await db.transaction(async (dbTxn) => { + const queries: Promise[] = [] + for (let i = 0; i < 20; i++) { + const name = `user${i}` + const query = dbTxn.db + .insertInto('repo_push_event') + .values({ + eventType: 'takedown', + subjectDid: name, + }) + .execute() + names.push(name) + queries.push(query) + } + promise = Promise.allSettled(queries) + throw new Error() + }) + } catch (err) { + expect(err).toBeDefined() + } + if (promise) { + await promise + } + + const res = await db.db + .selectFrom('repo_push_event') + .selectAll() + .where('subjectDid', 'in', names) + .execute() + expect(res.length).toBe(0) + }) + }) +}) diff --git a/packages/bsky/tests/admin/get-record.test.ts b/packages/ozone/tests/get-record.test.ts similarity index 92% rename from packages/bsky/tests/admin/get-record.test.ts rename to packages/ozone/tests/get-record.test.ts index 3807724fa6c..303e0f054d0 100644 --- a/packages/bsky/tests/admin/get-record.test.ts +++ b/packages/ozone/tests/get-record.test.ts @@ -1,12 +1,11 @@ -import { SeedClient, TestNetwork } from '@atproto/dev-env' +import { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env' import AtpAgent from '@atproto/api' import { AtUri } from '@atproto/syntax' import { REASONOTHER, REASONSPAM, -} from '../../src/lexicon/types/com/atproto/moderation/defs' -import { forSnapshot } from '../_util' -import basicSeed from '../seeds/basic' +} from '../src/lexicon/types/com/atproto/moderation/defs' +import { forSnapshot } from './_util' describe('admin get record view', () => { let network: TestNetwork @@ -15,7 +14,7 @@ describe('admin get record view', () => { beforeAll(async () => { network = await TestNetwork.create({ - dbPostgresSchema: 'views_admin_get_record', + dbPostgresSchema: 'ozone_admin_get_record', }) agent = network.pds.getClient() sc = network.getSeedClient() diff --git a/packages/bsky/tests/admin/get-repo.test.ts b/packages/ozone/tests/get-repo.test.ts similarity index 93% rename from packages/bsky/tests/admin/get-repo.test.ts rename to packages/ozone/tests/get-repo.test.ts index 1e95f8cc0fc..1e0491465f5 100644 --- a/packages/bsky/tests/admin/get-repo.test.ts +++ b/packages/ozone/tests/get-repo.test.ts @@ -1,11 +1,10 @@ -import { SeedClient, TestNetwork } from '@atproto/dev-env' +import { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env' import AtpAgent from '@atproto/api' import { REASONOTHER, REASONSPAM, -} from '../../src/lexicon/types/com/atproto/moderation/defs' -import { forSnapshot } from '../_util' -import basicSeed from '../seeds/basic' +} from '../src/lexicon/types/com/atproto/moderation/defs' +import { forSnapshot } from './_util' describe('admin get repo view', () => { let network: TestNetwork @@ -14,7 +13,7 @@ describe('admin get repo view', () => { beforeAll(async () => { network = await TestNetwork.create({ - dbPostgresSchema: 'views_admin_get_repo', + dbPostgresSchema: 'ozone_admin_get_repo', }) agent = network.pds.getClient() sc = network.getSeedClient() diff --git a/packages/bsky/tests/admin/moderation-appeals.test.ts b/packages/ozone/tests/moderation-appeals.test.ts similarity index 94% rename from packages/bsky/tests/admin/moderation-appeals.test.ts rename to packages/ozone/tests/moderation-appeals.test.ts index 8b2af9a5a42..81f230bef82 100644 --- a/packages/bsky/tests/admin/moderation-appeals.test.ts +++ b/packages/ozone/tests/moderation-appeals.test.ts @@ -1,20 +1,19 @@ -import { TestNetwork, SeedClient } from '@atproto/dev-env' +import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' import AtpAgent, { ComAtprotoAdminDefs, ComAtprotoAdminEmitModerationEvent, ComAtprotoAdminQueryModerationStatuses, } from '@atproto/api' -import basicSeed from '../seeds/basic' import { REASONMISLEADING, REASONSPAM, -} from '../../src/lexicon/types/com/atproto/moderation/defs' +} from '../src/lexicon/types/com/atproto/moderation/defs' import { REVIEWCLOSED, REVIEWOPEN, } from '@atproto/api/src/client/types/com/atproto/admin/defs' import { REASONAPPEAL } from '@atproto/api/src/client/types/com/atproto/moderation/defs' -import { REVIEWESCALATED } from '../../src/lexicon/types/com/atproto/admin/defs' +import { REVIEWESCALATED } from '../src/lexicon/types/com/atproto/admin/defs' describe('moderation-appeals', () => { let network: TestNetwork @@ -27,7 +26,7 @@ describe('moderation-appeals', () => { ) => { return pdsAgent.api.com.atproto.admin.emitModerationEvent(eventData, { encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('moderator'), + headers: network.ozone.adminAuthHeaders('moderator'), }) } @@ -35,14 +34,14 @@ describe('moderation-appeals', () => { statusQuery: ComAtprotoAdminQueryModerationStatuses.QueryParams, ) => agent.api.com.atproto.admin.queryModerationStatuses(statusQuery, { - headers: network.bsky.adminAuthHeaders('moderator'), + headers: network.ozone.adminAuthHeaders('moderator'), }) beforeAll(async () => { network = await TestNetwork.create({ - dbPostgresSchema: 'bsky_moderation_statuses', + dbPostgresSchema: 'ozone_moderation_statuses', }) - agent = network.bsky.getClient() + agent = network.ozone.getClient() pdsAgent = network.pds.getClient() sc = network.getSeedClient() await basicSeed(sc) @@ -65,6 +64,7 @@ describe('moderation-appeals', () => { expect(data.subjectStatuses[0]?.appealed).toEqual(appealed) return data.subjectStatuses[0] } + describe('appeals from users', () => { const getBobsPostSubject = () => ({ $type: 'com.atproto.repo.strongRef', diff --git a/packages/bsky/tests/admin/moderation-events.test.ts b/packages/ozone/tests/moderation-events.test.ts similarity index 94% rename from packages/bsky/tests/admin/moderation-events.test.ts rename to packages/ozone/tests/moderation-events.test.ts index 174167034db..73149dc06d8 100644 --- a/packages/bsky/tests/admin/moderation-events.test.ts +++ b/packages/ozone/tests/moderation-events.test.ts @@ -1,11 +1,10 @@ -import { TestNetwork, SeedClient } from '@atproto/dev-env' +import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' import AtpAgent, { ComAtprotoAdminDefs } from '@atproto/api' -import { forSnapshot } from '../_util' -import basicSeed from '../seeds/basic' +import { forSnapshot } from './_util' import { REASONMISLEADING, REASONSPAM, -} from '../../src/lexicon/types/com/atproto/moderation/defs' +} from '../src/lexicon/types/com/atproto/moderation/defs' describe('moderation-events', () => { let network: TestNetwork @@ -71,9 +70,9 @@ describe('moderation-events', () => { beforeAll(async () => { network = await TestNetwork.create({ - dbPostgresSchema: 'bsky_moderation_events', + dbPostgresSchema: 'ozone_moderation_events', }) - agent = network.bsky.getClient() + agent = network.ozone.getClient() pdsAgent = network.pds.getClient() sc = network.getSeedClient() await basicSeed(sc) @@ -197,10 +196,10 @@ describe('moderation-events', () => { const defaultEvents = await getPaginatedEvents() const reversedEvents = await getPaginatedEvents('asc') - expect(allEvents.data.events.length).toEqual(4) + expect(allEvents.data.events.length).toEqual(5) expect(defaultEvents.length).toEqual(allEvents.data.events.length) expect(reversedEvents.length).toEqual(allEvents.data.events.length) - expect(reversedEvents[0].id).toEqual(defaultEvents[3].id) + expect(reversedEvents[0].id).toEqual(defaultEvents[4].id) }) }) diff --git a/packages/bsky/tests/admin/moderation-statuses.test.ts b/packages/ozone/tests/moderation-statuses.test.ts similarity index 93% rename from packages/bsky/tests/admin/moderation-statuses.test.ts rename to packages/ozone/tests/moderation-statuses.test.ts index 5109cc43b0e..5f63dbfe9a4 100644 --- a/packages/bsky/tests/admin/moderation-statuses.test.ts +++ b/packages/ozone/tests/moderation-statuses.test.ts @@ -1,14 +1,13 @@ -import { TestNetwork, SeedClient } from '@atproto/dev-env' +import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env' import AtpAgent, { ComAtprotoAdminDefs, ComAtprotoAdminQueryModerationStatuses, } from '@atproto/api' -import { forSnapshot } from '../_util' -import basicSeed from '../seeds/basic' +import { forSnapshot } from './_util' import { REASONMISLEADING, REASONSPAM, -} from '../../src/lexicon/types/com/atproto/moderation/defs' +} from '../src/lexicon/types/com/atproto/moderation/defs' describe('moderation-statuses', () => { let network: TestNetwork @@ -74,9 +73,9 @@ describe('moderation-statuses', () => { beforeAll(async () => { network = await TestNetwork.create({ - dbPostgresSchema: 'bsky_moderation_statuses', + dbPostgresSchema: 'ozone_moderation_statuses', }) - agent = network.bsky.getClient() + agent = network.ozone.getClient() pdsAgent = network.pds.getClient() sc = network.getSeedClient() await basicSeed(sc) diff --git a/packages/ozone/tests/moderation.test.ts b/packages/ozone/tests/moderation.test.ts new file mode 100644 index 00000000000..20cab2f2b49 --- /dev/null +++ b/packages/ozone/tests/moderation.test.ts @@ -0,0 +1,961 @@ +import { + TestNetwork, + ImageRef, + RecordRef, + SeedClient, + basicSeed, +} from '@atproto/dev-env' +import AtpAgent, { + ComAtprotoAdminEmitModerationEvent, + ComAtprotoAdminQueryModerationStatuses, + ComAtprotoModerationCreateReport, +} from '@atproto/api' +import { AtUri } from '@atproto/syntax' +import { forSnapshot } from './_util' +import { + REASONMISLEADING, + REASONOTHER, + REASONSPAM, +} from '../src/lexicon/types/com/atproto/moderation/defs' +import { + ModEventLabel, + ModEventTakedown, + REVIEWCLOSED, + REVIEWESCALATED, +} from '../src/lexicon/types/com/atproto/admin/defs' +import { EventReverser } from '../src' +import { TestOzone } from '@atproto/dev-env/src/ozone' + +type BaseCreateReportParams = + | { account: string } + | { content: { uri: string; cid: string } } +type CreateReportParams = BaseCreateReportParams & { + author: string +} & Omit + +type TakedownParams = BaseCreateReportParams & + Omit + +describe('moderation', () => { + let network: TestNetwork + let ozone: TestOzone + let agent: AtpAgent + let pdsAgent: AtpAgent + let sc: SeedClient + + const createReport = async (params: CreateReportParams) => { + const { author, ...rest } = params + return agent.api.com.atproto.moderation.createReport( + { + // Set default type to spam + reasonType: REASONSPAM, + ...rest, + subject: + 'account' in params + ? { + $type: 'com.atproto.admin.defs#repoRef', + did: params.account, + } + : { + $type: 'com.atproto.repo.strongRef', + uri: params.content.uri, + cid: params.content.cid, + }, + }, + { + headers: await network.serviceHeaders( + author, + network.ozone.ctx.cfg.service.did, + ), + encoding: 'application/json', + }, + ) + } + + const performTakedown = async ({ + durationInHours, + ...rest + }: TakedownParams & Pick) => + agent.api.com.atproto.admin.emitModerationEvent( + { + event: { + $type: 'com.atproto.admin.defs#modEventTakedown', + durationInHours, + }, + subject: + 'account' in rest + ? { + $type: 'com.atproto.admin.defs#repoRef', + did: rest.account, + } + : { + $type: 'com.atproto.repo.strongRef', + uri: rest.content.uri, + cid: rest.content.cid, + }, + createdBy: 'did:example:admin', + ...rest, + }, + { + encoding: 'application/json', + headers: ozone.adminAuthHeaders(), + }, + ) + + const performReverseTakedown = async (params: TakedownParams) => + agent.api.com.atproto.admin.emitModerationEvent( + { + event: { + $type: 'com.atproto.admin.defs#modEventReverseTakedown', + }, + subject: + 'account' in params + ? { + $type: 'com.atproto.admin.defs#repoRef', + did: params.account, + } + : { + $type: 'com.atproto.repo.strongRef', + uri: params.content.uri, + cid: params.content.cid, + }, + createdBy: 'did:example:admin', + ...params, + }, + { + encoding: 'application/json', + headers: ozone.adminAuthHeaders(), + }, + ) + + const getStatuses = async ( + params: ComAtprotoAdminQueryModerationStatuses.QueryParams, + ) => { + const { data } = await agent.api.com.atproto.admin.queryModerationStatuses( + params, + { headers: ozone.adminAuthHeaders() }, + ) + + return data + } + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'ozone_moderation', + }) + ozone = network.ozone + agent = network.ozone.getClient() + pdsAgent = network.pds.getClient() + sc = network.getSeedClient() + await basicSeed(sc) + await network.processAll() + }) + + afterAll(async () => { + await network.close() + }) + + describe('reporting', () => { + it('creates reports of a repo.', async () => { + const { data: reportA } = await createReport({ + reasonType: REASONSPAM, + account: sc.dids.bob, + author: sc.dids.alice, + }) + const { data: reportB } = await createReport({ + reasonType: REASONOTHER, + reason: 'impersonation', + account: sc.dids.bob, + author: sc.dids.carol, + }) + expect(forSnapshot([reportA, reportB])).toMatchSnapshot() + }) + + it("allows reporting a repo that doesn't exist.", async () => { + const promise = createReport({ + reasonType: REASONSPAM, + account: 'did:plc:unknown', + author: sc.dids.alice, + }) + await expect(promise).resolves.toBeDefined() + }) + + it('creates reports of a record.', async () => { + const postA = sc.posts[sc.dids.bob][0].ref + const postB = sc.posts[sc.dids.bob][1].ref + const { data: reportA } = await createReport({ + author: sc.dids.alice, + reasonType: REASONSPAM, + content: { + $type: 'com.atproto.repo.strongRef', + uri: postA.uriStr, + cid: postA.cidStr, + }, + }) + const { data: reportB } = await createReport({ + reasonType: REASONOTHER, + reason: 'defamation', + content: { + $type: 'com.atproto.repo.strongRef', + uri: postB.uriStr, + cid: postB.cidStr, + }, + author: sc.dids.carol, + }) + expect(forSnapshot([reportA, reportB])).toMatchSnapshot() + }) + + it("allows reporting a record that doesn't exist.", async () => { + const postA = sc.posts[sc.dids.bob][0].ref + const postB = sc.posts[sc.dids.bob][1].ref + const postUriBad = new AtUri(postA.uriStr) + postUriBad.rkey = 'badrkey' + + const promiseA = createReport({ + reasonType: REASONSPAM, + content: { + $type: 'com.atproto.repo.strongRef', + uri: postUriBad.toString(), + cid: postA.cidStr, + }, + author: sc.dids.alice, + }) + await expect(promiseA).resolves.toBeDefined() + + const promiseB = createReport({ + reasonType: REASONOTHER, + reason: 'defamation', + content: { + $type: 'com.atproto.repo.strongRef', + uri: postB.uri.toString(), + cid: postA.cidStr, // bad cid + }, + author: sc.dids.carol, + }) + await expect(promiseB).resolves.toBeDefined() + }) + }) + + describe('actioning', () => { + it('resolves reports on repos and records.', async () => { + const post = sc.posts[sc.dids.bob][1].ref + + await Promise.all([ + createReport({ + reasonType: REASONSPAM, + account: sc.dids.bob, + author: sc.dids.alice, + }), + createReport({ + reasonType: REASONOTHER, + reason: 'defamation', + content: { + uri: post.uri.toString(), + cid: post.cid.toString(), + }, + author: sc.dids.carol, + }), + ]) + + await performTakedown({ + account: sc.dids.bob, + }) + + const moderationStatusOnBobsAccount = await getStatuses({ + subject: sc.dids.bob, + }) + + // Validate that subject status is set to review closed and takendown flag is on + expect(moderationStatusOnBobsAccount.subjectStatuses[0]).toMatchObject({ + reviewState: REVIEWCLOSED, + takendown: true, + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.bob, + }, + }) + + // Cleanup + await performReverseTakedown({ + account: sc.dids.bob, + }) + }) + + it('supports escalating a subject', async () => { + const alicesPostRef = sc.posts[sc.dids.alice][0].ref + const alicesPostSubject = { + $type: 'com.atproto.repo.strongRef', + uri: alicesPostRef.uri.toString(), + cid: alicesPostRef.cid.toString(), + } + await agent.api.com.atproto.admin.emitModerationEvent( + { + event: { + $type: 'com.atproto.admin.defs#modEventEscalate', + comment: 'Y', + }, + subject: alicesPostSubject, + createdBy: 'did:example:admin', + }, + { + encoding: 'application/json', + headers: ozone.adminAuthHeaders('triage'), + }, + ) + + const alicesPostStatus = await getStatuses({ + subject: alicesPostRef.uri.toString(), + }) + + expect(alicesPostStatus.subjectStatuses[0]).toMatchObject({ + reviewState: REVIEWESCALATED, + takendown: false, + subject: alicesPostSubject, + }) + }) + + it('adds persistent comment on subject through comment event', async () => { + const alicesPostRef = sc.posts[sc.dids.alice][0].ref + const alicesPostSubject = { + $type: 'com.atproto.repo.strongRef', + uri: alicesPostRef.uri.toString(), + cid: alicesPostRef.cid.toString(), + } + await agent.api.com.atproto.admin.emitModerationEvent( + { + event: { + $type: 'com.atproto.admin.defs#modEventComment', + sticky: true, + comment: 'This is a persistent note', + }, + subject: alicesPostSubject, + createdBy: 'did:example:admin', + }, + { + encoding: 'application/json', + headers: ozone.adminAuthHeaders('triage'), + }, + ) + + const alicesPostStatus = await getStatuses({ + subject: alicesPostRef.uri.toString(), + }) + + expect(alicesPostStatus.subjectStatuses[0].comment).toEqual( + 'This is a persistent note', + ) + }) + + it('reverses status when revert event is triggered.', async () => { + const alicesPostRef = sc.posts[sc.dids.alice][0].ref + const emitModEvent = async ( + event: ComAtprotoAdminEmitModerationEvent.InputSchema['event'], + overwrites: Partial = {}, + ) => { + const baseAction = { + subject: { + $type: 'com.atproto.repo.strongRef', + uri: alicesPostRef.uriStr, + cid: alicesPostRef.cidStr, + }, + createdBy: 'did:example:admin', + } + return agent.api.com.atproto.admin.emitModerationEvent( + { + event, + ...baseAction, + ...overwrites, + }, + { + encoding: 'application/json', + headers: ozone.adminAuthHeaders(), + }, + ) + } + // Validate that subject status is marked as escalated + await emitModEvent({ + $type: 'com.atproto.admin.defs#modEventReport', + reportType: REASONSPAM, + }) + await emitModEvent({ + $type: 'com.atproto.admin.defs#modEventReport', + reportType: REASONMISLEADING, + }) + await emitModEvent({ + $type: 'com.atproto.admin.defs#modEventEscalate', + }) + const alicesPostStatusAfterEscalation = await getStatuses({ + subject: alicesPostRef.uriStr, + }) + expect( + alicesPostStatusAfterEscalation.subjectStatuses[0].reviewState, + ).toEqual(REVIEWESCALATED) + + // Validate that subject status is marked as takendown + + await emitModEvent({ + $type: 'com.atproto.admin.defs#modEventLabel', + createLabelVals: ['nsfw'], + negateLabelVals: [], + }) + await emitModEvent({ + $type: 'com.atproto.admin.defs#modEventTakedown', + }) + + const alicesPostStatusAfterTakedown = await getStatuses({ + subject: alicesPostRef.uriStr, + }) + expect(alicesPostStatusAfterTakedown.subjectStatuses[0]).toMatchObject({ + reviewState: REVIEWCLOSED, + takendown: true, + }) + + await emitModEvent({ + $type: 'com.atproto.admin.defs#modEventReverseTakedown', + }) + const alicesPostStatusAfterRevert = await getStatuses({ + subject: alicesPostRef.uriStr, + }) + // Validate that after reverting, the status of the subject is reverted to the last status changing event + expect(alicesPostStatusAfterRevert.subjectStatuses[0]).toMatchObject({ + reviewState: REVIEWCLOSED, + takendown: false, + }) + // Validate that after reverting, the last review date of the subject + // DOES NOT update to the the last status changing event + expect( + new Date( + alicesPostStatusAfterEscalation.subjectStatuses[0] + .lastReviewedAt as string, + ) < + new Date( + alicesPostStatusAfterRevert.subjectStatuses[0] + .lastReviewedAt as string, + ), + ).toBeTruthy() + }) + + it('negates an existing label.', async () => { + const { ctx } = ozone + const post = sc.posts[sc.dids.bob][0].ref + const bobsPostSubject = { + $type: 'com.atproto.repo.strongRef', + uri: post.uriStr, + cid: post.cidStr, + } + const modService = ctx.modService(ctx.db) + await modService.formatAndCreateLabels( + ctx.cfg.service.did, + post.uriStr, + post.cidStr, + { create: ['kittens'] }, + ) + await emitLabelEvent({ + negateLabelVals: ['kittens'], + createLabelVals: [], + subject: bobsPostSubject, + }) + await expect(getRecordLabels(post.uriStr)).resolves.toEqual([]) + + await emitLabelEvent({ + createLabelVals: ['kittens'], + negateLabelVals: [], + subject: bobsPostSubject, + }) + await expect(getRecordLabels(post.uriStr)).resolves.toEqual(['kittens']) + // Cleanup + await modService.formatAndCreateLabels( + ctx.cfg.service.did, + post.uriStr, + post.cidStr, + { negate: ['kittens'] }, + ) + }) + + it('no-ops when negating an already-negated label and reverses.', async () => { + const { ctx } = ozone + const post = sc.posts[sc.dids.bob][0].ref + const modService = ctx.modService(ctx.db) + await emitLabelEvent({ + negateLabelVals: ['bears'], + createLabelVals: [], + subject: { + $type: 'com.atproto.repo.strongRef', + uri: post.uriStr, + cid: post.cidStr, + }, + }) + await expect(getRecordLabels(post.uriStr)).resolves.toEqual([]) + await emitLabelEvent({ + createLabelVals: ['bears'], + negateLabelVals: [], + subject: { + $type: 'com.atproto.repo.strongRef', + uri: post.uriStr, + cid: post.cidStr, + }, + }) + await expect(getRecordLabels(post.uriStr)).resolves.toEqual(['bears']) + // Cleanup + await modService.formatAndCreateLabels( + ctx.cfg.service.did, + post.uriStr, + post.cidStr, + { negate: ['bears'] }, + ) + }) + + it('creates non-existing labels and reverses.', async () => { + const post = sc.posts[sc.dids.bob][0].ref + await emitLabelEvent({ + 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 emitLabelEvent({ + negateLabelVals: ['puppies', 'doggies'], + createLabelVals: [], + subject: { + $type: 'com.atproto.repo.strongRef', + uri: post.uriStr, + cid: post.cidStr, + }, + }) + await expect(getRecordLabels(post.uriStr)).resolves.toEqual([]) + }) + + it('creates labels on a repo and reverses.', async () => { + await emitLabelEvent({ + 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 emitLabelEvent({ + negateLabelVals: ['puppies', 'doggies'], + createLabelVals: [], + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.bob, + }, + }) + await expect(getRepoLabels(sc.dids.bob)).resolves.toEqual([]) + }) + + it('creates and negates labels on a repo and reverses.', async () => { + const { ctx } = ozone + const modService = ctx.modService(ctx.db) + await modService.formatAndCreateLabels( + ctx.cfg.service.did, + sc.dids.bob, + null, + { create: ['kittens'] }, + ) + await emitLabelEvent({ + 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 emitLabelEvent({ + negateLabelVals: ['puppies'], + createLabelVals: ['kittens'], + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.bob, + }, + }) + await expect(getRepoLabels(sc.dids.bob)).resolves.toEqual(['kittens']) + }) + + it('does not allow triage moderators to label.', async () => { + const attemptLabel = agent.api.com.atproto.admin.emitModerationEvent( + { + event: { + $type: 'com.atproto.admin.defs#modEventLabel', + negateLabelVals: ['a'], + createLabelVals: ['b', 'c'], + }, + createdBy: 'did:example:moderator', + reason: 'Y', + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.bob, + }, + }, + { + encoding: 'application/json', + headers: network.bsky.adminAuthHeaders('triage'), + }, + ) + await expect(attemptLabel).rejects.toThrow( + 'Must be a full moderator to label content', + ) + }) + + it('does not allow take down event on takendown post or reverse takedown on available post.', async () => { + await performTakedown({ + account: sc.dids.bob, + }) + await expect( + performTakedown({ + account: sc.dids.bob, + }), + ).rejects.toThrow('Subject is already taken down') + + // Cleanup + await performReverseTakedown({ + account: sc.dids.bob, + }) + await expect( + performReverseTakedown({ + account: sc.dids.bob, + }), + ).rejects.toThrow('Subject is not taken down') + }) + + it('fans out repo takedowns to pds', async () => { + await performTakedown({ + account: sc.dids.bob, + }) + await ozone.processAll() + + const res1 = await pdsAgent.api.com.atproto.admin.getSubjectStatus( + { + did: sc.dids.bob, + }, + { headers: network.pds.adminAuthHeaders() }, + ) + expect(res1.data.takedown?.applied).toBe(true) + + // cleanup + await performReverseTakedown({ account: sc.dids.bob }) + await ozone.processAll() + + const res2 = await pdsAgent.api.com.atproto.admin.getSubjectStatus( + { + did: sc.dids.bob, + }, + { headers: network.pds.adminAuthHeaders() }, + ) + expect(res2.data.takedown?.applied).toBe(false) + }) + + it('fans out record takedowns to pds', async () => { + const post = sc.posts[sc.dids.bob][0] + const uri = post.ref.uriStr + const cid = post.ref.cidStr + await performTakedown({ + content: { uri, cid }, + }) + await ozone.processAll() + const res1 = await pdsAgent.api.com.atproto.admin.getSubjectStatus( + { uri }, + { headers: network.pds.adminAuthHeaders() }, + ) + expect(res1.data.takedown?.applied).toBe(true) + + // cleanup + await performReverseTakedown({ content: { uri, cid } }) + await ozone.processAll() + + const res2 = await pdsAgent.api.com.atproto.admin.getSubjectStatus( + { uri }, + { headers: network.pds.adminAuthHeaders() }, + ) + expect(res2.data.takedown?.applied).toBe(false) + }) + + it('allows full moderators to takedown.', async () => { + await agent.api.com.atproto.admin.emitModerationEvent( + { + event: { + $type: 'com.atproto.admin.defs#modEventTakedown', + }, + createdBy: 'did:example:moderator', + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.bob, + }, + }, + { + encoding: 'application/json', + headers: network.bsky.adminAuthHeaders('moderator'), + }, + ) + // cleanup + await reverse({ + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.bob, + }, + }) + }) + + it('does not allow non-full moderators to takedown.', async () => { + const attemptTakedownTriage = + agent.api.com.atproto.admin.emitModerationEvent( + { + event: { + $type: 'com.atproto.admin.defs#modEventTakedown', + }, + createdBy: 'did:example:moderator', + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.bob, + }, + }, + { + encoding: 'application/json', + headers: network.bsky.adminAuthHeaders('triage'), + }, + ) + await expect(attemptTakedownTriage).rejects.toThrow( + 'Must be a full moderator to perform an account takedown', + ) + }) + it('automatically reverses actions marked with duration', async () => { + await createReport({ + reasonType: REASONSPAM, + account: sc.dids.bob, + author: sc.dids.alice, + }) + const { data: action } = await performTakedown({ + account: sc.dids.bob, + // Use negative value to set the expiry time in the past so that the action is automatically reversed + // right away without having to wait n number of hours for a successful assertion + durationInHours: -1, + }) + await ozone.processAll() + + const { data: statusesAfterTakedown } = + await agent.api.com.atproto.admin.queryModerationStatuses( + { subject: sc.dids.bob }, + { headers: network.bsky.adminAuthHeaders('moderator') }, + ) + + expect(statusesAfterTakedown.subjectStatuses[0]).toMatchObject({ + takendown: true, + }) + + // In the actual app, this will be instantiated and run on server startup + const reverser = new EventReverser( + network.ozone.ctx.db, + network.ozone.ctx.modService, + ) + await reverser.findAndRevertDueActions() + await ozone.processAll() + + const [{ data: eventList }, { data: statuses }] = await Promise.all([ + agent.api.com.atproto.admin.queryModerationEvents( + { subject: sc.dids.bob }, + { headers: network.bsky.adminAuthHeaders('moderator') }, + ), + agent.api.com.atproto.admin.queryModerationStatuses( + { subject: sc.dids.bob }, + { headers: network.bsky.adminAuthHeaders('moderator') }, + ), + ]) + + expect(statuses.subjectStatuses[0]).toMatchObject({ + takendown: false, + reviewState: REVIEWCLOSED, + }) + // Verify that the automatic reversal is attributed to the original moderator of the temporary action + // and that the reason is set to indicate that the action was automatically reversed. + expect(eventList.events[0]).toMatchObject({ + createdBy: action.createdBy, + event: { + $type: 'com.atproto.admin.defs#modEventReverseTakedown', + comment: + '[SCHEDULED_REVERSAL] Reverting action as originally scheduled', + }, + }) + }) + + async function emitLabelEvent( + opts: Partial & { + subject: ComAtprotoAdminEmitModerationEvent.InputSchema['subject'] + createLabelVals: ModEventLabel['createLabelVals'] + negateLabelVals: ModEventLabel['negateLabelVals'] + }, + ) { + const { createLabelVals, negateLabelVals } = opts + const result = await agent.api.com.atproto.admin.emitModerationEvent( + { + event: { + $type: 'com.atproto.admin.defs#modEventLabel', + createLabelVals, + negateLabelVals, + }, + createdBy: 'did:example:admin', + reason: 'Y', + ...opts, + }, + { + encoding: 'application/json', + headers: network.bsky.adminAuthHeaders(), + }, + ) + return result.data + } + + async function reverse( + opts: Partial & { + subject: ComAtprotoAdminEmitModerationEvent.InputSchema['subject'] + }, + ) { + await agent.api.com.atproto.admin.emitModerationEvent( + { + event: { + $type: 'com.atproto.admin.defs#modEventReverseTakedown', + }, + createdBy: 'did:example:admin', + reason: 'Y', + ...opts, + }, + { + encoding: 'application/json', + headers: network.bsky.adminAuthHeaders(), + }, + ) + } + + async function getRecordLabels(uri: string) { + const result = await agent.api.com.atproto.admin.getRecord( + { uri }, + { headers: network.bsky.adminAuthHeaders() }, + ) + 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: network.bsky.adminAuthHeaders() }, + ) + 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 + beforeAll(async () => { + const { ctx } = network.bsky + post = sc.posts[sc.dids.carol][0] + blob = post.images[1] + imageUri = ctx.imgUriBuilder + .getPresetUri( + 'feed_thumbnail', + sc.dids.carol, + blob.image.ref.toString(), + ) + .replace(ctx.cfg.publicUrl || '', network.bsky.url) + // Warm image server cache + await fetch(imageUri) + const cached = await fetch(imageUri) + expect(cached.headers.get('x-cache')).toEqual('hit') + await performTakedown({ + content: { + uri: post.ref.uriStr, + cid: post.ref.cidStr, + }, + subjectBlobCids: [blob.image.ref.toString()], + }) + await ozone.processAll() + }) + + it('sets blobCids in moderation status', async () => { + const { subjectStatuses } = await getStatuses({ + subject: post.ref.uriStr, + }) + + expect(subjectStatuses[0].subjectBlobCids).toEqual([ + blob.image.ref.toString(), + ]) + }) + + it('prevents resolution of blob', async () => { + const blobPath = `/blob/${sc.dids.carol}/${blob.image.ref.toString()}` + const resolveBlob = await fetch(`${network.bsky.url}${blobPath}`) + expect(resolveBlob.status).toEqual(404) + expect(await resolveBlob.json()).toEqual({ + error: 'NotFoundError', + message: 'Blob not found', + }) + }) + + it('prevents image blob from being served, even when cached.', async () => { + const fetchImage = await fetch(imageUri) + expect(fetchImage.status).toEqual(404) + expect(await fetchImage.json()).toEqual({ message: 'Image not found' }) + }) + + it('fans takedown out to pds', async () => { + const res = await pdsAgent.api.com.atproto.admin.getSubjectStatus( + { + did: sc.dids.carol, + blob: blob.image.ref.toString(), + }, + { headers: network.pds.adminAuthHeaders() }, + ) + expect(res.data.takedown?.applied).toBe(true) + }) + + it('restores blob when action is reversed.', async () => { + await performReverseTakedown({ + content: { + uri: post.ref.uriStr, + cid: post.ref.cidStr, + }, + subjectBlobCids: [blob.image.ref.toString()], + }) + + await ozone.processAll() + + // Can resolve blob + const blobPath = `/blob/${sc.dids.carol}/${blob.image.ref.toString()}` + const resolveBlob = await fetch(`${network.bsky.url}${blobPath}`) + expect(resolveBlob.status).toEqual(200) + + // 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) + }) + + it('fans reversal out to pds', async () => { + const res = await pdsAgent.api.com.atproto.admin.getSubjectStatus( + { + did: sc.dids.carol, + blob: blob.image.ref.toString(), + }, + { headers: network.pds.adminAuthHeaders() }, + ) + expect(res.data.takedown?.applied).toBe(false) + }) + }) +}) diff --git a/packages/bsky/tests/admin/repo-search.test.ts b/packages/ozone/tests/repo-search.test.ts similarity index 94% rename from packages/bsky/tests/admin/repo-search.test.ts rename to packages/ozone/tests/repo-search.test.ts index 9e643ba12e0..0d41b014c1b 100644 --- a/packages/bsky/tests/admin/repo-search.test.ts +++ b/packages/ozone/tests/repo-search.test.ts @@ -1,7 +1,6 @@ -import { SeedClient, TestNetwork } from '@atproto/dev-env' +import { SeedClient, TestNetwork, usersBulkSeed } from '@atproto/dev-env' import AtpAgent from '@atproto/api' -import { paginateAll } from '../_util' -import usersBulkSeed from '../seeds/users-bulk' +import { paginateAll } from './_util' describe('admin repo search view', () => { let network: TestNetwork @@ -11,7 +10,7 @@ describe('admin repo search view', () => { beforeAll(async () => { network = await TestNetwork.create({ - dbPostgresSchema: 'views_admin_repo_search', + dbPostgresSchema: 'ozone_admin_repo_search', }) agent = network.pds.getClient() sc = network.getSeedClient() diff --git a/packages/ozone/tests/server.test.ts b/packages/ozone/tests/server.test.ts new file mode 100644 index 00000000000..4224a636739 --- /dev/null +++ b/packages/ozone/tests/server.test.ts @@ -0,0 +1,76 @@ +import { AddressInfo } from 'net' +import express from 'express' +import axios, { AxiosError } from 'axios' +import { TestNetwork } from '@atproto/dev-env' +import { handler as errorHandler } from '../src/error' +import { TestOzone } from '@atproto/dev-env/src/ozone' + +describe('server', () => { + let network: TestNetwork + let ozone: TestOzone + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'ozone_server', + }) + ozone = network.ozone + }) + + afterAll(async () => { + await network.close() + }) + + it('preserves 404s.', async () => { + const promise = axios.get(`${ozone.url}/unknown`) + await expect(promise).rejects.toThrow('failed with status code 404') + }) + + it('error handler turns unknown errors into 500s.', async () => { + const app = express() + app.get('/oops', () => { + throw new Error('Oops!') + }) + app.use(errorHandler) + const srv = app.listen() + const port = (srv.address() as AddressInfo).port + const promise = axios.get(`http://localhost:${port}/oops`) + await expect(promise).rejects.toThrow('failed with status code 500') + srv.close() + try { + await promise + } catch (err: unknown) { + const axiosError = err as AxiosError + expect(axiosError.response?.status).toEqual(500) + expect(axiosError.response?.data).toEqual({ + error: 'InternalServerError', + message: 'Internal Server Error', + }) + } + }) + + it('healthcheck succeeds when database is available.', async () => { + const { data, status } = await axios.get(`${ozone.url}/xrpc/_health`) + expect(status).toEqual(200) + expect(data).toEqual({ version: '0.0.0' }) + }) + + it('healthcheck fails when database is unavailable.', async () => { + await ozone.ctx.db.close() + let error: AxiosError + try { + await axios.get(`${ozone.url}/xrpc/_health`) + throw new Error('Healthcheck should have failed') + } catch (err) { + if (axios.isAxiosError(err)) { + error = err + } else { + throw err + } + } + expect(error.response?.status).toEqual(503) + expect(error.response?.data).toEqual({ + version: '0.0.0', + error: 'Service Unavailable', + }) + }) +}) diff --git a/packages/ozone/tsconfig.build.json b/packages/ozone/tsconfig.build.json new file mode 100644 index 00000000000..02a84823b65 --- /dev/null +++ b/packages/ozone/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["**/*.spec.ts", "**/*.test.ts"] +} diff --git a/packages/ozone/tsconfig.json b/packages/ozone/tsconfig.json new file mode 100644 index 00000000000..3f6ca1c27ec --- /dev/null +++ b/packages/ozone/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "emitDeclarationOnly": true + }, + "module": "nodenext", + "include": ["./src", "__tests__/**/**.ts"], + "references": [ + { "path": "../api/tsconfig.build.json" }, + { "path": "../common/tsconfig.build.json" }, + { "path": "../crypto/tsconfig.build.json" }, + { "path": "../identifier/tsconfig.build.json" }, + { "path": "../lexicon/tsconfig.build.json" }, + { "path": "../lex-cli/tsconfig.build.json" }, + { "path": "../repo/tsconfig.build.json" }, + { "path": "../uri/tsconfig.build.json" }, + { "path": "../xrpc-server/tsconfig.build.json" } + ] +} diff --git a/packages/pds/src/api/app/bsky/actor/getProfile.ts b/packages/pds/src/api/app/bsky/actor/getProfile.ts index 4c4f0958abe..732c7babe1a 100644 --- a/packages/pds/src/api/app/bsky/actor/getProfile.ts +++ b/packages/pds/src/api/app/bsky/actor/getProfile.ts @@ -16,7 +16,7 @@ export default function (server: Server, ctx: AppContext) { 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), + requester ? await ctx.appviewAuthHeaders(requester) : authPassthru(req), ) if (res.data.did === requester) { return await handleReadAfterWrite(ctx, requester, res, getProfileMunge) diff --git a/packages/pds/src/api/app/bsky/actor/getProfiles.ts b/packages/pds/src/api/app/bsky/actor/getProfiles.ts index bc78a26044e..2f0f1405378 100644 --- a/packages/pds/src/api/app/bsky/actor/getProfiles.ts +++ b/packages/pds/src/api/app/bsky/actor/getProfiles.ts @@ -14,7 +14,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did const res = await ctx.appViewAgent.api.app.bsky.actor.getProfiles( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) const hasSelf = res.data.profiles.some((prof) => prof.did === requester) if (hasSelf) { diff --git a/packages/pds/src/api/app/bsky/actor/getSuggestions.ts b/packages/pds/src/api/app/bsky/actor/getSuggestions.ts index 70f6bb6adef..5fd8b260276 100644 --- a/packages/pds/src/api/app/bsky/actor/getSuggestions.ts +++ b/packages/pds/src/api/app/bsky/actor/getSuggestions.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did const res = await ctx.appViewAgent.api.app.bsky.actor.getSuggestions( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/actor/searchActors.ts b/packages/pds/src/api/app/bsky/actor/searchActors.ts index 86b028f5242..7c184de1116 100644 --- a/packages/pds/src/api/app/bsky/actor/searchActors.ts +++ b/packages/pds/src/api/app/bsky/actor/searchActors.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did const res = await ctx.appViewAgent.api.app.bsky.actor.searchActors( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts b/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts index 9e969c4700a..c1a8738488d 100644 --- a/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/pds/src/api/app/bsky/actor/searchActorsTypeahead.ts @@ -9,7 +9,7 @@ export default function (server: Server, ctx: AppContext) { const res = await ctx.appViewAgent.api.app.bsky.actor.searchActorsTypeahead( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts b/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts index 1a5f3a15ed3..384d68500d4 100644 --- a/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts +++ b/packages/pds/src/api/app/bsky/feed/getActorFeeds.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did const res = await ctx.appViewAgent.api.app.bsky.feed.getActorFeeds( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/feed/getActorLikes.ts b/packages/pds/src/api/app/bsky/feed/getActorLikes.ts index d0d18787f9d..07fb66fd828 100644 --- a/packages/pds/src/api/app/bsky/feed/getActorLikes.ts +++ b/packages/pds/src/api/app/bsky/feed/getActorLikes.ts @@ -17,7 +17,7 @@ export default function (server: Server, ctx: AppContext) { const res = await ctx.appViewAgent.api.app.bsky.feed.getActorLikes( params, - requester ? await ctx.serviceAuthHeaders(requester) : authPassthru(req), + requester ? await ctx.appviewAuthHeaders(requester) : authPassthru(req), ) if (requester) { return await handleReadAfterWrite(ctx, requester, res, getAuthorMunge) diff --git a/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts index 26c001990e3..17ae8f0ac10 100644 --- a/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getAuthorFeed.ts @@ -17,7 +17,7 @@ export default function (server: Server, ctx: AppContext) { auth.credentials.type === 'access' ? auth.credentials.did : null const res = await ctx.appViewAgent.api.app.bsky.feed.getAuthorFeed( params, - requester ? await ctx.serviceAuthHeaders(requester) : authPassthru(req), + requester ? await ctx.appviewAuthHeaders(requester) : authPassthru(req), ) if (requester) { return await handleReadAfterWrite(ctx, requester, res, getAuthorMunge) diff --git a/packages/pds/src/api/app/bsky/feed/getFeed.ts b/packages/pds/src/api/app/bsky/feed/getFeed.ts index 3a768905dc2..82bb2e30b16 100644 --- a/packages/pds/src/api/app/bsky/feed/getFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getFeed.ts @@ -10,7 +10,7 @@ export default function (server: Server, ctx: AppContext) { const { data: feed } = await ctx.appViewAgent.api.app.bsky.feed.getFeedGenerator( { feed: params.feed }, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) const res = await ctx.appViewAgent.api.app.bsky.feed.getFeed( params, diff --git a/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts b/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts index 78ee243bc08..57c4731db53 100644 --- a/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts +++ b/packages/pds/src/api/app/bsky/feed/getFeedGenerator.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did const res = await ctx.appViewAgent.api.app.bsky.feed.getFeedGenerator( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts b/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts index 064525afcec..1370fbbd6f3 100644 --- a/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts +++ b/packages/pds/src/api/app/bsky/feed/getFeedGenerators.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did const res = await ctx.appViewAgent.api.app.bsky.feed.getFeedGenerators( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/feed/getLikes.ts b/packages/pds/src/api/app/bsky/feed/getLikes.ts index 20d96690ac6..ad656dfbd4c 100644 --- a/packages/pds/src/api/app/bsky/feed/getLikes.ts +++ b/packages/pds/src/api/app/bsky/feed/getLikes.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did const res = await ctx.appViewAgent.api.app.bsky.feed.getLikes( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/feed/getListFeed.ts b/packages/pds/src/api/app/bsky/feed/getListFeed.ts index 7cc5d1f2bd2..06e2abcbfe5 100644 --- a/packages/pds/src/api/app/bsky/feed/getListFeed.ts +++ b/packages/pds/src/api/app/bsky/feed/getListFeed.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did const res = await ctx.appViewAgent.api.app.bsky.feed.getListFeed( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/feed/getPostThread.ts b/packages/pds/src/api/app/bsky/feed/getPostThread.ts index 93cd5fd641f..00bf0b01d82 100644 --- a/packages/pds/src/api/app/bsky/feed/getPostThread.ts +++ b/packages/pds/src/api/app/bsky/feed/getPostThread.ts @@ -44,7 +44,7 @@ export default function (server: Server, ctx: AppContext) { try { const res = await ctx.appViewAgent.api.app.bsky.feed.getPostThread( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return await handleReadAfterWrite( @@ -207,7 +207,7 @@ const readAfterWriteNotFound = async ( try { const parentsRes = await ctx.appViewAgent.api.app.bsky.feed.getPostThread( { uri: highestParent, parentHeight: params.parentHeight, depth: 0 }, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) thread.parent = parentsRes.data.thread } catch (err) { diff --git a/packages/pds/src/api/app/bsky/feed/getPosts.ts b/packages/pds/src/api/app/bsky/feed/getPosts.ts index 2f7a75d71fc..f04927a4997 100644 --- a/packages/pds/src/api/app/bsky/feed/getPosts.ts +++ b/packages/pds/src/api/app/bsky/feed/getPosts.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did const res = await ctx.appViewAgent.api.app.bsky.feed.getPosts( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts b/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts index b84a9dc27b7..e797967fc2a 100644 --- a/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts +++ b/packages/pds/src/api/app/bsky/feed/getRepostedBy.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did const res = await ctx.appViewAgent.api.app.bsky.feed.getRepostedBy( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts b/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts index 179570f6d4f..17fbf947471 100644 --- a/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts +++ b/packages/pds/src/api/app/bsky/feed/getSuggestedFeeds.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did const res = await ctx.appViewAgent.api.app.bsky.feed.getSuggestedFeeds( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/feed/getTimeline.ts b/packages/pds/src/api/app/bsky/feed/getTimeline.ts index d8515bcbfbb..6139432580a 100644 --- a/packages/pds/src/api/app/bsky/feed/getTimeline.ts +++ b/packages/pds/src/api/app/bsky/feed/getTimeline.ts @@ -14,7 +14,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did const res = await ctx.appViewAgent.api.app.bsky.feed.getTimeline( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return await handleReadAfterWrite(ctx, requester, res, getTimelineMunge) }, diff --git a/packages/pds/src/api/app/bsky/feed/searchPosts.ts b/packages/pds/src/api/app/bsky/feed/searchPosts.ts index 85384751ea1..e9942432fde 100644 --- a/packages/pds/src/api/app/bsky/feed/searchPosts.ts +++ b/packages/pds/src/api/app/bsky/feed/searchPosts.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did const res = await ctx.appViewAgent.api.app.bsky.feed.searchPosts( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/graph/getBlocks.ts b/packages/pds/src/api/app/bsky/graph/getBlocks.ts index 14aa3b3de5f..ff1f299f43f 100644 --- a/packages/pds/src/api/app/bsky/graph/getBlocks.ts +++ b/packages/pds/src/api/app/bsky/graph/getBlocks.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did const res = await ctx.appViewAgent.api.app.bsky.graph.getBlocks( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/graph/getFollowers.ts b/packages/pds/src/api/app/bsky/graph/getFollowers.ts index e3e5d8ba64e..18f1e0df397 100644 --- a/packages/pds/src/api/app/bsky/graph/getFollowers.ts +++ b/packages/pds/src/api/app/bsky/graph/getFollowers.ts @@ -10,7 +10,7 @@ export default function (server: Server, ctx: AppContext) { 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), + requester ? await ctx.appviewAuthHeaders(requester) : authPassthru(req), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/graph/getFollows.ts b/packages/pds/src/api/app/bsky/graph/getFollows.ts index 821b83fa359..91955965ed6 100644 --- a/packages/pds/src/api/app/bsky/graph/getFollows.ts +++ b/packages/pds/src/api/app/bsky/graph/getFollows.ts @@ -10,7 +10,7 @@ export default function (server: Server, ctx: AppContext) { 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), + requester ? await ctx.appviewAuthHeaders(requester) : authPassthru(req), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/graph/getList.ts b/packages/pds/src/api/app/bsky/graph/getList.ts index 6b4f3762d38..fb5776f5df2 100644 --- a/packages/pds/src/api/app/bsky/graph/getList.ts +++ b/packages/pds/src/api/app/bsky/graph/getList.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did const res = await ctx.appViewAgent.api.app.bsky.graph.getList( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/graph/getListBlocks.ts b/packages/pds/src/api/app/bsky/graph/getListBlocks.ts index 4e1ba8de8ca..376de0ba914 100644 --- a/packages/pds/src/api/app/bsky/graph/getListBlocks.ts +++ b/packages/pds/src/api/app/bsky/graph/getListBlocks.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did const res = await ctx.appViewAgent.api.app.bsky.graph.getListBlocks( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/graph/getListMutes.ts b/packages/pds/src/api/app/bsky/graph/getListMutes.ts index fc0d8c2f051..c489124642c 100644 --- a/packages/pds/src/api/app/bsky/graph/getListMutes.ts +++ b/packages/pds/src/api/app/bsky/graph/getListMutes.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did const res = await ctx.appViewAgent.api.app.bsky.graph.getListMutes( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/graph/getLists.ts b/packages/pds/src/api/app/bsky/graph/getLists.ts index 0c680321f9c..61a1cb89079 100644 --- a/packages/pds/src/api/app/bsky/graph/getLists.ts +++ b/packages/pds/src/api/app/bsky/graph/getLists.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did const res = await ctx.appViewAgent.api.app.bsky.graph.getLists( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/graph/getMutes.ts b/packages/pds/src/api/app/bsky/graph/getMutes.ts index 51177137b3a..0dc0e72412c 100644 --- a/packages/pds/src/api/app/bsky/graph/getMutes.ts +++ b/packages/pds/src/api/app/bsky/graph/getMutes.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did const res = await ctx.appViewAgent.api.app.bsky.graph.getMutes( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts b/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts index b8d578bfd25..0a11361f05c 100644 --- a/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts +++ b/packages/pds/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts @@ -9,7 +9,7 @@ export default function (server: Server, ctx: AppContext) { const res = await ctx.appViewAgent.api.app.bsky.graph.getSuggestedFollowsByActor( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/graph/muteActor.ts b/packages/pds/src/api/app/bsky/graph/muteActor.ts index 7e38d21ee42..2b2f218b44d 100644 --- a/packages/pds/src/api/app/bsky/graph/muteActor.ts +++ b/packages/pds/src/api/app/bsky/graph/muteActor.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did await ctx.appViewAgent.api.app.bsky.graph.muteActor(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), + ...(await ctx.appviewAuthHeaders(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 c224d85e3f3..97d524900f7 100644 --- a/packages/pds/src/api/app/bsky/graph/muteActorList.ts +++ b/packages/pds/src/api/app/bsky/graph/muteActorList.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did await ctx.appViewAgent.api.app.bsky.graph.muteActorList(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), + ...(await ctx.appviewAuthHeaders(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 37166420f31..0f7a1610321 100644 --- a/packages/pds/src/api/app/bsky/graph/unmuteActor.ts +++ b/packages/pds/src/api/app/bsky/graph/unmuteActor.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did await ctx.appViewAgent.api.app.bsky.graph.unmuteActor(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), + ...(await ctx.appviewAuthHeaders(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 0dea669feb6..aaf5225bded 100644 --- a/packages/pds/src/api/app/bsky/graph/unmuteActorList.ts +++ b/packages/pds/src/api/app/bsky/graph/unmuteActorList.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did await ctx.appViewAgent.api.app.bsky.graph.unmuteActorList(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), + ...(await ctx.appviewAuthHeaders(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 da8be8aec50..bca4bf3d46f 100644 --- a/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts +++ b/packages/pds/src/api/app/bsky/notification/getUnreadCount.ts @@ -9,7 +9,7 @@ export default function (server: Server, ctx: AppContext) { const res = await ctx.appViewAgent.api.app.bsky.notification.getUnreadCount( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/notification/listNotifications.ts b/packages/pds/src/api/app/bsky/notification/listNotifications.ts index 21f4e8db642..4a514ae03f4 100644 --- a/packages/pds/src/api/app/bsky/notification/listNotifications.ts +++ b/packages/pds/src/api/app/bsky/notification/listNotifications.ts @@ -9,7 +9,7 @@ export default function (server: Server, ctx: AppContext) { const res = await ctx.appViewAgent.api.app.bsky.notification.listNotifications( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/app/bsky/notification/updateSeen.ts b/packages/pds/src/api/app/bsky/notification/updateSeen.ts index 3678cacd37f..dc0217ffb67 100644 --- a/packages/pds/src/api/app/bsky/notification/updateSeen.ts +++ b/packages/pds/src/api/app/bsky/notification/updateSeen.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did await ctx.appViewAgent.api.app.bsky.notification.updateSeen(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), + ...(await ctx.appviewAuthHeaders(requester)), encoding: 'application/json', }) }, diff --git a/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts index 0d7eb366a03..08466ed9f5c 100644 --- a/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/pds/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -10,7 +10,7 @@ export default function (server: Server, ctx: AppContext) { const res = await ctx.appViewAgent.api.app.bsky.unspecced.getPopularFeedGenerators( params, - await ctx.serviceAuthHeaders(requester), + await ctx.appviewAuthHeaders(requester), ) return { encoding: 'application/json', diff --git a/packages/pds/src/api/com/atproto/admin/emitModerationEvent.ts b/packages/pds/src/api/com/atproto/admin/emitModerationEvent.ts index cea8a9fb664..b2befdd53cc 100644 --- a/packages/pds/src/api/com/atproto/admin/emitModerationEvent.ts +++ b/packages/pds/src/api/com/atproto/admin/emitModerationEvent.ts @@ -7,7 +7,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.authVerifier.role, handler: async ({ req, input }) => { const { data: result } = - await ctx.appViewAgent.com.atproto.admin.emitModerationEvent( + await ctx.moderationAgent.com.atproto.admin.emitModerationEvent( input.body, authPassthru(req, true), ) diff --git a/packages/pds/src/api/com/atproto/admin/getAccountInfo.ts b/packages/pds/src/api/com/atproto/admin/getAccountInfo.ts index 9953dc9d56b..7b94a8c57d8 100644 --- a/packages/pds/src/api/com/atproto/admin/getAccountInfo.ts +++ b/packages/pds/src/api/com/atproto/admin/getAccountInfo.ts @@ -1,15 +1,12 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { InvalidRequestError } from '@atproto/xrpc-server' -import { ensureValidAdminAud } from '../../../../auth-verifier' import { INVALID_HANDLE } from '@atproto/syntax' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getAccountInfo({ auth: ctx.authVerifier.roleOrAdminService, - handler: async ({ params, auth }) => { - // any admin role auth can get account info, but verify aud on service jwt - ensureValidAdminAud(auth, params.did) + handler: async ({ params }) => { const [account, invites, invitedBy] = await Promise.all([ ctx.accountManager.getAccount(params.did, true), ctx.accountManager.getAccountInvitesCodes(params.did), diff --git a/packages/pds/src/api/com/atproto/admin/getModerationEvent.ts b/packages/pds/src/api/com/atproto/admin/getModerationEvent.ts index 3ac6e0f72be..d368c3bfd72 100644 --- a/packages/pds/src/api/com/atproto/admin/getModerationEvent.ts +++ b/packages/pds/src/api/com/atproto/admin/getModerationEvent.ts @@ -7,7 +7,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.authVerifier.role, handler: async ({ req, params }) => { const { data } = - await ctx.appViewAgent.com.atproto.admin.getModerationEvent( + await ctx.moderationAgent.com.atproto.admin.getModerationEvent( params, authPassthru(req), ) diff --git a/packages/pds/src/api/com/atproto/admin/getRecord.ts b/packages/pds/src/api/com/atproto/admin/getRecord.ts index 9b6860ca1f2..90575354028 100644 --- a/packages/pds/src/api/com/atproto/admin/getRecord.ts +++ b/packages/pds/src/api/com/atproto/admin/getRecord.ts @@ -7,7 +7,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.authVerifier.role, handler: async ({ req, params }) => { const { data: recordDetailAppview } = - await ctx.appViewAgent.com.atproto.admin.getRecord( + await ctx.moderationAgent.com.atproto.admin.getRecord( params, authPassthru(req), ) diff --git a/packages/pds/src/api/com/atproto/admin/getRepo.ts b/packages/pds/src/api/com/atproto/admin/getRepo.ts index f70ddc7e0fe..85592c52b14 100644 --- a/packages/pds/src/api/com/atproto/admin/getRepo.ts +++ b/packages/pds/src/api/com/atproto/admin/getRepo.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getRepo({ auth: ctx.authVerifier.role, handler: async ({ req, params }) => { - const res = await ctx.appViewAgent.com.atproto.admin.getRepo( + const res = await ctx.moderationAgent.com.atproto.admin.getRepo( params, authPassthru(req), ) diff --git a/packages/pds/src/api/com/atproto/admin/getSubjectStatus.ts b/packages/pds/src/api/com/atproto/admin/getSubjectStatus.ts index ad212391c58..767714cec36 100644 --- a/packages/pds/src/api/com/atproto/admin/getSubjectStatus.ts +++ b/packages/pds/src/api/com/atproto/admin/getSubjectStatus.ts @@ -4,12 +4,11 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { OutputSchema } from '../../../../lexicon/types/com/atproto/admin/getSubjectStatus' -import { ensureValidAdminAud } from '../../../../auth-verifier' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getSubjectStatus({ auth: ctx.authVerifier.roleOrAdminService, - handler: async ({ params, auth }) => { + handler: async ({ params }) => { const { did, uri, blob } = params let body: OutputSchema | null = null if (blob) { @@ -18,7 +17,6 @@ export default function (server: Server, ctx: AppContext) { 'Must provide a did to request blob state', ) } - ensureValidAdminAud(auth, did) const takedown = await ctx.actorStore.read(did, (store) => store.repo.blob.getBlobTakedownStatus(CID.parse(blob)), ) @@ -34,7 +32,6 @@ export default function (server: Server, ctx: AppContext) { } } else if (uri) { const parsedUri = new AtUri(uri) - ensureValidAdminAud(auth, parsedUri.hostname) const [takedown, cid] = await ctx.actorStore.read( parsedUri.hostname, (store) => @@ -54,7 +51,6 @@ export default function (server: Server, ctx: AppContext) { } } } else if (did) { - ensureValidAdminAud(auth, did) const takedown = await ctx.accountManager.getAccountTakedownStatus(did) if (takedown) { body = { diff --git a/packages/pds/src/api/com/atproto/admin/queryModerationEvents.ts b/packages/pds/src/api/com/atproto/admin/queryModerationEvents.ts index 4ccb0ac9f6b..00e12439649 100644 --- a/packages/pds/src/api/com/atproto/admin/queryModerationEvents.ts +++ b/packages/pds/src/api/com/atproto/admin/queryModerationEvents.ts @@ -7,7 +7,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.authVerifier.role, handler: async ({ req, params }) => { const { data: result } = - await ctx.appViewAgent.com.atproto.admin.queryModerationEvents( + await ctx.moderationAgent.com.atproto.admin.queryModerationEvents( params, authPassthru(req), ) diff --git a/packages/pds/src/api/com/atproto/admin/queryModerationStatuses.ts b/packages/pds/src/api/com/atproto/admin/queryModerationStatuses.ts index 4f6c85e17d2..d2b2f36a1fe 100644 --- a/packages/pds/src/api/com/atproto/admin/queryModerationStatuses.ts +++ b/packages/pds/src/api/com/atproto/admin/queryModerationStatuses.ts @@ -7,7 +7,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.authVerifier.role, handler: async ({ req, params }) => { const { data } = - await ctx.appViewAgent.com.atproto.admin.queryModerationStatuses( + await ctx.moderationAgent.com.atproto.admin.queryModerationStatuses( params, authPassthru(req), ) diff --git a/packages/pds/src/api/com/atproto/admin/searchRepos.ts b/packages/pds/src/api/com/atproto/admin/searchRepos.ts index 4125b84eed9..5e21b2ab894 100644 --- a/packages/pds/src/api/com/atproto/admin/searchRepos.ts +++ b/packages/pds/src/api/com/atproto/admin/searchRepos.ts @@ -7,7 +7,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.authVerifier.role, handler: async ({ req, params }) => { const { data: result } = - await ctx.appViewAgent.com.atproto.admin.searchRepos( + await ctx.moderationAgent.com.atproto.admin.searchRepos( params, authPassthru(req), ) diff --git a/packages/pds/src/api/com/atproto/admin/updateSubjectStatus.ts b/packages/pds/src/api/com/atproto/admin/updateSubjectStatus.ts index 649f906cd7e..29991da2b2c 100644 --- a/packages/pds/src/api/com/atproto/admin/updateSubjectStatus.ts +++ b/packages/pds/src/api/com/atproto/admin/updateSubjectStatus.ts @@ -8,7 +8,6 @@ import { } from '../../../../lexicon/types/com/atproto/admin/defs' import { isMain as isStrongRef } from '../../../../lexicon/types/com/atproto/repo/strongRef' import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' -import { ensureValidAdminAud } from '../../../../auth-verifier' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.updateSubjectStatus({ @@ -24,16 +23,13 @@ export default function (server: Server, ctx: AppContext) { const { subject, takedown } = input.body if (takedown) { if (isRepoRef(subject)) { - ensureValidAdminAud(auth, subject.did) await ctx.accountManager.takedownAccount(subject.did, takedown) } else if (isStrongRef(subject)) { const uri = new AtUri(subject.uri) - ensureValidAdminAud(auth, uri.hostname) await ctx.actorStore.transact(uri.hostname, (store) => store.record.updateRecordTakedownStatus(uri, takedown), ) } else if (isRepoBlobRef(subject)) { - ensureValidAdminAud(auth, subject.did) await ctx.actorStore.transact(subject.did, (store) => store.repo.blob.updateBlobTakedownStatus( CID.parse(subject.cid), diff --git a/packages/pds/src/api/com/atproto/moderation/createReport.ts b/packages/pds/src/api/com/atproto/moderation/createReport.ts index 315b72c080a..7b3cc998e22 100644 --- a/packages/pds/src/api/com/atproto/moderation/createReport.ts +++ b/packages/pds/src/api/com/atproto/moderation/createReport.ts @@ -7,10 +7,13 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ input, auth }) => { const requester = auth.credentials.did const { data: result } = - await ctx.appViewAgent.com.atproto.moderation.createReport(input.body, { - ...(await ctx.serviceAuthHeaders(requester)), - encoding: 'application/json', - }) + await ctx.moderationAgent.com.atproto.moderation.createReport( + input.body, + { + ...(await ctx.moderationAuthHeaders(requester)), + encoding: 'application/json', + }, + ) return { encoding: 'application/json', body: result, diff --git a/packages/pds/src/auth-verifier.ts b/packages/pds/src/auth-verifier.ts index 7a09135a72c..dc5f0bc29d5 100644 --- a/packages/pds/src/auth-verifier.ts +++ b/packages/pds/src/auth-verifier.ts @@ -217,7 +217,7 @@ export class AuthVerifier { } const payload = await verifyServiceJwt( jwtStr, - null, + this.dids.entryway ?? this.dids.pds, async (did, forceRefresh) => { if (did !== this.dids.admin) { throw new AuthRequiredError( @@ -381,21 +381,6 @@ export const parseBasicAuth = ( return { username, password } } -export const ensureValidAdminAud = ( - auth: RoleOutput | AdminServiceOutput, - subjectDid: string, -) => { - if ( - auth.credentials.type === 'service' && - auth.credentials.aud !== subjectDid - ) { - throw new AuthRequiredError( - 'jwt audience does not match account did', - 'BadJwtAudience', - ) - } -} - const authScopes = new Set(Object.values(AuthScope)) const isAuthScope = (val: unknown): val is AuthScope => { return authScopes.has(val as any) diff --git a/packages/pds/src/config/config.ts b/packages/pds/src/config/config.ts index c1676c25908..2f8f295b2b0 100644 --- a/packages/pds/src/config/config.ts +++ b/packages/pds/src/config/config.ts @@ -167,18 +167,21 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { repoBackfillLimitMs: env.repoBackfillLimitMs ?? DAY, } - 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') - } + assert(env.bskyAppViewUrl) + assert(env.bskyAppViewDid) const bskyAppViewCfg: ServerConfig['bskyAppView'] = { url: env.bskyAppViewUrl, did: env.bskyAppViewDid, - proxyModeration: env.bskyAppViewModeration ?? false, cdnUrlPattern: env.bskyAppViewCdnUrlPattern, } + assert(env.modServiceUrl) + assert(env.modServiceDid) + const modServiceCfg: ServerConfig['modService'] = { + url: env.modServiceUrl, + did: env.modServiceDid, + } + const redisCfg: ServerConfig['redis'] = env.redisScratchAddress ? { address: env.redisScratchAddress, @@ -211,6 +214,7 @@ export const envToCfg = (env: ServerEnvironment): ServerConfig => { moderationEmail: moderationEmailCfg, subscription: subscriptionCfg, bskyAppView: bskyAppViewCfg, + modService: modServiceCfg, redis: redisCfg, rateLimits: rateLimitsCfg, crawlers: crawlersCfg, @@ -229,6 +233,7 @@ export type ServerConfig = { moderationEmail: EmailConfig | null subscription: SubscriptionConfig bskyAppView: BksyAppViewConfig + modService: ModServiceConfig redis: RedisScratchConfig | null rateLimits: RateLimitsConfig crawlers: string[] @@ -330,6 +335,10 @@ export type RateLimitsConfig = export type BksyAppViewConfig = { url: string did: string - proxyModeration: boolean cdnUrlPattern?: string } + +export type ModServiceConfig = { + url: string + did: string +} diff --git a/packages/pds/src/config/env.ts b/packages/pds/src/config/env.ts index bfcf4e36956..cc5f698fa80 100644 --- a/packages/pds/src/config/env.ts +++ b/packages/pds/src/config/env.ts @@ -1,4 +1,4 @@ -import { envInt, envStr, envBool, envList } from './util' +import { envInt, envStr, envBool, envList } from '@atproto/common' export const readEnv = (): ServerEnvironment => { return { @@ -69,9 +69,12 @@ export const readEnv = (): ServerEnvironment => { // appview 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'), + // mod service + modServiceUrl: envStr('PDS_MOD_SERVICE_URL'), + modServiceDid: envStr('PDS_MOD_SERVICE_DID'), + // rate limits rateLimitsEnabled: envBool('PDS_RATE_LIMITS_ENABLED'), rateLimitBypassKey: envStr('PDS_RATE_LIMIT_BYPASS_KEY'), @@ -165,9 +168,12 @@ export type ServerEnvironment = { // appview bskyAppViewUrl?: string bskyAppViewDid?: string - bskyAppViewModeration?: boolean bskyAppViewCdnUrlPattern?: string + // mod service + modServiceUrl?: string + modServiceDid?: string + // rate limits rateLimitsEnabled?: boolean rateLimitBypassKey?: string diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index 8f47992c008..6a5b2927df1 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -43,6 +43,7 @@ export type AppContextOptions = { redisScratch?: Redis crawlers: Crawlers appViewAgent: AtpAgent + moderationAgent: AtpAgent entrywayAgent?: AtpAgent authVerifier: AuthVerifier plcRotationKey: crypto.Keypair @@ -67,6 +68,7 @@ export class AppContext { public redisScratch?: Redis public crawlers: Crawlers public appViewAgent: AtpAgent + public moderationAgent: AtpAgent public entrywayAgent: AtpAgent | undefined public authVerifier: AuthVerifier public plcRotationKey: crypto.Keypair @@ -87,6 +89,7 @@ export class AppContext { this.redisScratch = opts.redisScratch this.crawlers = opts.crawlers this.appViewAgent = opts.appViewAgent + this.moderationAgent = opts.moderationAgent this.entrywayAgent = opts.entrywayAgent this.authVerifier = opts.authVerifier this.plcRotationKey = opts.plcRotationKey @@ -159,6 +162,7 @@ export class AppContext { : undefined const appViewAgent = new AtpAgent({ service: cfg.bskyAppView.url }) + const moderationAgent = new AtpAgent({ service: cfg.modService.url }) const entrywayAgent = cfg.entryway ? new AtpAgent({ service: cfg.entryway.url }) @@ -185,7 +189,7 @@ export class AppContext { dids: { pds: cfg.service.did, entryway: cfg.entryway?.did, - admin: cfg.bskyAppView.did, + admin: cfg.modService.did, }, }) @@ -226,6 +230,7 @@ export class AppContext { redisScratch, crawlers, appViewAgent, + moderationAgent, entrywayAgent, authVerifier, plcRotationKey, @@ -234,11 +239,15 @@ export class AppContext { }) } - async serviceAuthHeaders(did: string, audience?: string) { - const aud = audience ?? this.cfg.bskyAppView.did - if (!aud) { - throw new Error('Could not find bsky appview did') - } + async appviewAuthHeaders(did: string) { + return this.serviceAuthHeaders(did, this.cfg.bskyAppView.did) + } + + async moderationAuthHeaders(did: string) { + return this.serviceAuthHeaders(did, this.cfg.modService.did) + } + + async serviceAuthHeaders(did: string, aud: string) { const keypair = await this.actorStore.keypair(did) return createServiceAuthHeaders({ iss: did, diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 40c50cd1687..386f77196e7 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -15,6 +15,7 @@ import * as ComAtprotoAdminDisableInviteCodes from './types/com/atproto/admin/di import * as ComAtprotoAdminEmitModerationEvent from './types/com/atproto/admin/emitModerationEvent' import * as ComAtprotoAdminEnableAccountInvites from './types/com/atproto/admin/enableAccountInvites' import * as ComAtprotoAdminGetAccountInfo from './types/com/atproto/admin/getAccountInfo' +import * as ComAtprotoAdminGetAccountInfos from './types/com/atproto/admin/getAccountInfos' import * as ComAtprotoAdminGetInviteCodes from './types/com/atproto/admin/getInviteCodes' import * as ComAtprotoAdminGetModerationEvent from './types/com/atproto/admin/getModerationEvent' import * as ComAtprotoAdminGetRecord from './types/com/atproto/admin/getRecord' @@ -265,6 +266,17 @@ export class AdminNS { return this._server.xrpc.method(nsid, cfg) } + getAccountInfos( + cfg: ConfigOf< + AV, + ComAtprotoAdminGetAccountInfos.Handler>, + ComAtprotoAdminGetAccountInfos.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.admin.getAccountInfos' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + getInviteCodes( cfg: ConfigOf< AV, diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index c0e7e51fddc..258d297c69e 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -436,6 +436,12 @@ export const schemaDict = { email: { type: 'string', }, + relatedRecords: { + type: 'array', + items: { + type: 'unknown', + }, + }, indexedAt: { type: 'string', format: 'datetime', @@ -1046,6 +1052,45 @@ export const schemaDict = { }, }, }, + ComAtprotoAdminGetAccountInfos: { + lexicon: 1, + id: 'com.atproto.admin.getAccountInfos', + defs: { + main: { + type: 'query', + description: 'Get details about some accounts.', + parameters: { + type: 'params', + required: ['dids'], + properties: { + dids: { + type: 'array', + items: { + type: 'string', + format: 'did', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['infos'], + properties: { + infos: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.admin.defs#accountView', + }, + }, + }, + }, + }, + }, + }, + }, ComAtprotoAdminGetInviteCodes: { lexicon: 1, id: 'com.atproto.admin.getInviteCodes', @@ -7875,6 +7920,7 @@ export const ids = { ComAtprotoAdminEmitModerationEvent: 'com.atproto.admin.emitModerationEvent', ComAtprotoAdminEnableAccountInvites: 'com.atproto.admin.enableAccountInvites', ComAtprotoAdminGetAccountInfo: 'com.atproto.admin.getAccountInfo', + ComAtprotoAdminGetAccountInfos: 'com.atproto.admin.getAccountInfos', ComAtprotoAdminGetInviteCodes: 'com.atproto.admin.getInviteCodes', ComAtprotoAdminGetModerationEvent: 'com.atproto.admin.getModerationEvent', ComAtprotoAdminGetRecord: 'com.atproto.admin.getRecord', diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/defs.ts b/packages/pds/src/lexicon/types/com/atproto/admin/defs.ts index 4be9efb21a9..8236f848fa0 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/defs.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/defs.ts @@ -255,6 +255,7 @@ export interface AccountView { did: string handle: string email?: string + relatedRecords?: {}[] indexedAt: string invitedBy?: ComAtprotoServerDefs.InviteCode invites?: ComAtprotoServerDefs.InviteCode[] diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/getAccountInfos.ts b/packages/pds/src/lexicon/types/com/atproto/admin/getAccountInfos.ts new file mode 100644 index 00000000000..46d917293a8 --- /dev/null +++ b/packages/pds/src/lexicon/types/com/atproto/admin/getAccountInfos.ts @@ -0,0 +1,46 @@ +/** + * 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 ComAtprotoAdminDefs from './defs' + +export interface QueryParams { + dids: string[] +} + +export type InputSchema = undefined + +export interface OutputSchema { + infos: ComAtprotoAdminDefs.AccountView[] + [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 +} + +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/util/date.ts b/packages/pds/src/util/date.ts deleted file mode 100644 index af9767a0f7f..00000000000 --- a/packages/pds/src/util/date.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * This function takes a number as input and returns a Date object, - * which is the current date and time plus the input number of hours. - * - * @param {number} hours - The number of hours to add to the current date and time. - * @param {Date} startingDate - If provided, the function will add `hours` to the provided date instead of the current date. - * @returns {Date} - The new Date object, which is the current date and time plus the input number of hours. - */ -export function addHoursToDate(hours: number, startingDate?: Date): Date { - // When date is passe, let's clone before calling `setHours()` so that we are not mutating the original date - const currentDate = startingDate ? new Date(startingDate) : new Date() - currentDate.setHours(currentDate.getHours() + hours) - return currentDate -} diff --git a/packages/pds/tests/admin-auth.test.ts b/packages/pds/tests/admin-auth.test.ts new file mode 100644 index 00000000000..4cf7d5c26a5 --- /dev/null +++ b/packages/pds/tests/admin-auth.test.ts @@ -0,0 +1,145 @@ +import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env' +import AtpAgent from '@atproto/api' +import { Secp256k1Keypair } from '@atproto/crypto' +import { createServiceAuthHeaders } from '@atproto/xrpc-server' +import usersSeed from './seeds/users' +import { RepoRef } from '../src/lexicon/types/com/atproto/admin/defs' + +describe('admin auth', () => { + let network: TestNetworkNoAppView + let agent: AtpAgent + let sc: SeedClient + + let repoSubject: RepoRef + + const modServiceDid = 'did:example:mod' + const altModDid = 'did:example:alt' + let modServiceKey: Secp256k1Keypair + let pdsDid: string + + beforeAll(async () => { + network = await TestNetworkNoAppView.create({ + dbPostgresSchema: 'pds_admin_auth', + pds: { + modServiceDid, + }, + }) + + pdsDid = network.pds.ctx.cfg.service.did + + modServiceKey = await Secp256k1Keypair.create() + const origResolve = network.pds.ctx.idResolver.did.resolveAtprotoKey + network.pds.ctx.idResolver.did.resolveAtprotoKey = async ( + did: string, + forceRefresh?: boolean, + ) => { + if (did === modServiceDid || did === altModDid) { + return modServiceKey.did() + } + return origResolve(did, forceRefresh) + } + + agent = network.pds.getClient() + sc = network.getSeedClient() + await usersSeed(sc) + await network.processAll() + repoSubject = { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.bob, + } + }) + + afterAll(async () => { + await network.close() + }) + + it('allows service auth requests from the configured appview did', async () => { + const headers = await createServiceAuthHeaders({ + iss: modServiceDid, + aud: pdsDid, + keypair: modServiceKey, + }) + await agent.api.com.atproto.admin.updateSubjectStatus( + { + subject: repoSubject, + takedown: { applied: true, ref: 'test-repo' }, + }, + { + ...headers, + encoding: 'application/json', + }, + ) + + const res = await agent.api.com.atproto.admin.getSubjectStatus( + { + did: repoSubject.did, + }, + headers, + ) + expect(res.data.subject.did).toBe(repoSubject.did) + expect(res.data.takedown?.applied).toBe(true) + }) + + it('does not allow requests from another did', async () => { + const headers = await createServiceAuthHeaders({ + iss: altModDid, + aud: pdsDid, + keypair: modServiceKey, + }) + const attempt = agent.api.com.atproto.admin.updateSubjectStatus( + { + subject: repoSubject, + takedown: { applied: true, ref: 'test-repo' }, + }, + { + ...headers, + encoding: 'application/json', + }, + ) + await expect(attempt).rejects.toThrow('Untrusted issuer for admin actions') + }) + + it('does not allow requests with a bad signature', async () => { + const badKey = await Secp256k1Keypair.create() + const headers = await createServiceAuthHeaders({ + iss: modServiceDid, + aud: pdsDid, + keypair: badKey, + }) + const attempt = agent.api.com.atproto.admin.updateSubjectStatus( + { + subject: repoSubject, + takedown: { applied: true, ref: 'test-repo' }, + }, + { + ...headers, + encoding: 'application/json', + }, + ) + await expect(attempt).rejects.toThrow( + 'jwt signature does not match jwt issuer', + ) + }) + + it('does not allow requests with a bad aud', async () => { + // repo subject is bob, so we set alice as the audience + const headers = await createServiceAuthHeaders({ + iss: modServiceDid, + aud: sc.dids.alice, + keypair: modServiceKey, + }) + const attempt = agent.api.com.atproto.admin.updateSubjectStatus( + { + subject: repoSubject, + takedown: { applied: true, ref: 'test-repo' }, + }, + { + ...headers, + encoding: 'application/json', + }, + ) + await expect(attempt).rejects.toThrow( + 'jwt audience does not match service did', + ) + }) +}) diff --git a/packages/pds/tests/blob-deletes.test.ts b/packages/pds/tests/blob-deletes.test.ts index 019f6dec92f..e40883afc95 100644 --- a/packages/pds/tests/blob-deletes.test.ts +++ b/packages/pds/tests/blob-deletes.test.ts @@ -47,7 +47,7 @@ describe('blob deletes', () => { it('deletes blob when record is deleted', async () => { const img = await sc.uploadFile( alice, - 'tests/sample-img/key-portrait-small.jpg', + '../dev-env/src/seed/img/key-portrait-small.jpg', 'image/jpeg', ) const post = await sc.post(alice, 'test', undefined, [img]) @@ -64,12 +64,12 @@ describe('blob deletes', () => { it('deletes blob when blob-ref in record is updated', async () => { const img = await sc.uploadFile( alice, - 'tests/sample-img/key-portrait-small.jpg', + '../dev-env/src/seed/img/key-portrait-small.jpg', 'image/jpeg', ) const img2 = await sc.uploadFile( alice, - 'tests/sample-img/key-landscape-small.jpg', + '../dev-env/src/seed/img/key-landscape-small.jpg', 'image/jpeg', ) await updateProfile(sc, alice, img.image, img.image) @@ -93,12 +93,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/sample-img/key-portrait-small.jpg', + '../dev-env/src/seed/img/key-portrait-small.jpg', 'image/jpeg', ) const img2 = await sc.uploadFile( alice, - 'tests/sample-img/key-landscape-small.jpg', + '../dev-env/src/seed/img/key-landscape-small.jpg', 'image/jpeg', ) await updateProfile(sc, alice, img.image, img.image) @@ -119,7 +119,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/sample-img/key-portrait-small.jpg', + '../dev-env/src/seed/img/key-portrait-small.jpg', 'image/jpeg', ) const post = await sc.post(alice, 'post', undefined, [img]) @@ -166,12 +166,12 @@ describe('blob deletes', () => { it('does delete blob from user blob store if another user is using it', async () => { const imgAlice = await sc.uploadFile( alice, - 'tests/sample-img/key-landscape-small.jpg', + '../dev-env/src/seed/img/key-landscape-small.jpg', 'image/jpeg', ) const imgBob = await sc.uploadFile( bob, - 'tests/sample-img/key-landscape-small.jpg', + '../dev-env/src/seed/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 19470e38394..e675119dca2 100644 --- a/packages/pds/tests/crud.test.ts +++ b/packages/pds/tests/crud.test.ts @@ -173,7 +173,9 @@ describe('crud operations', () => { }) it('attaches images to a post', async () => { - const file = await fs.readFile('tests/sample-img/key-landscape-small.jpg') + const file = await fs.readFile( + '../dev-env/src/seed/img/key-landscape-small.jpg', + ) const uploadedRes = await aliceAgent.api.com.atproto.repo.uploadBlob(file, { encoding: 'image/jpeg', }) diff --git a/packages/pds/tests/file-uploads.test.ts b/packages/pds/tests/file-uploads.test.ts index fd4c7ad1a17..7cde753bfb0 100644 --- a/packages/pds/tests/file-uploads.test.ts +++ b/packages/pds/tests/file-uploads.test.ts @@ -69,7 +69,9 @@ describe('file uploads', () => { }) it('uploads files', async () => { - smallFile = await fs.readFile('tests/sample-img/key-portrait-small.jpg') + smallFile = await fs.readFile( + '../dev-env/src/seed/img/key-portrait-small.jpg', + ) const res = await agent.api.com.atproto.repo.uploadBlob(smallFile, { headers: sc.getHeaders(alice), encoding: 'image/jpeg', @@ -125,7 +127,7 @@ describe('file uploads', () => { let largeFile: Uint8Array it('does not allow referencing a file that is outside blob constraints', async () => { - largeFile = await fs.readFile('tests/sample-img/hd-key.jpg') + largeFile = await fs.readFile('../dev-env/src/seed/img/hd-key.jpg') const res = await agent.api.com.atproto.repo.uploadBlob(largeFile, { headers: sc.getHeaders(alice), encoding: 'image/jpeg', @@ -154,7 +156,9 @@ describe('file uploads', () => { }) it('permits duplicate uploads of the same file', async () => { - const file = await fs.readFile('tests/sample-img/key-landscape-small.jpg') + const file = await fs.readFile( + '../dev-env/src/seed/img/key-landscape-small.jpg', + ) const { data: uploadA } = await agent.api.com.atproto.repo.uploadBlob( file, { @@ -218,7 +222,9 @@ describe('file uploads', () => { }) it('corrects a bad mimetype', async () => { - const file = await fs.readFile('tests/sample-img/key-landscape-large.jpg') + const file = await fs.readFile( + '../dev-env/src/seed/img/key-landscape-large.jpg', + ) const res = await agent.api.com.atproto.repo.uploadBlob(file, { headers: sc.getHeaders(alice), encoding: 'video/mp4', @@ -236,7 +242,7 @@ describe('file uploads', () => { }) it('handles pngs', async () => { - const file = await fs.readFile('tests/sample-img/at.png') + const file = await fs.readFile('../dev-env/src/seed/img/at.png') const res = await agent.api.com.atproto.repo.uploadBlob(file, { headers: sc.getHeaders(alice), encoding: 'image/png', diff --git a/packages/pds/tests/moderation.test.ts b/packages/pds/tests/moderation.test.ts index 562795af581..ba58bcc70eb 100644 --- a/packages/pds/tests/moderation.test.ts +++ b/packages/pds/tests/moderation.test.ts @@ -1,8 +1,6 @@ import { TestNetworkNoAppView, ImageRef, SeedClient } from '@atproto/dev-env' import AtpAgent from '@atproto/api' import { BlobNotFoundError } from '@atproto/repo' -import { Secp256k1Keypair } from '@atproto/crypto' -import { createServiceAuthHeaders } from '@atproto/xrpc-server' import basicSeed from './seeds/basic' import { RepoBlobRef, @@ -20,30 +18,11 @@ describe('moderation', () => { let blobSubject: RepoBlobRef let blobRef: ImageRef - const appviewDid = 'did:example:appview' - const altAppviewDid = 'did:example:alt' - let appviewKey: Secp256k1Keypair - beforeAll(async () => { network = await TestNetworkNoAppView.create({ dbPostgresSchema: 'moderation', - pds: { - bskyAppViewDid: appviewDid, - }, }) - appviewKey = await Secp256k1Keypair.create() - const origResolve = network.pds.ctx.idResolver.did.resolveAtprotoKey - network.pds.ctx.idResolver.did.resolveAtprotoKey = async ( - did: string, - forceRefresh?: boolean, - ) => { - if (did === appviewDid || did === altAppviewDid) { - return appviewKey.did() - } - return origResolve(did, forceRefresh) - } - agent = network.pds.getClient() sc = network.getSeedClient() await basicSeed(sc) @@ -221,7 +200,7 @@ describe('moderation', () => { it('prevents blob from being referenced again.', async () => { const uploaded = await sc.uploadFile( sc.dids.carol, - 'tests/sample-img/key-alt.jpg', + '../dev-env/src/seed/img/key-alt.jpg', 'image/jpeg', ) expect(uploaded.image.ref.equals(blobRef.image.ref)).toBeTruthy() @@ -319,98 +298,4 @@ describe('moderation', () => { ) }) }) - - describe('auth', () => { - it('allows service auth requests from the configured appview did', async () => { - const headers = await createServiceAuthHeaders({ - iss: appviewDid, - aud: repoSubject.did, - keypair: appviewKey, - }) - await agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: repoSubject, - takedown: { applied: true, ref: 'test-repo' }, - }, - { - ...headers, - encoding: 'application/json', - }, - ) - - const res = await agent.api.com.atproto.admin.getSubjectStatus( - { - did: repoSubject.did, - }, - headers, - ) - expect(res.data.subject.did).toBe(repoSubject.did) - expect(res.data.takedown?.applied).toBe(true) - }) - - it('does not allow requests from another did', async () => { - const headers = await createServiceAuthHeaders({ - iss: altAppviewDid, - aud: repoSubject.did, - keypair: appviewKey, - }) - const attempt = agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: repoSubject, - takedown: { applied: true, ref: 'test-repo' }, - }, - { - ...headers, - encoding: 'application/json', - }, - ) - await expect(attempt).rejects.toThrow( - 'Untrusted issuer for admin actions', - ) - }) - - it('does not allow requests with a bad signature', async () => { - const badKey = await Secp256k1Keypair.create() - const headers = await createServiceAuthHeaders({ - iss: appviewDid, - aud: repoSubject.did, - keypair: badKey, - }) - const attempt = agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: repoSubject, - takedown: { applied: true, ref: 'test-repo' }, - }, - { - ...headers, - encoding: 'application/json', - }, - ) - await expect(attempt).rejects.toThrow( - 'jwt signature does not match jwt issuer', - ) - }) - - it('does not allow requests with a bad signature', async () => { - // repo subject is bob, so we set alice as the audience - const headers = await createServiceAuthHeaders({ - iss: appviewDid, - aud: sc.dids.alice, - keypair: appviewKey, - }) - const attempt = agent.api.com.atproto.admin.updateSubjectStatus( - { - subject: repoSubject, - takedown: { applied: true, ref: 'test-repo' }, - }, - { - ...headers, - encoding: 'application/json', - }, - ) - await expect(attempt).rejects.toThrow( - 'jwt audience does not match account did', - ) - }) - }) }) diff --git a/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap index 85e2e3b99e5..5e9b4e0882a 100644 --- a/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap @@ -4,7 +4,7 @@ exports[`proxies admin requests creates reports of a repo. 1`] = ` Array [ Object { "createdAt": "1970-01-01T00:00:00.000Z", - "id": 2, + "id": 4, "reasonType": "com.atproto.moderation.defs#reasonSpam", "reportedBy": "user(0)", "subject": Object { @@ -14,7 +14,7 @@ Array [ }, Object { "createdAt": "1970-01-01T00:00:00.000Z", - "id": 3, + "id": 5, "reason": "impersonation", "reasonType": "com.atproto.moderation.defs#reasonOther", "reportedBy": "user(2)", @@ -34,7 +34,7 @@ Array [ "event": Object { "$type": "com.atproto.admin.defs#modEventAcknowledge", }, - "id": 5, + "id": 7, "subject": Object { "$type": "com.atproto.admin.defs#repoRef", "did": "user(0)", @@ -51,7 +51,7 @@ Array [ "comment": "impersonation", "reportType": "com.atproto.moderation.defs#reasonOther", }, - "id": 3, + "id": 5, "subject": Object { "$type": "com.atproto.admin.defs#repoRef", "did": "user(0)", @@ -67,7 +67,7 @@ Array [ "$type": "com.atproto.admin.defs#modEventReport", "reportType": "com.atproto.moderation.defs#reasonSpam", }, - "id": 2, + "id": 4, "subject": Object { "$type": "com.atproto.admin.defs#repoRef", "did": "user(0)", @@ -83,48 +83,93 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "createdBy": "user(1)", "event": Object { - "$type": "com.atproto.admin.defs#modEventReport", - "reportType": "com.atproto.moderation.defs#reasonSpam", + "$type": "com.atproto.admin.defs#modEventLabel", + "comment": "[AutoModerator]: Applying labels", + "createLabelVals": Array [ + "test-label", + "test-label-2", + ], + "negateLabelVals": Array [], }, "id": 2, "subject": Object { - "$type": "com.atproto.admin.defs#repoView", - "did": "user(0)", - "handle": "bob.test", + "$type": "com.atproto.admin.defs#recordView", + "blobCids": Array [ + "cids(1)", + ], + "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", - "moderation": Object { - "subjectStatus": Object { - "createdAt": "1970-01-01T00:00:00.000Z", - "id": 1, - "lastReportedAt": "1970-01-01T00:00:00.000Z", - "lastReviewedAt": "1970-01-01T00:00:00.000Z", - "lastReviewedBy": "did:example:admin", - "reviewState": "com.atproto.admin.defs#reviewClosed", - "subject": Object { - "$type": "com.atproto.admin.defs#repoRef", - "did": "user(0)", + "moderation": Object {}, + "repo": Object { + "did": "user(0)", + "handle": "bob.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "moderation": Object { + "subjectStatus": Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "id": 1, + "lastReportedAt": "1970-01-01T00:00:00.000Z", + "lastReviewedAt": "1970-01-01T00:00:00.000Z", + "lastReviewedBy": "did:example:admin", + "reviewState": "com.atproto.admin.defs#reviewClosed", + "subject": Object { + "$type": "com.atproto.admin.defs#repoRef", + "did": "user(0)", + }, + "subjectBlobCids": Array [], + "subjectRepoHandle": "bob.test", + "takendown": false, + "updatedAt": "1970-01-01T00:00:00.000Z", }, - "subjectBlobCids": Array [], - "subjectRepoHandle": "bob.test", - "takendown": false, - "updatedAt": "1970-01-01T00:00:00.000Z", }, + "relatedRecords": Array [ + Object { + "$type": "app.bsky.actor.profile", + "avatar": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(3)", + }, + "size": 3976, + }, + "description": "hi im bob label_me", + "displayName": "bobby", + }, + ], }, - "relatedRecords": Array [ - Object { - "$type": "app.bsky.actor.profile", - "avatar": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(0)", + "uri": "record(0)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(1)", + }, + "size": 4114, + }, }, - "size": 3976, + ], + }, + "reply": Object { + "parent": Object { + "cid": "cids(2)", + "uri": "record(1)", + }, + "root": Object { + "cid": "cids(2)", + "uri": "record(1)", }, - "description": "hi im bob label_me", - "displayName": "bobby", }, - ], + "text": "hear that label_me label_me_2", + }, }, "subjectBlobCids": Array [], "subjectBlobs": Array [], @@ -139,7 +184,7 @@ Array [ "event": Object { "$type": "com.atproto.admin.defs#modEventAcknowledge", }, - "id": 4, + "id": 6, "subject": Object { "$type": "com.atproto.repo.strongRef", "cid": "cids(0)", @@ -323,7 +368,7 @@ Object { "event": Object { "$type": "com.atproto.admin.defs#modEventAcknowledge", }, - "id": 4, + "id": 6, "subject": Object { "$type": "com.atproto.repo.strongRef", "cid": "cids(0)", @@ -340,7 +385,7 @@ Object { "event": Object { "$type": "com.atproto.admin.defs#modEventAcknowledge", }, - "id": 5, + "id": 7, "subject": Object { "$type": "com.atproto.admin.defs#repoRef", "did": "user(0)", diff --git a/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap index 107ae5667be..1ece8243e57 100644 --- a/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap @@ -79,7 +79,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -91,7 +91,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(3)", "val": "test-label", }, @@ -99,7 +99,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(3)", "val": "test-label-2", }, @@ -112,7 +112,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -190,7 +190,7 @@ Object { Object { "post": Object { "author": Object { - "did": "user(4)", + "did": "user(5)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -335,7 +335,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -347,7 +347,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(3)", "val": "test-label", }, @@ -355,7 +355,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(3)", "val": "test-label-2", }, @@ -368,7 +368,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -524,14 +524,14 @@ Object { "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(5)", + "did": "user(6)", "handle": "dan.test", "labels": Array [ Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", - "uri": "user(5)", + "src": "user(4)", + "uri": "user(6)", "val": "repo-action-label", }, ], @@ -548,7 +548,7 @@ Object { "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(4)", + "did": "user(5)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -571,7 +571,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -582,7 +582,7 @@ Object { }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -643,7 +643,7 @@ Object { "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(9)", "val": "test-label", }, @@ -745,7 +745,7 @@ Object { Object { "post": Object { "author": Object { - "did": "user(4)", + "did": "user(5)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -762,14 +762,14 @@ 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(6)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(5)@jpeg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(5)@jpeg", }, Object { - "alt": "tests/sample-img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(10)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(10)@jpeg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(10)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(10)@jpeg", }, ], }, @@ -818,7 +818,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -829,7 +829,7 @@ Object { }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/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 165257220d2..73781d5435a 100644 --- a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap @@ -103,7 +103,7 @@ Object { Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "user(2)", "val": "repo-action-label", }, @@ -183,7 +183,7 @@ Array [ Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(6)", "uri": "user(5)", "val": "repo-action-label", }, @@ -258,7 +258,7 @@ Array [ Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(6)", "uri": "user(5)", "val": "repo-action-label", }, @@ -296,7 +296,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -308,7 +308,7 @@ Object { "cid": "cids(0)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(2)", "uri": "record(0)", "val": "test-label", }, @@ -316,7 +316,7 @@ Object { "cid": "cids(0)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(2)", "uri": "record(0)", "val": "test-label-2", }, @@ -329,7 +329,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -362,8 +362,8 @@ 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)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", + "did": "user(3)", "displayName": "ali", "handle": "alice.test", "labels": Array [ @@ -371,7 +371,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(2)", + "src": "user(3)", "uri": "record(4)", "val": "self-label-a", }, @@ -379,7 +379,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(2)", + "src": "user(3)", "uri": "record(4)", "val": "self-label-b", }, @@ -406,8 +406,8 @@ Object { "root": 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)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", + "did": "user(3)", "displayName": "ali", "handle": "alice.test", "labels": Array [ @@ -415,7 +415,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(2)", + "src": "user(3)", "uri": "record(4)", "val": "self-label-a", }, @@ -423,7 +423,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(2)", + "src": "user(3)", "uri": "record(4)", "val": "self-label-b", }, @@ -746,7 +746,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -758,7 +758,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(3)", "val": "test-label", }, @@ -766,7 +766,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(3)", "val": "test-label-2", }, @@ -779,7 +779,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -874,7 +874,7 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/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", }, @@ -886,7 +886,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(3)", "val": "test-label", }, @@ -894,7 +894,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(3)", "val": "test-label-2", }, @@ -907,7 +907,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1063,14 +1063,14 @@ Object { "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(4)", + "did": "user(5)", "handle": "dan.test", "labels": Array [ Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", - "uri": "user(4)", + "src": "user(4)", + "uri": "user(5)", "val": "repo-action-label", }, ], @@ -1087,7 +1087,7 @@ Object { "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(5)", + "did": "user(6)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -1110,7 +1110,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1121,7 +1121,7 @@ Object { }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1182,7 +1182,7 @@ Object { "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(4)", "uri": "record(6)", "val": "test-label", }, @@ -1435,12 +1435,12 @@ Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(3)@jpeg", "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(3)@jpeg", }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.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", }, @@ -1491,7 +1491,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1502,7 +1502,7 @@ Object { }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1621,7 +1621,7 @@ Object { Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "user(2)", "val": "repo-action-label", }, @@ -1638,8 +1638,8 @@ Object { "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -1655,9 +1655,9 @@ 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(4)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(5)@jpeg", + "alt": "../dev-env/src/seed/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", }, ], }, @@ -1667,7 +1667,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(3)", "val": "test-label", }, @@ -1675,7 +1675,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(3)", "val": "test-label-2", }, @@ -1688,7 +1688,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1816,7 +1816,7 @@ Object { Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "user(2)", "val": "repo-action-label", }, @@ -1839,7 +1839,7 @@ Object { Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "user(2)", "val": "repo-action-label", }, @@ -1856,7 +1856,7 @@ Object { "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(5)", + "did": "user(6)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -1874,14 +1874,14 @@ 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(6)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(5)@jpeg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(5)@jpeg", }, Object { - "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", + "alt": "../dev-env/src/seed/img/key-alt.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(8)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(8)@jpeg", }, ], }, @@ -1889,8 +1889,8 @@ Object { "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -1930,7 +1930,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -1941,7 +1941,7 @@ Object { }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2001,7 +2001,7 @@ Object { "reason": Object { "$type": "app.bsky.feed.defs#reasonRepost", "by": Object { - "did": "user(5)", + "did": "user(6)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -2072,8 +2072,8 @@ Object { "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -2089,9 +2089,9 @@ 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(4)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(5)@jpeg", + "alt": "../dev-env/src/seed/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", }, ], }, @@ -2101,7 +2101,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(3)", "val": "test-label", }, @@ -2109,7 +2109,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(3)", "val": "test-label-2", }, @@ -2122,7 +2122,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2200,7 +2200,7 @@ Object { Object { "post": Object { "author": Object { - "did": "user(5)", + "did": "user(6)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -2328,8 +2328,8 @@ Object { Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -2345,9 +2345,9 @@ 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(4)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(5)@jpeg", + "alt": "../dev-env/src/seed/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", }, ], }, @@ -2357,7 +2357,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(3)", "val": "test-label", }, @@ -2365,7 +2365,7 @@ Object { "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(3)", "val": "test-label-2", }, @@ -2378,7 +2378,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2540,7 +2540,7 @@ Object { Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "user(2)", "val": "repo-action-label", }, @@ -2558,7 +2558,7 @@ Object { "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(5)", + "did": "user(6)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -2581,7 +2581,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2592,7 +2592,7 @@ Object { }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2653,7 +2653,7 @@ Object { "cid": "cids(11)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "record(13)", "val": "test-label", }, @@ -2680,8 +2680,8 @@ Object { Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -2761,7 +2761,7 @@ Object { Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "user(2)", "val": "repo-action-label", }, @@ -2778,7 +2778,7 @@ Object { "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(5)", + "did": "user(6)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -2796,14 +2796,14 @@ 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(6)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(5)@jpeg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(5)@jpeg", }, Object { - "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", + "alt": "../dev-env/src/seed/img/key-alt.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(8)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(8)@jpeg", }, ], }, @@ -2811,8 +2811,8 @@ Object { "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -2852,7 +2852,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2863,7 +2863,7 @@ Object { }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -2930,7 +2930,7 @@ Object { Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(3)", "uri": "user(2)", "val": "repo-action-label", }, @@ -2959,7 +2959,7 @@ Object { Object { "post": Object { "author": Object { - "did": "user(5)", + "did": "user(6)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -2976,14 +2976,14 @@ 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(6)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(5)@jpeg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(5)@jpeg", }, Object { - "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", + "alt": "../dev-env/src/seed/img/key-alt.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(7)/cids(8)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(7)/cids(8)@jpeg", }, ], }, @@ -2991,8 +2991,8 @@ Object { "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -3032,7 +3032,7 @@ Object { "$type": "app.bsky.embed.images", "images": Array [ Object { - "alt": "tests/sample-img/key-landscape-small.jpg", + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -3043,7 +3043,7 @@ Object { }, }, Object { - "alt": "tests/sample-img/key-alt.jpg", + "alt": "../dev-env/src/seed/img/key-alt.jpg", "image": Object { "$type": "blob", "mimeType": "image/jpeg", @@ -3075,8 +3075,8 @@ Object { Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -3215,7 +3215,7 @@ Object { Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "did:example:labeler", + "src": "user(1)", "uri": "user(0)", "val": "repo-action-label", }, @@ -3227,9 +3227,9 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "its me!", - "did": "user(1)", + "did": "user(2)", "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -3238,7 +3238,7 @@ Object { "cid": "cids(1)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(1)", + "src": "user(2)", "uri": "record(1)", "val": "self-label-a", }, @@ -3246,7 +3246,7 @@ Object { "cid": "cids(1)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(1)", + "src": "user(2)", "uri": "record(1)", "val": "self-label-b", }, @@ -3258,9 +3258,9 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", "description": "hi im bob label_me", - "did": "user(3)", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", diff --git a/packages/pds/tests/proxied/admin.test.ts b/packages/pds/tests/proxied/admin.test.ts index fd8538e802a..9906e4d129a 100644 --- a/packages/pds/tests/proxied/admin.test.ts +++ b/packages/pds/tests/proxied/admin.test.ts @@ -17,8 +17,6 @@ describe('proxies admin requests', () => { network = await TestNetwork.create({ dbPostgresSchema: 'proxy_admin', pds: { - // @NOTE requires admin pass be the same on pds and appview, which TestNetwork is handling for us. - bskyAppViewModeration: true, inviteRequired: true, }, }) @@ -222,6 +220,7 @@ describe('proxies admin requests', () => { encoding: 'application/json', }, ) + await network.processAll() // check profile and labels const tryGetProfileAppview = agent.api.app.bsky.actor.getProfile( { actor: sc.dids.alice }, @@ -250,6 +249,7 @@ describe('proxies admin requests', () => { encoding: 'application/json', }, ) + await network.processAll() // check profile and labels const { data: profileAppview } = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.alice }, @@ -283,6 +283,7 @@ describe('proxies admin requests', () => { encoding: 'application/json', }, ) + await network.processAll() // check thread and labels const tryGetPost = agent.api.app.bsky.feed.getPostThread( { uri: post.ref.uriStr, depth: 0 }, @@ -306,6 +307,7 @@ describe('proxies admin requests', () => { encoding: 'application/json', }, ) + await network.processAll() // check thread and labels const { data: threadAppview } = await agent.api.app.bsky.feed.getPostThread( { uri: post.ref.uriStr, depth: 0 }, diff --git a/packages/pds/tests/proxied/read-after-write.test.ts b/packages/pds/tests/proxied/read-after-write.test.ts index 52507a2730a..ca01b135c5b 100644 --- a/packages/pds/tests/proxied/read-after-write.test.ts +++ b/packages/pds/tests/proxied/read-after-write.test.ts @@ -45,7 +45,7 @@ describe('proxy read after write', () => { it('handles image formatting', async () => { const blob = await sc.uploadFile( alice, - 'tests/sample-img/key-landscape-small.jpg', + '../dev-env/src/seed/img/key-landscape-small.jpg', 'image/jpeg', ) await sc.updateProfile(alice, { displayName: 'blah', avatar: blob.image }) @@ -125,7 +125,7 @@ describe('proxy read after write', () => { it('handles read after write on threads with record embeds', async () => { const img = await sc.uploadFile( alice, - 'tests/sample-img/key-landscape-small.jpg', + '../dev-env/src/seed/img/key-landscape-small.jpg', 'image/jpeg', ) const replyRes1 = await agent.api.app.bsky.feed.post.create( diff --git a/packages/pds/tests/sample-img/at.png b/packages/pds/tests/sample-img/at.png deleted file mode 100644 index c5ee42f3a92..00000000000 Binary files a/packages/pds/tests/sample-img/at.png and /dev/null differ diff --git a/packages/pds/tests/sample-img/hd-key.jpg b/packages/pds/tests/sample-img/hd-key.jpg deleted file mode 100644 index cd0a4a79a12..00000000000 Binary files a/packages/pds/tests/sample-img/hd-key.jpg and /dev/null differ diff --git a/packages/pds/tests/sample-img/key-alt.jpg b/packages/pds/tests/sample-img/key-alt.jpg deleted file mode 100644 index 6dd89e602e8..00000000000 Binary files a/packages/pds/tests/sample-img/key-alt.jpg and /dev/null differ diff --git a/packages/pds/tests/sample-img/key-landscape-large.jpg b/packages/pds/tests/sample-img/key-landscape-large.jpg deleted file mode 100644 index 40d1be748be..00000000000 Binary files a/packages/pds/tests/sample-img/key-landscape-large.jpg and /dev/null differ diff --git a/packages/pds/tests/sample-img/key-landscape-small.jpg b/packages/pds/tests/sample-img/key-landscape-small.jpg deleted file mode 100644 index f449e0b4949..00000000000 Binary files a/packages/pds/tests/sample-img/key-landscape-small.jpg and /dev/null differ diff --git a/packages/pds/tests/sample-img/key-portrait-large.jpg b/packages/pds/tests/sample-img/key-portrait-large.jpg deleted file mode 100644 index 39e7f7ae844..00000000000 Binary files a/packages/pds/tests/sample-img/key-portrait-large.jpg and /dev/null differ diff --git a/packages/pds/tests/sample-img/key-portrait-small.jpg b/packages/pds/tests/sample-img/key-portrait-small.jpg deleted file mode 100644 index 6e457191eab..00000000000 Binary files a/packages/pds/tests/sample-img/key-portrait-small.jpg and /dev/null differ diff --git a/packages/pds/tests/seeds/basic.ts b/packages/pds/tests/seeds/basic.ts index 1085e2b381e..1590fda3d05 100644 --- a/packages/pds/tests/seeds/basic.ts +++ b/packages/pds/tests/seeds/basic.ts @@ -37,12 +37,12 @@ export default async ( }) const img1 = await sc.uploadFile( carol, - 'tests/sample-img/key-landscape-small.jpg', + '../dev-env/src/seed/img/key-landscape-small.jpg', 'image/jpeg', ) const img2 = await sc.uploadFile( carol, - 'tests/sample-img/key-alt.jpg', + '../dev-env/src/seed/img/key-alt.jpg', 'image/jpeg', ) await sc.post( @@ -103,7 +103,7 @@ export default async ( const replyImg = await sc.uploadFile( bob, - 'tests/sample-img/key-landscape-small.jpg', + '../dev-env/src/seed/img/key-landscape-small.jpg', 'image/jpeg', ) // must ensure ordering of replies in indexing diff --git a/packages/pds/tests/transfer-repo.test.ts b/packages/pds/tests/transfer-repo.test.ts index f2e6dca0bfd..53393fa89d1 100644 --- a/packages/pds/tests/transfer-repo.test.ts +++ b/packages/pds/tests/transfer-repo.test.ts @@ -75,7 +75,7 @@ describe('transfer repo', () => { } const img = await entrywaySc.uploadFile( did, - 'tests/sample-img/key-landscape-small.jpg', + '../dev-env/src/seed/img/key-landscape-small.jpg', 'image/jpeg', ) await entrywaySc.post(did, 'img post', undefined, [img]) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 860cce537ad..dc2fd699490 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -356,6 +356,9 @@ importers: '@atproto/lexicon': specifier: workspace:^ version: link:../lexicon + '@atproto/ozone': + specifier: workspace:^ + version: link:../ozone '@atproto/pds': specifier: workspace:^ version: link:../pds @@ -401,7 +404,7 @@ importers: devDependencies: ts-node: specifier: ^10.8.1 - version: 10.8.2(@swc/core@1.3.42)(@types/node@20.10.4)(typescript@5.3.3) + version: 10.8.2(@swc/core@1.3.42)(@types/node@18.17.8)(typescript@4.8.4) packages/identity: dependencies: @@ -473,6 +476,106 @@ importers: specifier: ^3.21.4 version: 3.21.4 + packages/ozone: + dependencies: + '@atproto/api': + specifier: workspace:^ + version: link:../api + '@atproto/common': + specifier: workspace:^ + version: link:../common + '@atproto/crypto': + specifier: workspace:^ + version: link:../crypto + '@atproto/identity': + specifier: workspace:^ + version: link:../identity + '@atproto/lexicon': + specifier: workspace:^ + version: link:../lexicon + '@atproto/syntax': + specifier: workspace:^ + version: link:../syntax + '@atproto/xrpc-server': + specifier: workspace:^ + version: link:../xrpc-server + '@did-plc/lib': + specifier: ^0.0.1 + version: 0.0.1 + compression: + specifier: ^1.7.4 + version: 1.7.4 + cors: + specifier: ^2.8.5 + version: 2.8.5 + express: + specifier: ^4.17.2 + version: 4.18.2 + express-async-errors: + specifier: ^3.1.1 + version: 3.1.1(express@4.18.2) + http-terminator: + specifier: ^3.2.0 + version: 3.2.0 + kysely: + specifier: ^0.22.0 + version: 0.22.0 + multiformats: + specifier: ^9.9.0 + version: 9.9.0 + p-queue: + specifier: ^6.6.2 + version: 6.6.2 + pg: + specifier: ^8.10.0 + version: 8.10.0 + pino: + specifier: ^8.15.0 + version: 8.15.0 + pino-http: + specifier: ^8.2.1 + version: 8.4.0 + typed-emitter: + specifier: ^2.1.0 + version: 2.1.0 + uint8arrays: + specifier: 3.0.0 + version: 3.0.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 + '@atproto/xrpc': + specifier: workspace:^ + version: link:../xrpc + '@did-plc/server': + specifier: ^0.0.1 + version: 0.0.1 + '@types/cors': + specifier: ^2.8.12 + version: 2.8.12 + '@types/express': + specifier: ^4.17.13 + version: 4.17.13 + '@types/express-serve-static-core': + specifier: ^4.17.36 + version: 4.17.36 + '@types/pg': + specifier: ^8.6.6 + version: 8.6.6 + '@types/qs': + specifier: ^6.9.7 + version: 6.9.7 + axios: + specifier: ^0.27.2 + version: 0.27.2 + packages/pds: dependencies: '@atproto/api': @@ -763,6 +866,15 @@ importers: specifier: 3.13.2 version: 3.13.2 + services/ozone: + dependencies: + '@atproto/ozone': + specifier: workspace:^ + version: link:../../packages/ozone + dd-trace: + specifier: 3.13.2 + version: 3.13.2 + services/pds: dependencies: '@atproto/aws': @@ -782,7 +894,7 @@ importers: version: 4.20.0 opentelemetry-plugin-better-sqlite3: specifier: ^1.1.0 - version: 1.1.0(better-sqlite3@9.2.2) + version: 1.1.0(better-sqlite3@7.6.2) packages: @@ -861,7 +973,7 @@ packages: p-queue: 6.6.2 pg: 8.10.0 pino: 8.15.0 - pino-http: 8.2.1 + pino-http: 8.4.0 sharp: 0.31.3 typed-emitter: 2.1.0 uint8arrays: 3.0.0 @@ -5587,12 +5699,6 @@ packages: /@types/node@18.17.8: resolution: {integrity: sha512-Av/7MqX/iNKwT9Tr60V85NqMnsmh8ilfJoBlIVibkXfitk9Q22D9Y5mSpm+FvG5DET7EbVfB40bOiLzKgYFgPw==} - /@types/node@20.10.4: - resolution: {integrity: sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==} - dependencies: - undici-types: 5.26.5 - dev: true - /@types/nodemailer@6.4.6: resolution: {integrity: sha512-pD6fL5GQtUKvD2WnPmg5bC2e8kWCAPDwMPmHe/ohQbW+Dy0EcHgZ2oCSuPlWNqk74LS5BVMig1SymQbFMPPK3w==} dependencies: @@ -6200,14 +6306,6 @@ packages: bindings: 1.5.0 prebuild-install: 7.1.1 - /better-sqlite3@9.2.2: - resolution: {integrity: sha512-qwjWB46il0lsDkeB4rSRI96HyDQr8sxeu1MkBVLMrwusq1KRu4Bpt1TMI+8zIJkDUtZ3umjAkaEjIlokZKWCQw==} - requiresBuild: true - dependencies: - bindings: 1.5.0 - prebuild-install: 7.1.1 - dev: false - /big-integer@1.6.51: resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==} engines: {node: '>=0.6'} @@ -7658,6 +7756,7 @@ packages: resolution: {integrity: sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==} dependencies: punycode: 1.4.1 + dev: false /fast-xml-parser@4.0.11: resolution: {integrity: sha512-4aUg3aNRR/WjQAcpceODG1C3x3lFANXRo8+1biqfieHmg9pyMt7qB4lQV/Ta6sJCTbA5vfD8fnA8S54JATiFUA==} @@ -9626,7 +9725,7 @@ packages: mimic-fn: 2.1.0 dev: true - /opentelemetry-plugin-better-sqlite3@1.1.0(better-sqlite3@9.2.2): + /opentelemetry-plugin-better-sqlite3@1.1.0(better-sqlite3@7.6.2): resolution: {integrity: sha512-yd+mgaB5W5JxzcQt9TvX1VIrusqtbbeuxSoZ6KQe4Ra0J/Kqkp6kz7dg0VQUU5+cenOWkza6xtvsT0KGXI03HA==} peerDependencies: better-sqlite3: ^7.1.1 || ^8.0.0 || ^9.0.0 @@ -9635,7 +9734,7 @@ packages: '@opentelemetry/core': 1.18.1(@opentelemetry/api@1.7.0) '@opentelemetry/instrumentation': 0.44.0(@opentelemetry/api@1.7.0) '@opentelemetry/semantic-conventions': 1.18.1 - better-sqlite3: 9.2.2 + better-sqlite3: 7.6.2 transitivePeerDependencies: - supports-color dev: false @@ -9879,6 +9978,7 @@ packages: pino: 8.15.0 pino-std-serializers: 6.2.2 process-warning: 2.2.0 + dev: false /pino-http@8.4.0: resolution: {integrity: sha512-9I1eRLxsujQJwLQTrHBU0wDlwnry2HzV2TlDwAsmZ9nT3Y2NQBLrz+DYp73L4i11vl/eudnFT8Eg0Kp62tMwEw==} @@ -10098,6 +10198,7 @@ packages: /punycode@1.4.1: resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + dev: false /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} @@ -11012,7 +11113,7 @@ packages: yn: 3.1.1 dev: true - /ts-node@10.8.2(@swc/core@1.3.42)(@types/node@20.10.4)(typescript@5.3.3): + /ts-node@10.8.2(@swc/core@1.3.42)(@types/node@18.17.8)(typescript@4.8.4): resolution: {integrity: sha512-LYdGnoGddf1D6v8REPtIH+5iq/gTDuZqv2/UJUU7tKjuEU8xVZorBM+buCGNjj+pGEud+sOoM4CX3/YzINpENA==} hasBin: true peerDependencies: @@ -11032,14 +11133,14 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.10.4 + '@types/node': 18.17.8 acorn: 8.10.0 acorn-walk: 8.2.0 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.3.3 + typescript: 4.8.4 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 dev: true @@ -11181,12 +11282,6 @@ packages: hasBin: true dev: true - /typescript@5.3.3: - resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} - engines: {node: '>=14.17'} - hasBin: true - dev: true - /uglify-js@3.17.4: resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} engines: {node: '>=0.8.0'} @@ -11208,10 +11303,6 @@ packages: which-boxed-primitive: 1.0.2 dev: true - /undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - dev: true - /unicode-canonical-property-names-ecmascript@2.0.0: resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} engines: {node: '>=4'} diff --git a/services/bsky/api.js b/services/bsky/api.js index 42737d72b56..534c102971d 100644 --- a/services/bsky/api.js +++ b/services/bsky/api.js @@ -27,7 +27,6 @@ const { ServerConfig, BskyAppView, makeAlgos, - PeriodicModerationEventReversal, } = require('@atproto/bsky') const main = async () => { @@ -133,18 +132,9 @@ const main = async () => { algos, }) - const periodicModerationEventReversal = new PeriodicModerationEventReversal( - bsky.ctx, - ) - const periodicModerationEventReversalRunning = - periodicModerationEventReversal.run() - await bsky.start() // Graceful shutdown (see also https://aws.amazon.com/blogs/containers/graceful-shutdowns-with-ecs/) const shutdown = async () => { - // Gracefully shutdown periodic-moderation-event-reversal before destroying bsky instance - periodicModerationEventReversal.destroy() - await periodicModerationEventReversalRunning await bsky.destroy() } process.on('SIGTERM', shutdown) diff --git a/services/ozone/Dockerfile b/services/ozone/Dockerfile new file mode 100644 index 00000000000..04fa0e851c7 --- /dev/null +++ b/services/ozone/Dockerfile @@ -0,0 +1,55 @@ +FROM node:18-alpine as build + +RUN npm install -g pnpm + +# Move files into the image and install +WORKDIR /app +COPY ./*.* ./ +# NOTE ozones's transitive dependencies go here: if that changes, this needs to be updated. +COPY ./packages/ozone ./packages/ozone +COPY ./packages/api ./packages/api +COPY ./packages/common ./packages/common +COPY ./packages/common-web ./packages/common-web +COPY ./packages/crypto ./packages/crypto +COPY ./packages/identity ./packages/identity +COPY ./packages/syntax ./packages/syntax +COPY ./packages/lexicon ./packages/lexicon +COPY ./packages/xrpc ./packages/xrpc +COPY ./packages/xrpc-server ./packages/xrpc-server +COPY ./services/ozone ./services/ozone + +# install all deps +RUN pnpm install --frozen-lockfile > /dev/null +# build all packages with external node_modules +RUN ATP_BUILD_SHALLOW=true pnpm build > /dev/null +# update main with publishConfig +RUN pnpm update-main-to-dist > /dev/null +# clean up +RUN rm -rf node_modules +# install only prod deps, hoisted to root node_modules dir +RUN pnpm install --prod --shamefully-hoist --frozen-lockfile --prefer-offline > /dev/null + +WORKDIR services/ozone + +# Uses assets from build stage to reduce build size +FROM node:18-alpine + +RUN apk add --update dumb-init + +# Avoid zombie processes, handle signal forwarding +ENTRYPOINT ["dumb-init", "--"] + +WORKDIR /app/services/ozone +COPY --from=build /app /app + +EXPOSE 3000 +ENV PORT=3000 +ENV NODE_ENV=production + +# https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#non-root-user +USER node +CMD ["node", "--enable-source-maps", "api.js"] + +LABEL org.opencontainers.image.source=https://github.com/bluesky-social/atproto +LABEL org.opencontainers.image.description="Ozone" +LABEL org.opencontainers.image.licenses=MIT diff --git a/services/ozone/api.js b/services/ozone/api.js new file mode 100644 index 00000000000..f8d3f48f8f5 --- /dev/null +++ b/services/ozone/api.js @@ -0,0 +1,58 @@ +'use strict' /* eslint-disable */ + +require('dd-trace') // Only works with commonjs + .init({ logInjection: true }) + .tracer.use('express', { + hooks: { + request: (span, req) => { + maintainXrpcResource(span, req) + }, + }, + }) + +// Tracer code above must come before anything else +const path = require('path') +const { + OzoneService, + envToCfg, + envToSecrets, + readEnv, + httpLogger, +} = require('@atproto/ozone') + +const main = async () => { + const env = readEnv() + const cfg = envToCfg(env) + const secrets = envToSecrets(env) + const ozone = await OzoneService.create(cfg, secrets) + + await ozone.start() + + httpLogger.info('ozone is running') + + // Graceful shutdown (see also https://aws.amazon.com/blogs/containers/graceful-shutdowns-with-ecs/) + process.on('SIGTERM', async () => { + httpLogger.info('ozone is stopping') + + await ozone.destroy() + + httpLogger.info('ozone is stopped') + }) +} + +const maintainXrpcResource = (span, req) => { + // Show actual xrpc method as resource rather than the route pattern + if (span && req.originalUrl?.startsWith('/xrpc/')) { + span.setTag( + 'resource.name', + [ + req.method, + path.posix.join(req.baseUrl || '', req.path || '', '/').slice(0, -1), // Ensures no trailing slash + ] + .filter(Boolean) + .join(' '), + ) + } +} + +main() diff --git a/services/ozone/daemon.js b/services/ozone/daemon.js new file mode 100644 index 00000000000..a36df00d87f --- /dev/null +++ b/services/ozone/daemon.js @@ -0,0 +1,25 @@ +'use strict' /* eslint-disable */ + +require('dd-trace/init') // Only works with commonjs + +// Tracer code above must come before anything else +const { + OzoneDaemon, + envToCfg, + envToSecrets, + readEnv, +} = require('@atproto/ozone') + +const main = async () => { + const env = readEnv() + const cfg = envToCfg(env) + const secrets = envToSecrets(env) + const daemon = await OzoneDaemon.create(cfg, secrets) + + await daemon.start() + process.on('SIGTERM', async () => { + await daemon.destroy() + }) +} + +main() diff --git a/services/ozone/package.json b/services/ozone/package.json new file mode 100644 index 00000000000..bc959ff8e4d --- /dev/null +++ b/services/ozone/package.json @@ -0,0 +1,8 @@ +{ + "name": "ozone-service", + "private": true, + "dependencies": { + "@atproto/ozone": "workspace:^", + "dd-trace": "3.13.2" + } +} diff --git a/tsconfig.json b/tsconfig.json index e9a88ea1164..3307ccb7e4b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,6 +25,7 @@ "references": [ { "path": "./packages/pds/tsconfig.build.json" }, { "path": "./packages/bsky/tsconfig.build.json" }, + { "path": "./packages/ozone/tsconfig.build.json" }, { "path": "./packages/api/tsconfig.build.json" }, { "path": "./packages/aws/tsconfig.build.json" }, { "path": "./packages/common/tsconfig.build.json" },